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:
- Poll the sensor every 5 seconds using Modbus RTU Read Input Registers (function code 0x04)
- Parse temperature and humidity values from both integer (16-bit) and float (32-bit) registers
- Publish the parsed values to MQTT topic
cycbox/eid041as a JSON payload
EID041 Register Map
| Register Address | Count | Data Type | Description |
|---|---|---|---|
| 0x0000 | 1 | int16 | Temperature (0.1°C resolution, signed) |
| 0x0001 | 1 | uint16 | Humidity (0.1%RH resolution) |
| 0x0002-0x0003 | 2 | float32 (big-endian) | Temperature (°C) |
| 0x0004-0x0005 | 2 | float32 (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:
| Register | Logical Key | Protocol-Type Key | Description |
|---|---|---|---|
| 0x0000 | modbus_rtu_3:30001 | modbus_rtu_3:input_0000 | Temperature (int16 raw) |
| 0x0001 | modbus_rtu_3:30002 | modbus_rtu_3:input_0001 | Humidity (uint16 raw) |
| 0x0002 | modbus_rtu_3:30003 | modbus_rtu_3:input_0002 | Temperature float high word |
| 0x0003 | modbus_rtu_3:30004 | modbus_rtu_3:input_0003 | Temperature float low word |
| 0x0004 | modbus_rtu_3:30005 | modbus_rtu_3:input_0004 | Humidity float high word |
| 0x0005 | modbus_rtu_3:30006 | modbus_rtu_3:input_0005 | Humidity float low word |
View Full example script on GitHub .