modbus
stableEncode and decode Modbus TCP and RTU protocol frames for industrial device communication, with support for coils, discrete inputs, and holding/input registers.
use plugin modbus::{encode_read_coils, encode_read_discrete_inputs, encode_read_holding_registers, …} Functions (14)
- encode_read_coils Encode Read Coils request (FC 0x01)
- encode_read_discrete_inputs Encode Read Discrete Inputs request (FC 0x02)
- encode_read_holding_registers Encode Read Holding Registers request (FC 0x03)
- encode_read_input_registers Encode Read Input Registers request (FC 0x04)
- encode_write_single_coil Encode Write Single Coil request (FC 0x05)
- encode_write_single_register Encode Write Single Register request (FC 0x06)
- encode_write_multiple_coils Encode Write Multiple Coils request (FC 0x0F)
- encode_write_multiple_registers Encode Write Multiple Registers request (FC 0x10)
- decode_response Decode a Modbus TCP response frame
- decode_registers Decode register values from response PDU data
- decode_coils Decode coil/discrete-input bits from response data
- encode_rtu_request Encode a Modbus RTU request with CRC-16
- crc16_modbus Compute CRC-16/Modbus checksum
- function_name Return human-readable name for a function code
Overview
modbus is a stateless codec for the Modbus protocol: it does not open sockets or
talk to devices itself, it only turns request parameters into the exact bytes a
device expects and turns response bytes back into Zolo values. You pair it with a
transport plugin (TCP for Modbus TCP, a serial link for Modbus RTU) — encode a
request, send the returned bytes over the wire, then feed the reply back into the
decoders. Every encoder returns a bytes frame and every decoder takes bytes,
so the plugin is just a pure translation layer between Zolo numbers/booleans and
the on-wire frame format.
Two framings are supported. Modbus TCP frames carry a 7-byte MBAP header
(auto-assigned transaction id, zero protocol id, length, and unit id) and are
produced by all the encode_* functions and parsed by decode_response. Modbus
RTU frames are raw unit_id + function + payload with a trailing little-endian
CRC-16, built by encode_rtu_request and verifiable with crc16_modbus. Coil and
register payloads from a response body are unpacked with decode_coils and
decode_registers.
Common patterns
Build a Read Holding Registers request, then parse the device reply and unpack the 16-bit values:
use plugin modbus::{encode_read_holding_registers, decode_response, decode_registers}
let request = encode_read_holding_registers(1, 0, 2)
// ...send `request` over TCP and read the reply bytes...
let reply = decode_response(request)
if reply["is_error"] {
print("device returned exception")
} else {
let regs = decode_registers(reply["data"])
print("reg1={regs[1]} reg2={regs[2]}")
}
Toggle a coil and confirm the function code by name:
use plugin modbus::{encode_write_single_coil, decode_response, function_name}
let frame = encode_write_single_coil(1, 0, true)
let reply = decode_response(frame)
print("{function_name(reply["function_code"])}: unit {reply["unit_id"]}")
Build a serial RTU request and check its trailing CRC:
use plugin modbus::{encode_rtu_request, crc16_modbus}
let frame = encode_rtu_request(1, 0x03, 0, 10)
let body = [0x01, 0x03, 0x00, 0x00, 0x00, 0x0A]
print("expected CRC: {crc16_modbus(body)}")
print("RTU frame: {frame}")
Encode Read Coils request (FC 0x01)
Encodes a Modbus TCP Read Coils request (function code 0x01) with a full MBAP header. unit_id identifies the slave device; address is the starting coil address; count is the number of coils to read.
use plugin modbus::{encode_read_coils, decode_response, decode_coils}
let frame = encode_read_coils(1, 0, 8)
print("frame bytes: {frame}")
Read 16 coils starting at a higher address — count is the number of bits, not
bytes, so the device packs the result into two bytes:
use plugin modbus::{encode_read_coils}
let frame = encode_read_coils(2, 200, 16)
print("requesting 16 coils from unit 2")
Encode Read Discrete Inputs request (FC 0x02)
Encodes a Read Discrete Inputs request (function code 0x02). Discrete inputs are read-only single-bit values, typically connected to physical sensors.
use plugin modbus::{encode_read_discrete_inputs}
let frame = encode_read_discrete_inputs(1, 100, 4)
Encode Read Holding Registers request (FC 0x03)
Encodes a Read Holding Registers request (function code 0x03) for reading 16-bit read/write registers from a slave device.
use plugin modbus::{encode_read_holding_registers, decode_response, decode_registers}
let frame = encode_read_holding_registers(1, 0, 10)
Encode Read Input Registers request (FC 0x04)
Encodes a Read Input Registers request (function code 0x04) for reading 16-bit read-only registers, typically from sensors or analog inputs.
use plugin modbus::{encode_read_input_registers}
let frame = encode_read_input_registers(1, 200, 2)
Encode Write Single Coil request (FC 0x05)
Encodes a Write Single Coil request (function code 0x05). value is a boolean: true turns the coil ON (0xFF00), false turns it OFF (0x0000).
use plugin modbus::{encode_write_single_coil}
let turn_on = encode_write_single_coil(1, 0, true)
let turn_off = encode_write_single_coil(1, 0, false)
Encode Write Single Register request (FC 0x06)
Encodes a Write Single Register request (function code 0x06) to write a single 16-bit value to a holding register.
use plugin modbus::{encode_write_single_register}
let frame = encode_write_single_register(1, 40001, 1234)
Encode Write Multiple Coils request (FC 0x0F)
Encodes a Write Multiple Coils request (function code 0x0F). values is a table of booleans, packed into bytes LSB-first per the Modbus spec.
use plugin modbus::{encode_write_multiple_coils}
let frame = encode_write_multiple_coils(1, 0, [true, false, true, true, false, false, true, false])
Encode Write Multiple Registers request (FC 0x10)
Encodes a Write Multiple Registers request (function code 0x10). values is a table of integers, each written as a 16-bit big-endian word.
use plugin modbus::{encode_write_multiple_registers}
let frame = encode_write_multiple_registers(1, 40000, [100, 200, 300])
Writing a single-element table still uses function code 0x10 (multiple), which some PLCs require even for one register:
use plugin modbus::{encode_write_multiple_registers}
let setpoint = encode_write_multiple_registers(1, 40010, [4200])
print("setpoint frame ready")
Decode a Modbus TCP response frame
Decodes a Modbus TCP response frame into {transaction_id, unit_id, function_code, is_error, data}. If is_error is true, function_code has the high bit set and the error code is in data[1].
use plugin modbus::{encode_read_holding_registers, decode_response}
let request = encode_read_holding_registers(1, 0, 2)
let response = decode_response(request)
print("fc={response["function_code"]} error={response["is_error"]}")
The transaction_id field lets you correlate a reply with the request that
produced it when several are in flight:
use plugin modbus::{encode_read_coils, decode_response}
let req = encode_read_coils(1, 0, 4)
let resp = decode_response(req)
print("tx {resp["transaction_id"]} from unit {resp["unit_id"]}")
Decode register values from response PDU data
Decodes the PDU data portion of a read-registers response into a table of integer register values. Pass the data field from decode_response.
use plugin modbus::{decode_registers}
let pdu_data = [0x04, 0x00, 0x0A, 0x00, 0x14]
let regs = decode_registers(pdu_data)
print(regs[1])
print(regs[2])
Decode coil/discrete-input bits from response data
Decodes the PDU data portion of a read-coils or read-discrete-inputs response into a table of booleans. count specifies how many coil values to extract.
use plugin modbus::{decode_coils}
let pdu_data = [0x01, 0b10110101]
let coils = decode_coils(pdu_data, 8)
print(coils[1])
print(coils[2])
Encode a Modbus RTU request with CRC-16
Encodes a Modbus RTU request frame: unit_id + function + address (2 BE) + count (2 BE) + CRC-16 (2 LE). Use for serial (RS-485/RS-232) Modbus communication instead of TCP.
use plugin modbus::{encode_rtu_request}
let frame = encode_rtu_request(1, 0x03, 0, 10)
print("RTU frame: {frame}")
Compute CRC-16/Modbus checksum
Computes the CRC-16/Modbus checksum (polynomial 0xA001, initial value 0xFFFF) for the given byte sequence. Use to verify RTU frame integrity.
use plugin modbus::{crc16_modbus}
let data = [0x01, 0x03, 0x00, 0x00, 0x00, 0x0A]
let crc = crc16_modbus(data)
print("CRC: {crc}")
Split the checksum into its on-wire little-endian bytes (low byte first) to
match how encode_rtu_request appends it:
use plugin modbus::{crc16_modbus}
let crc = crc16_modbus([0x01, 0x06, 0x00, 0x01, 0x00, 0x03])
let lo = crc % 256
let hi = crc / 256
print("CRC bytes: {lo} {hi}")
Return human-readable name for a function code
Returns the human-readable name for a Modbus function code integer. Returns "Unknown" for unrecognised codes.
use plugin modbus::{function_name}
print(function_name(0x03))
print(function_name(0x10))
print(function_name(0xFF))