Skip to main content

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 CodeOperationHelper Function
0x01Read Coilsmodbus_read_coils()
0x02Read Discrete Inputsmodbus_read_discrete_inputs()
0x03Read Holding Registersmodbus_read_holding_registers()
0x04Read Input Registersmodbus_read_input_registers()
0x05Write Single Coilmodbus_write_single_coil()
0x06Write Single Registermodbus_write_single_register()
0x0FWrite Multiple Coilsmodbus_write_multiple_coils()
0x10Write Multiple Registersmodbus_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 sending
  • connection_id (number, optional) - Target connection ID (default: 0)

Returns:

  • true - Request scheduled successfully
  • false - 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 sending
  • connection_id (number, optional) - Target connection ID (default: 0)

Returns:

  • true - Request scheduled successfully
  • false - 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 sending
  • connection_id (number, optional) - Target connection ID (default: 0)

Returns:

  • true - Request scheduled successfully
  • false - 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 sending
  • connection_id (number, optional) - Target connection ID (default: 0)

Returns:

  • true - Request scheduled successfully
  • false - 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 sending
  • connection_id (number, optional) - Target connection ID (default: 0)

Returns:

  • true - Request scheduled successfully
  • false - 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 sending
  • connection_id (number, optional) - Target connection ID (default: 0)

Returns:

  • true - Request scheduled successfully
  • false - 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 sending
  • connection_id (number, optional) - Target connection ID (default: 0)

Returns:

  • true - Request scheduled successfully
  • false - Failed to schedule

Validation:

  • Table must contain 1-2000 boolean values
  • Each value must be true or false
  • 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 sending
  • connection_id (number, optional) - Target connection ID (default: 0)

Returns:

  • true - Request scheduled successfully
  • false - 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

  1. Request Timing: Space requests appropriately to avoid overwhelming slaves
  2. Batch Operations: Use write multiple functions instead of multiple single writes
  3. Connection Filtering: Only poll connections with Modbus codecs
  4. 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