s7

MoonBit S7 Communication Protocol Library for Siemens PLC

moon add RabitLogic/s7@0.2.6
Download zip
Version
0.2.6
License
MIT
Last updated
6 days ago
Downloads
25

Dependencies

README

#MoonBit S7

License: MIT MoonBit Version

A pure MoonBit implementation of the Siemens S7 communication protocol for PLC devices.

Fully compatible with Rust s7 library API.

#Features

  • S7 Client APIag_read/write, eb_read/write, ab_read/write, mb_read/write (15 methods)
  • PLC Controlplc_start/stop/restart, plc_status
  • SZL Diagnosticscpu_info, cp_info
  • COTP — ISO Connection Request/Confirm (independent layer)
  • PDU Chunking — Automatic large read/write splitting
  • Field TypesBoolField, WordField, FloatField, DoubleField with Field trait
  • Field CollectionsFieldValue enum for heterogeneous field arrays
  • 100% Official Async I/O — transport runs entirely on moonbitlang/async (@io), no raw FFI, no C stubs
  • Robust transport — TPKT-framed reads via read_exactly (handles partial TCP segments and large responses), with configurable connect/read/write timeouts
  • Native only — Linux, macOS, Windows; the whole library targets native exclusively (no wasm/js support)

#Quick Start

#Add dependency

moon add RabitLogic/s7

#Basic usage

import "RabitLogic/s7/client"
import "RabitLogic/s7/transport"

async fn main {
// Create client with default config (192.168.0.1:102)
let cl = @client.S7Client::new()

// Connect to PLC (TCP + COTP + PDU negotiation)
let conn = match cl.connect() {
Err(e) => {
println("connect failed: \{e.to_string()}")
return
}
Ok(c) => c
}
defer conn.disconnect() // auto-disconnect on scope exit

// Read 10 bytes from DB 1, starting at offset 0
match conn.ag_read(1, 0, 10) {
Err(e) => println("read failed: \{e.to_string()}")
Ok(data) => println("Read \{data.length()} bytes")
}
}

#API Reference

#S7Client

MethodDescription
new() / new_with_config(c)Create client
async connect()Connect to PLC (TCP + COTP + PDU negotiation)
disconnect()Disconnect
async ag_read(db, start, size)Read from data block (DB)
async ag_write(db, start, data)Write to data block
async ag_read_string(db, start, max_len)Read an S7 STRING field → String
async ag_write_string(db, start, max_len, value)Write a String to an S7 STRING field (UTF-8)
async ag_read_int(db, start) / ag_write_intRead/write signed 16-bit Int
async ag_read_word(db, start) / ag_write_wordRead/write unsigned 16-bit Word
async ag_read_dint(db, start) / ag_write_dintRead/write signed 32-bit DInt
async ag_read_dword(db, start) / ag_write_dwordRead/write unsigned 32-bit DWord (Int64)
async ag_read_bool(db, start, bit) / ag_write_boolRead/write a single bit (Bool, bit 0..7) — atomic bit write
async ag_read_real(db, start) / ag_write_realRead/write a 32-bit IEEE-754 REAL (returned as Double)
async ag_read_lreal(db, start) / ag_write_lrealRead/write a 64-bit IEEE-754 LREAL (Double)
async eb_read/eb_writeRead/write process inputs (E/A)
async ab_read/ab_writeRead/write process outputs (A/A)
async mb_read/mb_writeRead/write flags/markers (M)
async tm_read(n, count) / ct_read(n, count)Read count timers/counters (2 bytes each) starting at T{n}/C{n}
async plc_start/stop/restartPLC operating mode control
async plc_statusGet CPU status (Unknown/Stop/Run)
async cpu_infoGet CPU module info (SZL 0x001C)
async cp_infoGet CP/network info (SZL 0x0131)
async as_read_szl(id, index)Read a raw SZL (System Status List) by id/index
is_connectedCheck connection status

#Working with S7 STRING fields (elegant)

Read/write a String field without touching bytes:

// Read the `code` field (String, max 254) at offset 4 of DB200
match conn.ag_read_string(200, 4, 254) {
Ok(s) => println("code = '\{s}'")
}

// Write a string (UTF-8 encoded, truncated to max_len)
let _ = conn.ag_write_string(200, 4, 254, "Hello S7")

Protocol helpers (also used internally):

