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 .