Skip to content

modbus

stable

Encode 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, …}
14 functions Systems
/ filter jk navigate Esc clear
Functions (14)
  1. encode_read_coils Encode Read Coils request (FC 0x01)
  2. encode_read_discrete_inputs Encode Read Discrete Inputs request (FC 0x02)
  3. encode_read_holding_registers Encode Read Holding Registers request (FC 0x03)
  4. encode_read_input_registers Encode Read Input Registers request (FC 0x04)
  5. encode_write_single_coil Encode Write Single Coil request (FC 0x05)
  6. encode_write_single_register Encode Write Single Register request (FC 0x06)
  7. encode_write_multiple_coils Encode Write Multiple Coils request (FC 0x0F)
  8. encode_write_multiple_registers Encode Write Multiple Registers request (FC 0x10)
  9. decode_response Decode a Modbus TCP response frame
  10. decode_registers Decode register values from response PDU data
  11. decode_coils Decode coil/discrete-input bits from response data
  12. encode_rtu_request Encode a Modbus RTU request with CRC-16
  13. crc16_modbus Compute CRC-16/Modbus checksum
  14. 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))
enespt-br