FunctionDescription
@protocol.encode_s7_string(max_len, s) -> Array[Byte]Encode String[max_len, cur_len, utf8...]
@protocol.decode_s7_string(bytes) -> StringDecode raw S7 STRING bytes → String
@protocol.StringField::new(db, off, max_len, value)Structured string field
StringField::to_bytes() / from_bytes(db, off, max_len, data) / value()Encode/decode field

#Working with numeric fields

MoonBit's Int is 32-bit signed, so unsigned 32-bit values (S7 DWORD) need Int64. Use the typed accessors:

// signed 32-bit
let di = conn.ag_read_dint(200, 104) // -> Int (-100000)
let _ = conn.ag_write_dint(200, 104, -100000)

// unsigned 32-bit — correctly returns 4000000000 (not a negative wrap)
let dw = conn.ag_read_dword(200, 108) // -> Int64 (4000000000)
let _ = conn.ag_write_dword(200, 108, 4000000000L)

Protocol field types: BoolField (bit), WordField (u16), DIntField (i32), DWordField (u32 → Int64), FloatField (f32), DoubleField (f64), StringField.

#TcpConfig

FieldDefaultDescription
host"192.168.0.1"PLC IP address
port102PLC port (ISO TCP)
rack0Rack number
slot1Slot number
conn_type1Connection type (PG/OP/Basic)
connect_timeout5000Connection timeout (ms)
read_timeout5000Read timeout (ms)
write_timeout5000Write timeout (ms)
use_tlsfalseTLS (reserved for future use)
local_tsap[0x01, 0x00]Local TSAP
remote_tsap[0x01, 0x01]Remote TSAP

#Field Types

// Bit field
let bf = BoolField::new(1, 0, 0) // DB1, byte 0, bit 0
let val = bf.value() // get bit value
let toggled = bf.set_value(true) // set bit (returns new field)

// Word (u16)
let wf = WordField::new(1, 2, bytes) // DB1, offset 2
let wval = wf.value() // 16-bit unsigned value

// Float (IEEE 754 f32)
let ff = FloatField::new(1, 4, bytes) // DB1, offset 4
let fval = ff.value() // 32-bit float (as Double)

// Double (IEEE 754 f64)
let df = DoubleField::new(1, 8, bytes) // DB1, offset 8
let dval = df.value() // 64-bit float

// FieldValue — heterogeneous collection
let fields : Array[FieldValue] = [
FBool(BoolField::new(1, 0, 0)),
FFloat(FloatField::new(1, 4, bytes)),
]
for f in fields {
println("DB\{f.data_block()} @ \{f.offset()}")
}

#Architecture

┌─────────────────────────────────────┐ │ S7Client │ │ ┌───────────────────────────────┐ │ │ │ TcpConnection │ │ │ │ ┌─────────────────────────┐ │ │ │ │ │ @socket.Tcp (async) │ │ │ │ │ │ + @io read/write │ │ │ │ │ └─────────────────────────┘ │ │ │ └───────────────────────────────┘ │ │ ┌───────────────────────────────┐ │ │ │ CotpConnection │ │ │ └───────────────────────────────┘ │ └─────────────────────────────────────┘ ↕ ┌─────────────────────────────────────┐ │ S7 Protocol Layer │ │ build_read_pdu / parse_response │ │ build_write_pdu / build_setup_pdu │ │ SZL, PLC control, COTP │ └─────────────────────────────────────┘

#Project Structure

s7/ ├── moon.mod # Module configuration ├── core/ │ ├── error.mbt # S7Error + S7Result │ └── lib.mbt # Version info ├── client/ │ └── client.mbt # S7Client (15 async methods) ├── transport/ │ ├── transport.mbt # Transport trait + ConnType │ ├── tcp.mbt # TcpConnection (official async @io) │ └── cotp.mbt # CotpConnection ├── protocol/ │ ├── types.mbt # DataType, Area, CpuStatus, CpuInfo, CPInfo │ ├── items.mbt # ReadItem, WriteItem │ ├── s7comm.mbt # PDU encode/decode, SZL, PLC control │ ├── field.mbt # BoolField, WordField, FloatField, DoubleField │ └── constant.mbt # WL_*, TS_*, Area hex, data_size_byte ├── utils/ │ └── helpers.mbt # err_to_string, hex_dump, cpu_status_to_string ├── tools/ │ └── s7_sim.py # Local S7 PLC simulator for testing └── test_plc/ └── main.mbt # Demo program

#Platform Support

