Modbus Helpers
Codec Auto-Parsed Values
Some codecs automatically parse protocol responses and attach typed values to messages. These values are available in
on_receive() via message:get_value(id) and message.values_json without writing any Lua parsing code.
Modbus RTU Codec
When modbus_rtu codec is selected, responses are automatically correlated with prior requests and parsed into values.
Each register/coil produces two keys — a logical Modicon address and a protocol-type hex address:
| Function Code | Logical Key | Protocol-Type Key | Value Type |
|---|---|---|---|
| 0x01 Read Coils | modbus_rtu_{slave}:{1+addr} | modbus_rtu_{slave}:coil_{addr:04X} | boolean |
| 0x02 Read Discrete Inputs | modbus_rtu_{slave}:{10001+addr} | modbus_rtu_{slave}:discrete_{addr:04X} | boolean |
| 0x03 Read Holding Registers | modbus_rtu_{slave}:{40001+addr} | modbus_rtu_{slave}:holding_{addr:04X} | uint16 |
| 0x04 Read Input Registers | modbus_rtu_{slave}:{30001+addr} | modbus_rtu_{slave}:input_{addr:04X} | uint16 |
| 0x05 Write Single Coil | modbus_rtu_{slave}:{1+addr} | modbus_rtu_{slave}:coil_{addr:04X} | boolean |
| 0x06 Write Single Register | modbus_rtu_{slave}:{40001+addr} | modbus_rtu_{slave}:holding_{addr:04X} | uint16 |
{slave}is the Modbus slave address (decimal){addr}is the 0-based protocol address- Logical keys use the Modicon convention (decimal): coils start at 1, discrete inputs at 10001, input registers at 30001, holding registers at 40001
- Protocol-type keys use 4-digit uppercase hex for the address (e.g.,
coil_0000,holding_0003) - Both keys are emitted for every parsed value; use whichever is more convenient
- Values are only parsed when the codec can match a response to a prior request
- Integer/float values can be visualized on charts in the UI
Modbus TCP Codec
Same as RTU but with modbus_tcp prefix and unit ID from MBAP header. Each value also produces two keys:
| Function Code | Logical Key | Protocol-Type Key | Value Type |
|---|---|---|---|
| 0x01 Read Coils | modbus_tcp_{unit}:{1+addr} | modbus_tcp_{unit}:coil_{addr:04X} | boolean |
| 0x02 Read Discrete Inputs | modbus_tcp_{unit}:{10001+addr} | modbus_tcp_{unit}:discrete_{addr:04X} | boolean |
| 0x03 Read Holding Registers | modbus_tcp_{unit}:{40001+addr} | modbus_tcp_{unit}:holding_{addr:04X} | uint16 |
| 0x04 Read Input Registers | modbus_tcp_{unit}:{30001+addr} | modbus_tcp_{unit}:input_{addr:04X} | uint16 |
| 0x05 Write Single Coil | modbus_tcp_{unit}:{1+addr} | modbus_tcp_{unit}:coil_{addr:04X} | boolean |
| 0x06 Write Single Register | modbus_tcp_{unit}:{40001+addr} | modbus_tcp_{unit}:holding_{addr:04X} | uint16 |
Example: Senseair S8 CO₂ Sensor
Auto-parsed values coexist with manually added values via message:add_int_value(), message:add_float_value(), etc.
Use Lua to combine or transform auto-parsed uint16 registers into meaningful values (e.g., combining two registers into
a 32-bit value). The example below polls a Senseair S8 CO₂ sensor and demonstrates periodic polling,
value parsing, multi-register combination, and status/alarm decoding.
local SLAVE_ADDR = 254
local POLL_MS = 2000
local timer_ms = 0
function on_start()
log("info", "Starting Senseair S8 Modbus polling script.")
-- Read device info registers once at startup (Map Ver, FW Ver, Sensor ID Hi, Sensor ID Lo).
-- A non-zero delay is given to each subsequent call so the script waits for the previous
-- request's response before sending the next one. Without staggered delays, requests would
-- be queued back-to-back and the device may not respond correctly to rapid consecutive reads.
modbus_rtu_read_input_registers(SLAVE_ADDR, 0x001B, 4, 100, 0)
-- Send the next request 200 ms after the previous one, giving the device time to respond
modbus_rtu_read_holding_registers(SLAVE_ADDR, 0x001F, 1, 200, 0)
end
function on_timer(now_ms)
-- on_timer fires every 100 ms; accumulate elapsed time manually
timer_ms = timer_ms + 100
-- Periodically read active data (CO2, status, alarms) every POLL_MS milliseconds.
-- Delay is 0 here because this is the only request in the polling cycle.
if timer_ms >= POLL_MS then
-- Read 4 registers at 0x0000: MeterStatus, AlarmStatus, OutputStatus, SpaceCO2
modbus_rtu_read_input_registers(SLAVE_ADDR, 0x0000, 4, 0, 0)
timer_ms = 0
end
end
function on_receive()
-- Only process messages from connection 0 (the primary serial port)
if message.connection_id ~= 0 then return false end
local modified = false
-- Space CO2 concentration (protocol address 0x0003, logical address 30004)
local co2 = message:get_value(string.format("modbus_rtu_%d:input_0003", SLAVE_ADDR))
if co2 then
message:add_int_value("CO2_ppm", co2)
modified = true
end
-- MeterStatus register (protocol address 0x0000): bit-field error flags
local status = message:get_value(string.format("modbus_rtu_%d:input_0000", SLAVE_ADDR))
if status then
message:add_int_value("MeterStatus", status)
if status ~= 0 then
-- Bit 0: fatal internal error; Bit 5: measurement out of valid range
local fatal = bit.band(status, 0x01)
local out_of_range = bit.band(status, 0x20)
if fatal > 0 then log("error", "Sensor reported FATAL ERROR (bit 0)") end
if out_of_range > 0 then log("warn", "Sensor reading OUT OF RANGE (bit 5)") end
end
modified = true
end
-- OutputStatus / alarm flags (protocol address 0x0002): bit 0 reflects active alarm state
local out_status = message:get_value(string.format("modbus_rtu_%d:input_0002", SLAVE_ADDR))
if out_status then
local alarm = (bit.band(out_status, 0x01) > 0)
message:add_bool_value("Alarm_Active", alarm)
modified = true
end
-- Firmware version (protocol address 0x001C): high byte = major, low byte = minor
local fw = message:get_value(string.format("modbus_rtu_%d:input_001C", SLAVE_ADDR))
if fw then
local main_ver = bit.rshift(fw, 8)
local sub_ver = bit.band(fw, 0xFF)
message:add_string_value("Firmware_Version", string.format("%d.%d", main_ver, sub_ver))
modified = true
end
-- Sensor ID: two consecutive 16-bit registers combined into a 32-bit value.
-- id_hi (0x001D) holds the upper 16 bits; id_lo (0x001E) holds the lower 16 bits.
-- Both must be present in the same message (they come from a single multi-register read).
local id_hi = message:get_value(string.format("modbus_rtu_%d:input_001D", SLAVE_ADDR))
local id_lo = message:get_value(string.format("modbus_rtu_%d:input_001E", SLAVE_ADDR))
if id_hi and id_lo then
local sensor_id = id_hi * 65536 + id_lo
message:add_int_value("Sensor_ID", sensor_id)
modified = true
end
-- ABC (Automatic Baseline Correction) period in hours (holding register 0x001F)
local abc = message:get_value(string.format("modbus_rtu_%d:holding_001F", SLAVE_ADDR))
if abc then
message:add_int_value("ABC_Period_Hours", abc)
modified = true
end
-- Return true only when at least one value was added, so the engine
-- knows the message payload has been enriched and should be forwarded
return modified
end
Key patterns:
- Staggered startup reads: Each
modbus_rtu_read_*call inon_startuses a non-zerodelayso the next request is not queued until the device has had time to respond to the previous one. Without staggered delays, rapid back-to-back requests can cause devices to drop or misframe responses. - Timer accumulation:
on_timerfires every 100 ms; elapsed time is accumulated manually to achieve a polling period longer than the minimum callback interval. - Protocol-type hex keys:
input_0003,holding_001F, etc. use the 4-digit uppercase hex form of the 0-based protocol address — easier to cross-reference with a device datasheet than Modicon addresses. - Multi-register 32-bit value: Two adjacent uint16 registers are combined with
id_hi * 65536 + id_loto reconstruct a 32-bit sensor ID. return modified: Returningtrueonly when values were added avoids unnecessary engine work for messages that belong to other devices or function codes.
Modbus RTU Helpers
All RTU functions return true on success, false on failure. Invalid parameters raise a Lua runtime error.
All accept an optional trailing connection_id (default: 0).
| Function | Parameters |
|---|---|
modbus_rtu_read_coils(slave, start, qty, delay, connection_id) | slave: 0-255, start: 0-65535, qty: 1-2000, delay: ms |
modbus_rtu_read_discrete_inputs(slave, start, qty, delay, connection_id) | slave: 0-255, start: 0-65535, qty: 1-2000, delay: ms |
modbus_rtu_read_holding_registers(slave, start, qty, delay, connection_id) | slave: 0-255, start: 0-65535, qty: 1-125, delay: ms |
modbus_rtu_read_input_registers(slave, start, qty, delay, connection_id) | slave: 0-255, start: 0-65535, qty: 1-125, delay: ms |
modbus_rtu_write_single_coil(slave, addr, value, delay, connection_id) | slave: 0-255, addr: 0-65535, value: boolean, delay: ms |
modbus_rtu_write_single_register(slave, addr, value, delay, connection_id) | slave: 0-255, addr: 0-65535, value: 0-65535, delay: ms |
modbus_rtu_write_multiple_coils(slave, start, values_table, delay, connection_id) | slave: 0-255, start: 0-65535, values_table: boolean array (1-2000), delay: ms |
modbus_rtu_write_multiple_registers(slave, start, values_table, delay, connection_id) | slave: 0-255, start: 0-65535, values_table: integer array (1-125, each 0-65535), delay: ms |
Modbus TCP Helpers
Same as RTU but without slave parameter (unit ID is in MBAP header). All return true/false.
All accept an optional trailing connection_id (default: 0).
| Function | Parameters |
|---|---|
modbus_tcp_read_coils(start, qty, delay, connection_id) | start: 0-65535, qty: 1-2000, delay: ms |
modbus_tcp_read_discrete_inputs(start, qty, delay, connection_id) | start: 0-65535, qty: 1-2000, delay: ms |
modbus_tcp_read_holding_registers(start, qty, delay, connection_id) | start: 0-65535, qty: 1-125, delay: ms |
modbus_tcp_read_input_registers(start, qty, delay, connection_id) | start: 0-65535, qty: 1-125, delay: ms |
modbus_tcp_write_single_coil(addr, value, delay, connection_id) | addr: 0-65535, value: boolean, delay: ms |
modbus_tcp_write_single_register(addr, value, delay, connection_id) | addr: 0-65535, value: 0-65535, delay: ms |
modbus_tcp_write_multiple_coils(start, values_table, delay, connection_id) | start: 0-65535, values_table: boolean array (1-2000), delay: ms |
modbus_tcp_write_multiple_registers(start, values_table, delay, connection_id) | start: 0-65535, values_table: integer array (1-125, each 0-65535), delay: ms |