Kontakt
Documentation and software
HELLOS UNI / MODBUS-RTU API

Modbus RTU API reference

HELLOS-UNI can run as a Modbus RTU server on the same half-duplex RS-485 interface used by HBUS. The implementation is intentionally isolated from the HBUS transport. Selecting a protocol takes effect after restart.

This document applies to HELLOS-UNI firmware 4.0 and register-map version 1.

Serial and server behavior

  • 8 data bits, no parity, 1 stop bit (8N1).
  • The configured HBUS baud rate is also used by Modbus RTU.
  • Server address range: 1 through 247.
  • Supported functions: 03, 04, 06 and 16.
  • Register words and multi-word values are big-endian. Modbus CRC bytes are sent low byte first.
  • A 32-bit value occupies two consecutive registers, high word first.
  • At 19200 baud and below, frame separation is 3.5 character times. Above 19200 baud it is fixed at 1.75 ms.
  • The module never sends unsolicited Modbus frames. HBUS event data is stored in event registers until a client reads it.
  • Broadcast address zero accepts ordinary output and PWM writes without a response. It cannot execute the recovery service command.

The addresses below are zero-based Modbus PDU addresses. Software that shows one-based 3xxxx or 4xxxx notation may display a value one greater than the address in this document.

Value conventions

  • 0xffff means unavailable or an unused slot.
  • 0x8000 means an unavailable signed measurement.
  • Signed values use 16-bit two's complement.
  • Temperature and humidity values are multiplied by 10.
  • ADC values use tenths of a percent from 0 through 1000.
  • PWM duty uses integer percent from 0 through 100.

Input registers - function 04

System registers

Address Value
0 Register-map version, currently 1
1 Status flags: bit 0 debug, bit 1 filesystem update, bit 2 event overflow, bit 3 sensor-slot overflow
2-3 Uptime in seconds, unsigned 32-bit
4-5 Hardware-derived device ID, unsigned 32-bit
6-7 UART baud rate, unsigned 32-bit
8 Modbus address
9 Configured mode: 0 HBUS, 1 Modbus RTU
10 Active mode: 0 HBUS, 1 Modbus RTU
11 Used DS18x20 slots, maximum 32
12 Used SHT3x slots, maximum 6
13 Used SCD4x slots, maximum 6
14-16 Port mode code for A, B and C
17-18 CRC32 of the complete software version string
19-20 CRC32 of the hardware version string
21 Sensor-slot overflow flags: DS bit 0, SHT bit 1, SCD bit 2
22-31 Reserved, reads as 0xffff

Port mode codes are stable within map version 1:

Code Mode Code Mode
0 sensor-autodetect 8 output_safe
1 button_simple 9 pwm_50hz
2 button_double 10 pwm_1khz
3 input 11 pwm_25khz
4 input_safe 12 adc_raw_passive
5 impulse_counter 13 adc_raw_active
6 impulse_counter_safe 14 adc_pullup_passive
7 output 15 adc_pullup_active
16 encoder 17 encoder_reverse

Port-state registers

Every 12-register block uses channels A0 through A3, B0 through B3, then C0 through C3.

Address Value
100-111 Logical input state, 0 or 1
120-143 Impulse counters, 12 unsigned 32-bit values
150-161 ADC value in tenths of a percent
170-181 Logical output state, 0 or 1
190-201 PWM duty in percent

Compact digital-input polling

The compact block is intended for installations with many modules. A client reads registers 32 through 46 in one request and compares the press counters with its previous sample. This does not lose a short press between polling cycles and keeps the client implementation to one request per module.

Bits and counter positions use channels A0 through A3, B0 through B3, then C0 through C3.

Address Value
32 Current logical input-state bitmap, bits 0 through 11
33 Valid input-state bitmap for input and input_safe ports
34 Valid press-counter bitmap for button and input ports
35-46 Unsigned 16-bit press counter per channel

The counters wrap from 65535 to zero and reset after module restart. A logical-input transition to active and a button_simple or button_double press increment the corresponding counter. Releases do not. The first values read after connecting establish the client baseline and must not be interpreted as new presses.

Communication diagnostics

Diagnostic counters are unsigned 32-bit values stored high word first. They wrap naturally and reset after module restart.

Address Value
50-51 Valid requests received for this module
52-53 Invalid or overflowing RTU frames received
54-55 Valid RTU frames addressed to another module
56-57 Responses successfully handed to the UART
58-59 Response transmission failures

Event registers