TargetStatusBackend
Linuxnative (official moonbitlang/async @io)
macOSnative (official moonbitlang/async @io)
Windowsnative (official moonbitlang/async @io)

The whole library is native-only: every package sets supported_targets = "+native". The wasm / wasm-gc / js targets are not supportedmoonbitlang/async provides no real socket implementation for them, so only native can talk to a PLC. Build with moon build --target native (and moon run test_plc --target native).

#Comparison with Rust s7

The MoonBit S7 library is fully feature-aligned with the Rust s7 library:

FeatureRust s7MoonBit S7
Client API15 methods15 async methods
Transport traitsend + pdu_length + negotiate + connection_typesend + pdu_length + is_connected + disconnect + connection_type
Field typesBool + Float + Double + WordBoolField + FloatField + DoubleField + WordField
Field traittrait Fieldtrait Field + FieldValue enum
PLC Controlstart/stop/restart/plc_status✅ plc_start/stop/restart/plc_status
SZLcpu_info/cp_info✅ cpu_info/cp_info
COTPembedded in TCP✅ independent CotpConnection
I/O modelsync blocking⚡ async (moonbitlang/async @io)
Cross-platformstd libraryofficial async library (no FFI, no C stubs)

#Testing

#Unit tests

moon test

#Functional test against a simulated PLC

The repository ships a small Python S7 simulator (tools/s7_sim.py) that emulates a Siemens PLC over the wire: TCP + COTP handshake, PDU-size negotiation, PLC status, SZL (CPU info) reads, and DB read/write. You can exercise the full client against it without real hardware:

# 1. Start the simulator (listens on 127.0.0.1:102; note: port 102 is privileged — # if you are not root, bind a high port like 1102 and point test_plc at it) python tools/s7_sim.py 127.0.0.1 102 # 2. Point test_plc at the simulator and run it (native target!) moon run test_plc --target native

The whole library is native-only, so always build/run with --target native (the default wasm-gc target builds nothing).

The demo program verifies the whole stack end-to-end: connect, plc_status (RUN), cpu_info (module type / serial / AS name / copyright / module name), DB read, DB write and read-back, and a non-zero-offset read. To test against your own PLC, edit host/port in test_plc/main.mbt (currently set to 192.168.1.40:102, a S7-PLCSIM Advanced instance in Bridged mode).

The simulator serves a simulated DB200 whose first bytes are initialized to 0x00, 0x11, 0x22, ...; the demo writes 0xAA 0xBB to it and reads it back to confirm persistence.

#Connectivity & firewall notes

When testing against a real PLC or a VM on your LAN, ping often fails even though the device is reachable at layer 2 (ARP works) — usually because the Windows firewall blocks ICMP echo. To allow ping on a Windows target (verified):

netsh advfirewall firewall add rule name="Allow ICMPv4" protocol=icmpv4:8,any dir=in action=allow

To also allow the S7 protocol itself (ISO-on-TCP, port 102) inbound on that machine:

netsh advfirewall firewall add rule name="Allow S7 ISO-TCP 102" dir=in action=allow protocol=TCP localport=102

Tip: if a VM is not reachable at the expected LAN address, check its virtual NIC mode. In NAT mode the VM sits on a different subnet (e.g. VMware VMnet8 = 192.168.47.x) and a static LAN IP like 192.168.1.11 won't be reachable — switch the NIC to Bridged mode so it joins the host LAN.

Note: S7-1200/1500 DBs are optimized by default ("Optimized block access"). Optimized DBs cannot be read/written with absolute addresses — the PLC returns an "item not found" error. Disable "Optimized block access" in the DB properties in TIA Portal and download the project to the PLC again.

S7-1500 / PLCSIM Advanced read-item format (important if a real PLC rejects every absolute read): this library uses the modern S7-1500 read item layout

12 0A 10 <WL> <count> <db> <area> <addr2> <addr1> <addr0>

(transport marker 0x10, word length, element count big-endian, DB, area, 24-bit address big-endian). The classic snap7 layout (12 0A <TS> <db> <area> <addr> 00 00 <count>) is rejected by S7-PLCSIM Advanced with "object not found" even for absolute M/I/Q areas. This was verified byte-for-byte against the Rust s7 crate, which reads DB200 fine from PLCSIM. The response parser also reads the item return code at the fixed offset the PLC returns (full-frame byte 21) and the payload from byte 25.

#License

MIT License — see LICENSE