MoonBit S7 Communication Protocol Library for Siemens PLC
Dependencies
Fully compatible with Rust s7 library API.
moon add RabitLogic/s7import "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")
}
}| Method | Description |
|---|---|
| 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_int | Read/write signed 16-bit Int |
| async ag_read_word(db, start) / ag_write_word | Read/write unsigned 16-bit Word |
| async ag_read_dint(db, start) / ag_write_dint | Read/write signed 32-bit DInt |
| async ag_read_dword(db, start) / ag_write_dword | Read/write unsigned 32-bit DWord (Int64) |
| async ag_read_bool(db, start, bit) / ag_write_bool | Read/write a single bit (Bool, bit 0..7) — atomic bit write |
| async ag_read_real(db, start) / ag_write_real | Read/write a 32-bit IEEE-754 REAL (returned as Double) |
| async ag_read_lreal(db, start) / ag_write_lreal | Read/write a 64-bit IEEE-754 LREAL (Double) |
| async eb_read/eb_write | Read/write process inputs (E/A) |
| async ab_read/ab_write | Read/write process outputs (A/A) |
| async mb_read/mb_write | Read/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/restart | PLC operating mode control |
| async plc_status | Get CPU status (Unknown/Stop/Run) |
| async cpu_info | Get CPU module info (SZL 0x001C) |
| async cp_info | Get CP/network info (SZL 0x0131) |
| async as_read_szl(id, index) | Read a raw SZL (System Status List) by id/index |
| is_connected | Check connection status |
// 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")| Function | Description |
|---|---|
| @protocol.encode_s7_string(max_len, s) -> Array[Byte] | Encode String → [max_len, cur_len, utf8...] |
| @protocol.decode_s7_string(bytes) -> String | Decode 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 |
// 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)| Field | Default | Description |
|---|---|---|
| host | "192.168.0.1" | PLC IP address |
| port | 102 | PLC port (ISO TCP) |
| rack | 0 | Rack number |
| slot | 1 | Slot number |
| conn_type | 1 | Connection type (PG/OP/Basic) |
| connect_timeout | 5000 | Connection timeout (ms) |
| read_timeout | 5000 | Read timeout (ms) |
| write_timeout | 5000 | Write timeout (ms) |
| use_tls | false | TLS (reserved for future use) |
| local_tsap | [0x01, 0x00] | Local TSAP |
| remote_tsap | [0x01, 0x01] | Remote TSAP |
// 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()}")
}┌─────────────────────────────────────┐
│ 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 │
└─────────────────────────────────────┘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| Target | Status | Backend |
|---|---|---|
| Linux | ✅ | native (official moonbitlang/async @io) |
| macOS | ✅ | native (official moonbitlang/async @io) |
| Windows | ✅ | native (official moonbitlang/async @io) |
The whole library is native-only: every package sets supported_targets = "+native". The wasm / wasm-gc / js targets are not supported — moonbitlang/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).
| Feature | Rust s7 | MoonBit S7 |
|---|---|---|
| Client API | 15 methods | 15 async methods |
| Transport trait | send + pdu_length + negotiate + connection_type | send + pdu_length + is_connected + disconnect + connection_type |
| Field types | Bool + Float + Double + Word | BoolField + FloatField + DoubleField + WordField |
| Field trait | trait Field | ✅ trait Field + FieldValue enum |
| PLC Control | start/stop/restart/plc_status | ✅ plc_start/stop/restart/plc_status |
| SZL | cpu_info/cp_info | ✅ cpu_info/cp_info |
| COTP | embedded in TCP | ✅ independent CotpConnection |
| I/O model | sync blocking | ⚡ async (moonbitlang/async @io) |
| Cross-platform | std library | official async library (no FFI, no C stubs) |
moon test# 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 nativeThe whole library is native-only, so always build/run with --target native (the default wasm-gc target builds nothing).
netsh advfirewall firewall add rule name="Allow ICMPv4" protocol=icmpv4:8,any dir=in action=allownetsh advfirewall firewall add rule name="Allow S7 ISO-TCP 102" dir=in action=allow protocol=TCP localport=102Tip: 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 layout12 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.
MoonBit S7 Communication Protocol Library for Siemens PLC
Dependencies