Modbus Helpers
Modbus helper functions provide convenient Lua APIs for sending Modbus RTU and Modbus TCP requests. These functions automatically handle protocol framing, parameter validation, and codec-specific payload formatting.
Overview
The Modbus helpers support 8 standard Modbus function codes covering the most common industrial automation operations:
| Function Code | Operation | Helper Function |
|---|---|---|
| 0x01 | Read Coils | modbus_read_coils() |
| 0x02 | Read Discrete Inputs | modbus_read_discrete_inputs() |
| 0x03 | Read Holding Registers | modbus_read_holding_registers() |
| 0x04 | Read Input Registers | modbus_read_input_registers() |
| 0x05 | Write Single Coil | modbus_write_single_coil() |
| 0x06 | Write Single Register | modbus_write_single_register() |
| 0x0F | Write Multiple Coils | modbus_write_multiple_coils() |
| 0x10 | Write Multiple Registers | modbus_write_multiple_registers() |
Automatic RTU/TCP Detection
All helpers automatically detect the codec type (modbus_rtu_codec or modbus_tcp_codec) and format payloads accordingly:
- Modbus RTU: Includes slave address in payload
- Modbus TCP: Omits slave address (handled by MBAP header)
Multi-Connection Support
All functions accept an optional connection_id parameter to target specific connections. If omitted, messages are sent to connection 0 (default connection).
API Reference
modbus_read_coils(slave, start, qty, delay, connection_id) → boolean
Read coil status (function code 0x01).
Parameters:
slave(number) - Slave/unit ID (1-247)start(number) - Starting coil address (0-65535)qty(number) - Number of coils to read (1-2000)delay(number) - Delay in milliseconds before sendingconnection_id(number, optional) - Target connection ID (default: 0)
Returns:
true- Request scheduled successfullyfalse- Failed to schedule (channel closed or error)
Example:
-- Read 10 coils starting at address 100 from slave 1
local last_poll_time = 0
function on_timer(elapsed_ms)
if elapsed_ms - last_poll_time >= 1000 then -- Poll every second
modbus_read_coils(1, 100, 10, 0) -- Send to connection 0
last_poll_time = elapsed_ms
end
end
-- Multi-connection example: Poll different slaves on different connections
local last_multi_poll = 0
function on_timer(elapsed_ms)
if elapsed_ms - last_multi_poll >= 1000 then
modbus_read_coils(1, 100, 10, 0, 0) -- Slave 1 on connection 0
modbus_read_coils(2, 100, 10, 0, 1) -- Slave 2 on connection 1
last_multi_poll = elapsed_ms
end
end
modbus_read_discrete_inputs(slave, start, qty, delay, connection_id) → boolean
Read discrete input status (function code 0x02).
Parameters:
slave(number) - Slave/unit ID (1-247)start(number) - Starting input address (0-65535)qty(number) - Number of inputs to read (1-2000)delay(number) - Delay in milliseconds before sendingconnection_id(number, optional) - Target connection ID (default: 0)
Returns:
true- Request scheduled successfullyfalse- Failed to schedule
Example:
-- Read 8 discrete inputs from address 0
function on_start()
modbus_read_discrete_inputs(1, 0, 8, 100) -- Read after 100ms
end
modbus_read_holding_registers(slave, start, qty, delay, connection_id) → boolean
Read holding register values (function code 0x03).
Parameters:
slave(number) - Slave/unit ID (1-247)start(number) - Starting register address (0-65535)qty(number) - Number of registers to read (1-125)delay(number) - Delay in milliseconds before sendingconnection_id(number, optional) - Target connection ID (default: 0)
Returns:
true- Request scheduled successfullyfalse- Failed to schedule
Example:
-- Read 10 holding registers starting at address 40001 (Modbus address 0)
local last_poll_time = 0
function on_timer(elapsed_ms)
if elapsed_ms - last_poll_time >= 500 then
modbus_read_holding_registers(1, 0, 10, 0)
last_poll_time = elapsed_ms
end
end
-- Multi-register read with connection routing
local last_multi_read = 0
function on_timer(elapsed_ms)
if elapsed_ms - last_multi_read >= 1000 then
local count = get_connection_count()
for conn_id = 0, count - 1 do
if get_codec(conn_id) == "modbus_rtu" then
modbus_read_holding_registers(1, 0, 10, 0, conn_id)
end
end
last_multi_read = elapsed_ms
end
end
modbus_read_input_registers(slave, start, qty, delay, connection_id) → boolean
Read input register values (function code 0x04).
Parameters:
slave(number) - Slave/unit ID (1-247)start(number) - Starting register address (0-65535)qty(number) - Number of registers to read (1-125)delay(number) - Delay in milliseconds before sendingconnection_id(number, optional) - Target connection ID (default: 0)
Returns:
true- Request scheduled successfullyfalse- Failed to schedule
Example:
-- Read 5 input registers (sensor data)
function on_start()
modbus_read_input_registers(1, 0, 5, 0)
end
modbus_write_single_coil(slave, addr, value, delay, connection_id) → boolean
Write a single coil value (function code 0x05).
Parameters:
slave(number) - Slave/unit ID (1-247)addr(number) - Coil address (0-65535)value(boolean) - Coil value (true= ON/0xFF00,false= OFF/0x0000)delay(number) - Delay in milliseconds before sendingconnection_id(number, optional) - Target connection ID (default: 0)
Returns:
true- Request scheduled successfullyfalse- Failed to schedule
Example:
-- Turn on coil at address 100
function on_receive()
local payload = message.payload
if #payload > 0 and string.byte(payload, 1) == 0x01 then
-- Turn on coil 100 when we receive command 0x01
modbus_write_single_coil(1, 100, true, 0)
end
end
-- Connection-specific coil control
function on_receive()
if message.connection_id == 0 then
-- Control coil on connection 1 based on data from connection 0
modbus_write_single_coil(1, 100, true, 0, 1)
end
end
modbus_write_single_register(slave, addr, value, delay, connection_id) → boolean
Write a single register value (function code 0x06).
Parameters:
slave(number) - Slave/unit ID (1-247)addr(number) - Register address (0-65535)value(number) - Register value (0-65535, unsigned 16-bit)delay(number) - Delay in milliseconds before sendingconnection_id(number, optional) - Target connection ID (default: 0)
Returns:
true- Request scheduled successfullyfalse- Failed to schedule
Example:
-- Write value 1234 to holding register 0 every 5 seconds
local last_write_time = 0
function on_timer(elapsed_ms)
if elapsed_ms - last_write_time >= 5000 then
modbus_write_single_register(1, 0, 1234, 0)
last_write_time = elapsed_ms
end
end
modbus_write_multiple_coils(slave, start, values_table, delay, connection_id) → boolean
Write multiple coil values (function code 0x0F).
Parameters:
slave(number) - Slave/unit ID (1-247)start(number) - Starting coil address (0-65535)values_table(table) - Lua array of boolean values (1-2000 elements)delay(number) - Delay in milliseconds before sendingconnection_id(number, optional) - Target connection ID (default: 0)
Returns:
true- Request scheduled successfullyfalse- Failed to schedule
Validation:
- Table must contain 1-2000 boolean values
- Each value must be
trueorfalse - Values are packed into bytes (LSB-first) per Modbus specification
Example:
-- Write 8 coil values starting at address 100
function on_start()
local coils = {true, false, true, true, false, false, true, false}
modbus_write_multiple_coils(1, 100, coils, 100)
end
-- Pattern generation for testing
local last_pattern_time = 0
function on_timer(elapsed_ms)
if elapsed_ms - last_pattern_time >= 2000 then
-- Create alternating pattern
local pattern = {}
for i = 1, 16 do
pattern[i] = (i % 2 == 1)
end
modbus_write_multiple_coils(1, 0, pattern, 0)
last_pattern_time = elapsed_ms
end
end
modbus_write_multiple_registers(slave, start, values_table, delay, connection_id) → boolean
Write multiple register values (function code 0x10).
Parameters:
slave(number) - Slave/unit ID (1-247)start(number) - Starting register address (0-65535)values_table(table) - Lua array of integer values (1-125 elements)delay(number) - Delay in milliseconds before sendingconnection_id(number, optional) - Target connection ID (default: 0)
Returns:
true- Request scheduled successfullyfalse- Failed to schedule
Validation:
- Table must contain 1-125 integer values
- Each value must be in range 0-65535 (unsigned 16-bit)
- Values are encoded as big-endian u16
Example:
-- Write configuration to holding registers
function on_start()
local config = {100, 200, 300, 400, 500}
modbus_write_multiple_registers(1, 0, config, 0)
end
-- Write sensor data from one connection to another
function on_receive()
if message.connection_id == 0 then
-- Parse sensor data
local temp = read_u16_be(message.payload, 1)
local humidity = read_u16_be(message.payload, 3)
-- Write to Modbus device on connection 1
modbus_write_multiple_registers(1, 0, {temp, humidity}, 0, 1)
end
return false
end
Complete Example: Modbus Polling and Control
This example demonstrates a complete Modbus master implementation with polling, control, and multi-connection support:
-- Modbus Master with Polling and Control
-- Polls holding registers and controls coils based on register values
-- Configuration
local POLL_INTERVAL_MS = 1000
local SLAVE_ID = 1
local HOLDING_REG_START = 0
local HOLDING_REG_COUNT = 10
local COIL_START = 0
-- Track last poll time
local last_poll_time = 0
function on_start()
local count = get_connection_count()
log("info", "Modbus Master started with " .. count .. " connection(s)")
-- Log connection configurations
for i = 0, count - 1 do
local codec = get_codec(i)
local transport = get_transport(i)
log("info", "Connection " .. i .. ": " .. transport .. " with " .. codec)
end
end
function on_timer(elapsed_ms)
-- Poll each Modbus connection every POLL_INTERVAL_MS
if elapsed_ms - last_poll_time >= POLL_INTERVAL_MS then
local count = get_connection_count()
for conn_id = 0, count - 1 do
local codec = get_codec(conn_id)
-- Only poll Modbus connections
if codec == "modbus_rtu" or codec == "modbus_tcp" then
-- Read holding registers
modbus_read_holding_registers(
SLAVE_ID,
HOLDING_REG_START,
HOLDING_REG_COUNT,
0,
conn_id
)
end
end
last_poll_time = elapsed_ms
end
end
function on_receive()
local conn_id = message.connection_id
local payload = message.payload
-- Parse Modbus response (function code 0x03 - Read Holding Registers)
if #payload >= 3 and string.byte(payload, 1) == 0x03 then
local byte_count = string.byte(payload, 2)
local register_count = byte_count / 2
log("info", "Connection " .. conn_id .. ": Received " .. register_count .. " registers")
-- Parse register values
for i = 0, register_count - 1 do
local offset = 3 + (i * 2) -- Offset 3 = skip function code + byte count
if offset + 1 <= #payload then
local value = read_u16_be(payload, offset)
local reg_addr = HOLDING_REG_START + i
-- Add to chart
message:add_int_value("Register " .. reg_addr, value)
-- Control logic: Turn on coil if register value exceeds threshold
if value > 500 then
log("warn", "Register " .. reg_addr .. " exceeded threshold: " .. value)
modbus_write_single_coil(SLAVE_ID, COIL_START + i, true, 0, conn_id)
else
modbus_write_single_coil(SLAVE_ID, COIL_START + i, false, 0, conn_id)
end
end
end
return true -- We added values
end
return false
end
function on_send()
local conn_id = message.connection_id
log("debug", "Sending Modbus request to connection " .. conn_id)
return false
end
function on_send_confirm()
-- Optional: Track successful sends
return false
end
Common Patterns
Periodic Polling
Use on_timer() with a global variable to track polling intervals:
-- Track last poll time
local last_poll_time = 0
function on_timer(elapsed_ms)
-- Poll every second (1000ms)
if elapsed_ms - last_poll_time >= 1000 then
modbus_read_holding_registers(1, 0, 10, 0)
last_poll_time = elapsed_ms
end
end
Note: Since on_timer() is called every 100ms, using elapsed_ms % 1000 == 0 won't work reliably. Always use the pattern elapsed_ms - last_time >= interval with a global variable.
Sequential Requests
Use delays to sequence multiple requests:
function on_start()
-- Read different register banks with staggered delays
modbus_read_holding_registers(1, 0, 10, 0) -- Immediate
modbus_read_holding_registers(1, 10, 10, 100) -- After 100ms
modbus_read_holding_registers(1, 20, 10, 200) -- After 200ms
end
Response-Driven Writes
Parse responses and write based on values:
function on_receive()
local payload = message.payload
if #payload >= 5 and string.byte(payload, 1) == 0x03 then
local reg_value = read_u16_be(payload, 3)
-- Write to coil based on register value
if reg_value > 1000 then
modbus_write_single_coil(1, 100, true, 0)
end
end
return false
end
Multi-Connection Routing
Route Modbus commands to specific connections:
local last_poll_time = 0
function on_timer(elapsed_ms)
if elapsed_ms - last_poll_time >= 1000 then
-- Poll slave 1 on connection 0 (serial)
modbus_read_holding_registers(1, 0, 10, 0, 0)
-- Poll slave 2 on connection 1 (TCP)
modbus_read_holding_registers(2, 0, 10, 0, 1)
last_poll_time = elapsed_ms
end
end
Error Handling
All Modbus helpers perform parameter validation and return false on errors:
-- Invalid quantity (exceeds 125 for registers)
local success = modbus_read_holding_registers(1, 0, 200, 0)
if not success then
log("error", "Failed to schedule Modbus request")
end
-- Invalid register value (exceeds 65535)
local values = {70000} -- Out of range
modbus_write_multiple_registers(1, 0, values, 0) -- Raises Lua error
Common Validation Errors:
- Coils: Quantity must be 1-2000
- Discrete Inputs: Quantity must be 1-2000
- Holding Registers: Quantity must be 1-125
- Input Registers: Quantity must be 1-125
- Register values: Must be 0-65535 (unsigned 16-bit)
- Coil values: Must be boolean (
true/false)
Debugging
Enable debug logging to trace Modbus operations:
function on_send()
log("debug", "Sending Modbus request to slave " .. message.connection_id)
return false
end
function on_send_confirm()
log("debug", "Modbus request confirmed")
return false
end
function on_receive()
local payload = message.payload
log("debug", "Received " .. #payload .. " bytes")
-- Log function code
if #payload > 0 then
local func_code = string.byte(payload, 1)
log("debug", "Function code: 0x" .. string.format("%02X", func_code))
end
return false
end
Performance Considerations
- Request Timing: Space requests appropriately to avoid overwhelming slaves
- Batch Operations: Use write multiple functions instead of multiple single writes
- Connection Filtering: Only poll connections with Modbus codecs
- Response Parsing: Validate response length before reading to avoid errors
-- Good: Batch write
modbus_write_multiple_registers(1, 0, {100, 200, 300}, 0)
-- Less efficient: Multiple single writes
modbus_write_single_register(1, 0, 100, 0)
modbus_write_single_register(1, 1, 200, 100)
modbus_write_single_register(1, 2, 300, 200)
See Also
- Lua Script Overview - Core Lua scripting concepts and hooks
- Message API - Message object methods and binary reading functions
- MQTT Helpers - MQTT publish helper
- UDP Helpers - UDP send helper