Skip to main content

Modbus RTU Sensor to MQTT

Overview​

This example demonstrates how to poll a Modbus RTU temperature and humidity sensor (EID041) via serial port and publish the parsed values to an MQTT broker in JSON format. It showcases CycBox's capabilities for:

  • Polling Modbus RTU devices using the built-in Modbus RTU codec
  • Parsing sensor registers including both integer and float data types
  • Publishing structured data to MQTT in JSON format
  • Periodic data acquisition with configurable poll intervals

This is a common pattern in industrial IoT where sensor data from Modbus devices needs to be forwarded to cloud platforms or monitoring systems via MQTT.

Scenario​

An EID041 temperature and humidity sensor is connected via serial port (e.g., /dev/ttyUSB0) and communicates using Modbus RTU protocol. The requirements are:

  1. Poll the sensor every 5 seconds using Modbus RTU Read Input Registers (function code 0x04)
  2. Parse temperature and humidity values from both integer (16-bit) and float (32-bit) registers
  3. Publish the parsed values to MQTT topic cycbox/eid041 as a JSON payload

EID041 Register Map​

Register AddressCountData TypeDescription
0x00001int16Temperature (0.1°C resolution, signed)
0x00011uint16Humidity (0.1%RH resolution)
0x0002-0x00032float32 (big-endian)Temperature (°C)
0x0004-0x00052float32 (big-endian)Humidity (%RH)

Configuration​

CycBox is configured with two connections:

  • Connection 0: Serial port with Modbus RTU codec for polling the sensor
  • Connection 1: MQTT client for publishing parsed data
{
"version": "1.8.1",
"name": "EID041 Modbus RTU to MQTT",
"description": "Poll EID041 temperature & humidity sensor via Modbus RTU and publish to MQTT in JSON format",
"configs": [
{
"app": {
"app_transport": "serial",
"app_codec": "modbus_rtu_codec",
"app_transformer": "disable",
"app_encoding": "UTF-8"
},
"serial": {
"serial_port": "/dev/ttyUSB0",
"serial_baud_rate": 9600,
"serial_data_bits": 8,
"serial_parity": "none",
"serial_stop_bits": "1",
"serial_flow_control": "none"
},
"modbus_rtu_codec": {
"with_receive_timeout": 20
}
},
{
"app": {
"app_transport": "mqtt",
"app_codec": "timeout_codec",
"app_transformer": "disable",
"app_encoding": "UTF-8"
},
"mqtt": {
"mqtt_broker_url": "mqtt://broker.emqx.io:1883",
"mqtt_client_id": "cycbox_eid041",
"mqtt_username": "",
"mqtt_password": "",
"mqtt_use_tls": false,
"mqtt_ca_path": "",
"mqtt_client_cert_path": "",
"mqtt_client_key_path": "",
"mqtt_subscribe_topics": "cycbox/#",
"mqtt_subscribe_qos": 1
},
"timeout_codec": {
"with_receive_timeout": 100
}
}
]
}

Lua Script Logic​

The Lua script handles three tasks: periodic polling, response parsing, and MQTT publishing.

Script Breakdown​

-- Configuration
local SLAVE_ADDR = 3 -- Modbus slave address
local START_ADDR = 0x0000 -- Start reading from register 0
local NUM_REGISTERS = 6 -- Read all 6 registers (2 int + 4 float)
local POLL_INTERVAL = 5000 -- Poll every 5 seconds (5000ms)
local MQTT_TOPIC = "cycbox/eid041"
local MQTT_CONNECTION_ID = 1 -- MQTT is the second connection (0-based)

How It Works​

1. Periodic Polling (on_timer)

local last_poll_time = nil
function on_timer(timestamp_ms)
if last_poll_time == nil or timestamp_ms - last_poll_time >= POLL_INTERVAL then
modbus_rtu_read_input_registers(SLAVE_ADDR, START_ADDR, NUM_REGISTERS, 0, 0)
last_poll_time = timestamp_ms
end
end
  • Uses on_timer() which is called every 100ms
  • Tracks elapsed time to trigger a poll every 5 seconds
  • Sends a Modbus RTU Read Input Registers request (function code 0x04) to the sensor on connection 0

2. Response Parsing (on_receive)

function on_receive()
local payload = message.payload

if #payload < 3 then return false end

local slave_addr = read_u8(payload, 1)
local function_code = read_u8(payload, 2)

-- Only process Read Input Registers responses from our device
if function_code ~= 0x04 or slave_addr ~= SLAVE_ADDR then
return false
end

-- Use codec-parsed values for integer registers (logical key)
local temperature_int_raw = message:get_value(
string.format("modbus_rtu_%d:%d", slave_addr, 30001 + START_ADDR))
local humidity_int_raw = message:get_value(
string.format("modbus_rtu_%d:%d", slave_addr, 30001 + START_ADDR + 1))

-- Parse float values directly from payload bytes
local temperature_float_value = read_float_be(payload, 8)
local humidity_float_value = read_float_be(payload, 12)

-- ... build JSON and publish
end
  • Validates the Modbus response by checking function code and slave address
  • Retrieves integer register values using message:get_value() with the codec-assigned logical keys
  • Parses float32 values directly from the raw payload using read_float_be()

3. Value Extraction and MQTT Publishing

  -- Scale integer values (0.1 resolution)
local temperature_int_value = temperature_int_raw * 0.1
message:add_float_value("temperature_int", temperature_int_value)

-- Add float values
message:add_float_value("temperature_float", temperature_float_value)
message:add_float_value("humidity_float", humidity_float_value)

-- Build and publish JSON
local json_payload = '{"temperature":25.3,"humidity":60.1,"temperature_float":25.32,"humidity_float":60.12}'
mqtt_publish(MQTT_TOPIC, json_payload, 0, false, 0, MQTT_CONNECTION_ID)
  • Integer values are scaled by 0.1 to convert to real units
  • All values are added to the message metadata using message:add_float_value() for UI charting
  • A JSON payload is constructed and published to the MQTT topic on connection 1

MQTT Output Example​

{
"temperature": 25.3,
"humidity": 60.1,
"temperature_float": 25.32,
"humidity_float": 60.12
}

Modbus RTU Codec Value IDs​

The Modbus RTU codec automatically parses register values and assigns two keys per register:

  • Logical key: modbus_rtu_{slave_addr}:{30001 + register_address} (Modicon convention)
  • Protocol-type key: modbus_rtu_{slave_addr}:input_{addr:04X} (hex address)

For this example with slave address 3 and start address 0x0000:

RegisterLogical KeyProtocol-Type KeyDescription
0x0000modbus_rtu_3:30001modbus_rtu_3:input_0000Temperature (int16 raw)
0x0001modbus_rtu_3:30002modbus_rtu_3:input_0001Humidity (uint16 raw)
0x0002modbus_rtu_3:30003modbus_rtu_3:input_0002Temperature float high word
0x0003modbus_rtu_3:30004modbus_rtu_3:input_0003Temperature float low word
0x0004modbus_rtu_3:30005modbus_rtu_3:input_0004Humidity float high word
0x0005modbus_rtu_3:30006modbus_rtu_3:input_0005Humidity float low word

View Full example script on GitHub .