Event registers preserve the latest event per physical channel. A client detects a new event by observing a change in its sequence register. Sequence numbers wrap from 65535 to zero.

Address Value
220-231 Event sequence per channel
240-251 Last event type per channel
260-271 Last event value per channel, signed 16-bit
280-303 Last event timestamp in milliseconds, 12 unsigned 32-bit values
320-343 Accumulated encoder position, 12 signed 32-bit values

Event type codes are 1 button press, 2 button release, 3 logical input, 4 encoder delta and 5 ADC change. Button events store value 1. ADC event values use tenths of a percent.

Sensor slots

DS18x20 slots are sorted by their 32-bit HBUS sensor identifier. SHT3x and SCD4x slots are sorted by their HBUS index. A slot therefore remains stable as long as the set of detected devices and their identifiers remains unchanged.

Address Slot layout
400-527 32 DS18x20 slots, four registers each: ID high, ID low, signed temperature x10, flags
600-623 6 SHT3x slots, four registers each: HBUS index, signed temperature x10, humidity x10, flags
640-669 6 SCD4x slots, five registers each: HBUS index, CO2 ppm, signed temperature x10, humidity x10, flags

Flags currently contain 1 for a populated slot. Measurements include the configured compensation table, matching an HBUS read-all response.

Undefined addresses from 0 through 669 read as 0xffff. Reading above 669 returns Modbus exception 02.

Holding registers - functions 03, 06 and 16

Address Access Value
1000-1011 read/write Logical outputs A0-C3, 0 or 1
1020-1031 read/write PWM duty A0-C3, integer 0 through 100
1999 read/write Recovery service command

Registers for channels that are not configured for the corresponding output mode read as 0xffff and reject writes. Function 16 validates the complete request before applying it. One request cannot cross from the output block into the PWM block.

Writing hexadecimal 0x4842 to register 1999 with function 06 stores HBUS as the configured mode, returns the normal write response and restarts the module. This command is deliberately rejected at broadcast address zero. The register otherwise reads as zero.

Configuration and recovery

Configure bus mode and Modbus address through HBUS before switching:

{"device":{"set_bus_mode":{"mode":"modbus","modbus_address":17}}}

The stored mode is used on the next restart. Enabling the debug DIP switch at boot forces HBUS for that boot only and keeps the stored baud rate. This allows the settings to be corrected without erasing the module. Factory reset removes the bus configuration and baud-rate file, restoring HBUS at 19200 baud.

Practical Node-RED examples

The examples below use a module with Modbus address 1. Change unitid to the configured address of the target module.

Required serial settings

Use these settings in the node-red-contrib-modbus client:

  • serial connection
  • the same baud rate as the module
  • 8 data bits, no parity and 1 stop bit
  • serial type RTU-BUFFERD

The spelling RTU-BUFFERD is used by the Node-RED package. It is not the same option as command queue buffering.

The non-buffered RTU serial driver used during testing only handled reads up to 13 registers reliably. A function-04 response for 13 registers is 31 bytes, while a response for 14 registers is 33 bytes. The operating-system serial layer can split the longer response into multiple chunks and the non-buffered driver may treat the first chunk as the complete frame. RTU-BUFFERD collects the chunks until the expected Modbus response length has been received. Use it for every example in this section.

The following helper accepts the two common output shapes produced by the Modbus Flex Getter node:

function registerData(msg) {
    if (Array.isArray(msg.payload)) {
        return msg.payload;
    }
    if (msg.payload && Array.isArray(msg.payload.data)) {
        return msg.payload.data;
    }
    throw new Error("Modbus response does not contain a register array");
}

Read the complete module configuration

Use a Function node connected to a Modbus Flex Getter:

msg.payload = {
    value: 0,
    fc: 4,
    unitid: 1,
    address: 0,
    quantity: 22
};

return msg;

Decode the response with another Function node:

function registerData(msg) {
    if (Array.isArray(msg.payload)) {
        return msg.payload;
    }
    if (msg.payload && Array.isArray(msg.payload.data)) {
        return msg.payload.data;
    }
    throw new Error("Modbus response does not contain a register array");
}

function u32(high, low) {
    return ((high << 16) | low) >>> 0;
}

function hex32(value) {
    return value.toString(16).padStart(8, "0");
}

const portModes = {
    0: "sensor-autodetect",
    1: "button_simple",
    2: "button_double",
    3: "input",
    4: "input_safe",
    5: "impulse_counter",
    6: "impulse_counter_safe",
    7: "output",
    8: "output_safe",
    9: "pwm_50hz",
    10: "pwm_1khz",
    11: "pwm_25khz",
    12: "adc_raw_passive",
    13: "adc_raw_active",
    14: "adc_pullup_passive",
    15: "adc_pullup_active",
    16: "encoder",
    17: "encoder_reverse"
};

const r = registerData(msg);
const flags = r[1];
const sensorOverflow = r[21];

msg.payload = {
    firmware_api: "4.0",
    register_map_version: r[0],
    flags: {
        raw: flags,
        debug: Boolean(flags & 1),
        filesystem_update: Boolean(flags & 2),
        event_overflow: Boolean(flags & 4),
        sensor_slot_overflow: Boolean(flags & 8)
    },
    uptime: u32(r[2], r[3]),
    device_id: hex32(u32(r[4], r[5])),
    baudrate: u32(r[6], r[7]),
    modbus_address: r[8],
    configured_mode: r[9] === 1 ? "modbus" : "hbus",
    active_mode: r[10] === 1 ? "modbus" : "hbus",
    sensors: {
        ds18x20: r[11],
        sht3x: r[12],
        scd4x: r[13]
    },
    ports: {
        A: portModes[r[14]] || `unknown-${r[14]}`,
        B: portModes[r[15]] || `unknown-${r[15]}`,
        C: portModes[r[16]] || `unknown-${r[16]}`
    },
    software_crc32: hex32(u32(r[17], r[18])),
    hardware_crc32: hex32(u32(r[19], r[20])),
    sensor_overflow: {
        raw: sensorOverflow,
        ds18x20: Boolean(sensorOverflow & 1),
        sht3x: Boolean(sensorOverflow & 2),
        scd4x: Boolean(sensorOverflow & 4)
    }
};

return msg;

This block contains the complete configuration exposed through Modbus. Alias, compensation-table editing and other HBUS-specific configuration operations are not exposed as Modbus registers in firmware 4.0.

Discover connected sensors

The complete configuration read above already returns the number of used sensor slots in registers 11 through 13. To read only these counts:

msg.payload = {
    value: 0,
    fc: 4,
    unitid: 1,
    address: 11,
    quantity: 3
};

return msg;

The returned array contains:

[DS18x20 count, SHT3x count, SCD4x count]

Do not request a sensor block when its count is zero. For a non-zero count, read exactly the number of populated slots:

Sensor Start address Registers per sensor Read quantity
DS18x20 400 4 count * 4
SHT3x 600 4 count * 4
SCD4x 640 5 count * 5

Read DS18x20 sensors

This request reads one DS18x20 slot:

msg.payload = {
    value: 0,
    fc: 4,
    unitid: 1,
    address: 400,
    quantity: 4
};

return msg;

Decode one or more slots:

function registerData(msg) {
    return Array.isArray(msg.payload) ? msg.payload : msg.payload.data;
}

function u32(high, low) {
    return ((high << 16) | low) >>> 0;
}

function signed16(value) {
    return value & 0x8000 ? value - 0x10000 : value;
}

const r = registerData(msg);
const sensors = [];

for (let offset = 0; offset + 3 < r.length; offset += 4) {
    if (r[offset] === 0xffff && r[offset + 1] === 0xffff) {
        continue;
    }
    sensors.push({
        id: u32(r[offset], r[offset + 1]).toString(16).padStart(8, "0"),
        temperature: signed16(r[offset + 2]) / 10,
        valid: Boolean(r[offset + 3] & 1)
    });
}

msg.payload = sensors;
return msg;

Read SHT3x sensors

For one SHT3x slot, read address 600 with quantity 4. Each slot contains the HBUS index, temperature multiplied by 10, humidity multiplied by 10 and flags:

msg.payload = {
    value: 0,
    fc: 4,
    unitid: 1,
    address: 600,
    quantity: 4
};

return msg;
function registerData(msg) {
    return Array.isArray(msg.payload) ? msg.payload : msg.payload.data;
}

function signed16(value) {
    return value & 0x8000 ? value - 0x10000 : value;
}

const r = registerData(msg);
const sensors = [];

for (let offset = 0; offset + 3 < r.length; offset += 4) {
    if (r[offset] === 0xffff) {
        continue;
    }
    sensors.push({
        index: r[offset],
        temperature: signed16(r[offset + 1]) / 10,
        humidity: r[offset + 2] / 10,
        valid: Boolean(r[offset + 3] & 1)
    });
}

msg.payload = sensors;
return msg;

Read SCD4x sensors

For one SCD4x slot:

msg.payload = {
    value: 0,
    fc: 4,
    unitid: 1,
    address: 640,
    quantity: 5
};

return msg;

Decode one or more slots:

function registerData(msg) {
    return Array.isArray(msg.payload) ? msg.payload : msg.payload.data;
}

function signed16(value) {
    return value & 0x8000 ? value - 0x10000 : value;
}

const r = registerData(msg);
const sensors = [];

for (let offset = 0; offset + 4 < r.length; offset += 5) {
    if (r[offset] === 0xffff) {
        continue;
    }
    sensors.push({
        index: r[offset],
        co2: r[offset + 1],
        temperature: signed16(r[offset + 2]) / 10,
        humidity: r[offset + 3] / 10,
        valid: Boolean(r[offset + 4] & 1)
    });
}

msg.payload = sensors;
return msg;

All sensor measurements include the configured compensation values.

Read digital inputs and press counters

Read registers 32 through 46 to obtain all 12 physical channels in one request:

msg.payload = {
    value: 0,
    fc: 4,
    unitid: 1,
    address: 32,
    quantity: 15
};

return msg;

If only port A is configured as input or input_safe, quantity 7 is enough. It reads the three bitmaps and counters A0 through A3.

function registerData(msg) {
    return Array.isArray(msg.payload) ? msg.payload : msg.payload.data;
}

const r = registerData(msg);
const stateBitmap = r[0];
const inputValidBitmap = r[1];
const pressValidBitmap = r[2];
const channels = [];

for (let channel = 0; channel < r.length - 3; channel++) {
    const mask = 1 << channel;
    channels.push({
        index: channel,
        port: ["A", "B", "C"][Math.floor(channel / 4)],
        pin: channel % 4,
        input_valid: Boolean(inputValidBitmap & mask),
        active: Boolean(stateBitmap & mask),
        press_counter_valid: Boolean(pressValidBitmap & mask),
        press_counter: r[3 + channel]
    });
}

msg.payload = {
    state_bitmap: stateBitmap,
    input_valid_bitmap: inputValidBitmap,
    press_valid_bitmap: pressValidBitmap,
    channels: channels
};

return msg;

For reliable button handling, compare each press counter with its value from the previous polling cycle. The unsigned difference is:

const delta = (currentCounter - previousCounter) & 0xffff;

The first sample establishes the baseline and must not generate press events. This method preserves short presses that start and finish between two polling cycles.

Write and read logical outputs

The channel order is A0-A3, B0-B3 and C0-C3. For example, B0 is holding register 1004.

Set B0 to logical 1 with a Modbus Flex Write node:

msg.payload = {
    value: 1,
    fc: 6,
    unitid: 1,
    address: 1004,
    quantity: 1
};

return msg;

Set all four pins of port B in one function-16 request:

msg.payload = {
    value: [1, 0, 1, 0],
    fc: 16,
    unitid: 1,
    address: 1004,
    quantity: 4
};

return msg;

Read the four output states back with a Modbus Flex Getter:

msg.payload = {
    value: 0,
    fc: 3,
    unitid: 1,
    address: 1004,
    quantity: 4
};

return msg;

The target port must be configured as output or output_safe. Writes to a channel in another mode are rejected.

Write and read PWM outputs

PWM registers use integer duty cycle from 0 through 100. For example, B0 is holding register 1024.

Set B0 to 50 percent:

msg.payload = {
    value: 50,
    fc: 6,
    unitid: 1,
    address: 1024,
    quantity: 1
};

return msg;

Set all four PWM channels on port B:

msg.payload = {
    value: [0, 25, 50, 100],
    fc: 16,
    unitid: 1,
    address: 1024,
    quantity: 4
};

return msg;

Read the four duty-cycle values back with a Modbus Flex Getter:

msg.payload = {
    value: 0,
    fc: 3,
    unitid: 1,
    address: 1024,
    quantity: 4
};

return msg;

The target port must be configured as pwm_50hz, pwm_1khz or pwm_25khz. Values above 100 and writes to channels in another mode are rejected.

Return a module to HBUS mode

Use a Modbus Flex Write node to write the recovery magic value 0x4842 (18498 decimal) to holding register 1999:

msg.payload = {
    value: 18498,
    fc: 6,
    unitid: 1,
    address: 1999,
    quantity: 1
};

return msg;

The module first returns the normal function-06 response, stores HBUS as the configured mode and then restarts. The configured baud rate is preserved. Address zero broadcast is deliberately rejected for this operation.