Kontakt
Documentation and software
HELLOS UNI / HBUS API

HELLOS-UNI HBUS API reference

This documentation applies to HBUS protocol version 01 and HELLOS-UNI firmware version 4.0. The descriptions of port behaviour apply to HELLOS-UNI hardware version 2.1.

This document describes the wire protocol and application payloads implemented by the HELLOS-UNI firmware. JSON examples under Request payload and Response payload show only the value of the HBUS p field.

Author and design goals

HBUS was designed by HELLOS smart tech s.r.o. and has an open protocol specification, allowing developers to implement it in their own projects.

The protocol was designed to:

  • Support master-to-master (multi-master) communication on RS-485, so devices can send events immediately without waiting for a master to poll them.
  • Simplify implementation and integration into custom projects through a human-readable text format. Packets can be inspected directly by eye, without a dedicated binary protocol decoder.
  • Provide an alternative to the nearly 50-year-old Modbus RTU protocol, whose master/slave polling model is unsuitable for many event-driven applications.

HBUS uses an ASCII-based JSON frame format to support coexistence with Modbus RTU slave devices on RS-485. The function bytes used for ordinary Modbus reads and writes, such as 0x01-0x06, 0x0f and 0x10, are control characters and do not occur as raw bytes in compact HBUS JSON packets.

Software and standalone use

The HBUS software ecosystem includes:

  • hbusd, a server and MQTT gateway available as a Debian package.
  • A desktop application for Linux and Windows.
  • A mobile application, currently for Android only.

HBUS sensor devices can also operate independently of this software, without hbusd, an MQTT broker or any of these applications. Their human-readable JSON output can be received and inspected through any RS-485 converter configured with matching serial settings, making them straightforward to integrate into custom projects. A custom controller can use the protocol described below to send requests and acknowledge packets that require reliable delivery.

HELLOS-UNI can also be configured to send its complete state automatically at regular intervals, without polling or acknowledgements. Once configured, no custom request or ACK logic is needed to receive these reports. The readable text output can be viewed directly in a serial terminal or processed with standard text-processing tools and simple scripts. See Set automatic state broadcast interval.

Transport

HBUS uses compact JSON over an 8N1 half-duplex RS-485 link. The default baud rate is 19200, which is fully sufficient for typical polled readings of inputs and sensors. For applications using immediate event notifications, 115200 baud or higher is recommended to reduce packet transmission time and event latency. A complete packet has this structure:

{"H":"01","l":"0073","c":"84449af2","d":"00000000","s":"cf38542d","r":1,"p":{"button_press":{"A":{"1":[1082810]}}}}
Field Type Meaning
H string Protocol version. This firmware accepts 01.
l string Total packet length in bytes as four lowercase hexadecimal digits.
c string CRC-32 as eight lowercase hexadecimal digits.
d string Eight-character destination address.
s string Eight-character source address.
r integer 1 requests an ACK; 0 does not.
p object Application payload described below.

The CRC is calculated over the complete JSON packet after replacing the eight characters in c with eight underscore characters:

"c":"________"

Implementations must calculate l before calculating the CRC. There is no line terminator. Receivers find the {"H":"01" header in the byte stream and use l to determine the packet boundary, so unrelated bytes before a packet are ignored.

Wire-duration-dependent receive, line-acquisition and ACK deadlines are calculated from the baud rate used to initialize the UART. Reliable packets therefore use the same transport state machine from 1200 through 460800 baud. An ACK received during retransmission backoff still completes the original delivery and prevents an unnecessary duplicate packet.

Addresses

Address Meaning
00000000 Default central-controller address used by hbusd
ffffffff Broadcast destination
Other eight-digit hexadecimal value Individual module address

The module address defaults to the last four bytes of the RP2040 unique ID, formatted as eight lowercase hexadecimal characters. 00000000 and ffffffff cannot be assigned to a module.

ACK

A valid directed packet with r:1 is acknowledged before its application command is processed:

{"a":"84449af2"}

The ACK contains the CRC of the received packet. It confirms correct transport delivery only; it does not confirm that the requested operation succeeded. Command success or failure must be determined from a separate response packet. Broadcast packets are not acknowledged.

Module-originated button, encoder and measurement packets use reliable delivery. The module retries an unacknowledged packet up to eight times with an increasing randomized backoff.

Using the API through hbusd

With the default hbusd MQTT naming, send an entire payload as JSON to:

hbusd/<device-address>/send

Example:

mosquitto_pub -t hbusd/cf38542d/send -m '{"g":"a"}'

Received leaf values are published below:

hbusd/<device-address>/read/<payload-path>

For example, an identify response produces read/hw, read/sw and read/sw_source. Arrays are expanded by hbusd into separate MQTT messages on the same leaf topic, preserving event ordering for consumers such as Node-RED. MQTT prefix and send/read segments are configurable in hbusd.

Identify

Identify can be requested from all modules by broadcast or from one module by unicast.

Request destination: ffffffff

Request payload:

{"identify":true}

The module schedules its response after a random interval from zero to ten seconds to reduce collisions between multiple responders. The countdown is non-blocking, so normal application and HBUS processing continues meanwhile.

For an immediate unicast response, send this payload to the module address:

{"device":"identify"}

Response payload:

{
  "hw": "UNI-2.1.0",
  "sw": "3.2-2026-09-06-1780922bd",
  "sw_source": "frozen",
  "alias": "Kitchen switches"
}
Field Meaning
hw Hardware model/version string
sw Application version from the build or installed HBU manifest
sw_source frozen or update
alias User-assigned module name, or an empty string

The response is sent to the central-controller address 00000000. The module also sends this payload automatically after a configured idle interval. Debug mode sends it once immediately after startup.

Read all measurements

Request payload:

{"g":"a"}

The response contains the most recently sampled values after compensation. Sampling normally runs every ten seconds.

Example response payload:

{
  "u": 234,
  "ds": {
    "100000a3": {"tmp": 24.1}
  },
  "sht3x": {
    "0": {"tmp": 23.8, "hum": 47.2}
  },
  "scd4x": {
    "0": {"co2": 612, "tmp": 24.0, "hum": 46.9}
  },
  "input": {
    "A": {"0": true, "1": false, "2": false, "3": false}
  },
  "impulse_counter": {
    "B": {"0": 12345, "1": 81, "2": 0, "3": 907}
  },
  "pwm": {
    "B": {"0": 50, "1": 0, "2": 75, "3": 100}
  },
  "output": {
    "C": {"0": 1, "1": 0, "2": 0, "3": 1}
  },
  "adc": {
    "A": {"0": 0.0, "1": 25.0, "2": 50.1, "3": 100.0}
  }
}
Path Unit/meaning
u Module uptime in seconds
ds/<id>/tmp DS18x20 temperature in degrees Celsius
sht3x/<id>/tmp SHT3x temperature in degrees Celsius
sht3x/<id>/hum SHT3x relative humidity in percent
scd4x/<id>/co2 SCD4x carbon-dioxide concentration in ppm
scd4x/<id>/tmp SCD4x temperature in degrees Celsius
scd4x/<id>/hum SCD4x relative humidity in percent
input/<port>/<index> Last debounced maintained-input state; true means that the active-low input is closed
impulse_counter/<port>/<index> Absolute number of accepted active-low pulses since boot
pwm/<port>/<index> Last applied PWM duty cycle in percent
output/<port>/<index> Last applied logical output state, 0 or 1
adc/A/<index> Filtered ADC input from 0.0 through 100.0 percent

DS18x20 IDs are the last eight hexadecimal characters of the sensor ROM. SHT3x and SCD4x IDs are zero-based enumeration indexes assigned during startup.

The response uses reliable delivery and is addressed to the requester.

Input events

Input events are unsolicited reliable packets sent to 00000000.

Button press

{"button_press":{"A":{"1":[1082810]}}}

Button release

Release events are emitted only for a port configured as button_double.

{"button_release":{"A":{"1":[1083152]}}}

The array allows several events accumulated during one main-loop interval to be transported in one packet. Each value is a time.ticks_ms() timestamp and is not wall-clock time.

Maintained input

{"input":{"A":{"1":true}}}

Every debounced state change produces an event containing the current boolean state. When several changes are accumulated for the same input before the next packet is built, the latest state is sent. The module also keeps the complete state of all ports configured as input or input_safe and includes it in every response to {"g":"a"}.

The input mode reads the signal GPIO directly and uses the board's 10 kOhm pull-up. The input_safe mode keeps the signal GPIO as an input without a pull, then reads the paired pull-control GPIO through the 10 kOhm resistor using its RP2040 internal pull-up. Communication and the 30 ms debounce are identical for both modes.

The weaker internal pull-up makes input_safe more sensitive to leakage and interference on long cables. The series resistor provides current limiting for the sampled GPIO, not protection against external overvoltage; the signal GPIO remains physically connected to the port.

Impulse counter

Ports configured as impulse_counter or impulse_counter_safe count valid active-low pulses from zero after every boot. Counts are held only in RAM and are not written to flash. Each LOW pulse and the preceding HIGH level must last at least 1 ms. A valid pulse is counted on its rising edge.

Impulse changes never produce unsolicited packets. Absolute counts are included only in complete state payloads produced by {"g":"a"} and by the optional automatic complete-state broadcast. Counters can be reset only by restarting the module or reconfiguring its ports.

The normal mode reads the signal GPIO and uses the board's 10 kOhm pull-up. The safe mode reads the paired pull-control GPIO through the 10 kOhm resistor and uses the weaker RP2040 internal pull-up. The normal mode is preferred for fast pulses and long or noisy wiring.

ADC input

Port A can be configured as four ADC inputs. Passive modes publish values only as part of the response to {"g":"a"}. Active modes additionally send unsolicited reliable packets containing only inputs whose filtered value has changed sufficiently:

{"adc":{"A":{"1":37.6}}}

ADC values use a percentage range from 0.0 through 100.0. Samples are filtered at the native 12-bit resolution and then quantised to 9 bits before conversion to percent. This provides 512 distinct input levels, so the displayed decimal place does not imply 0.1 percent physical resolution.

The RP2040 ADC provides approximately 8.7 effective bits and is affected by the documented RP2040-E11 differential non-linearity error. These inputs are intended for controls such as potentiometers rather than precision voltage measurement.

Values up to 0.4 percent are reported as 0.0, while values from 99.6 percent are reported as 100.0. Active reporting uses a 1.0 percent deadband and is limited to one packet every 200 ms. Changes accumulated during that interval are coalesced, with only the latest value retained for each pin. A failed delivery keeps the latest values pending and delays the next attempt by one second.

The adc_raw_passive and adc_raw_active modes disable the board pull-ups. The adc_pullup_passive and adc_pullup_active modes enable them. All ADC modes are available only on port A.

PWM outputs

PWM is available only on ports B and C. A complete port is configured at one of the fixed hardware frequencies by using the pwm_50hz, pwm_1khz or pwm_25khz port mode. All pins are push-pull outputs and start at zero percent after every restart.

One command can update one or more pins, and can update both configured ports:

{"pwm":{"B":{"0":50,"2":75}}}

Port names and pin indexes are validated before any output is changed. Duty cycles must be integers from 0 through 100. After applying the complete command, the module immediately returns the complete state of every port named in the request:

{"pwm":{"B":{"0":50,"1":0,"2":75,"3":0}}}

The current state of all configured PWM ports can also be requested directly:

{"pwm":"get"}

PWM state is also included in every response to {"g":"a"}. Duty cycles are runtime state and are not written to flash.

Digital outputs

The output and output_safe modes provide four logical outputs on any universal port. Every pin starts at logical zero after restart. A single command can update one or more pins on one or more configured ports:

{"output":{"B":{"0":1,"2":0}}}

Port names, pin indexes and all values are validated before any output changes. Values must be the integers 0 or 1. The response contains the complete state of every port named in the request:

{"output":{"B":{"0":1,"1":0,"2":0,"3":0}}}

The current state of all configured output ports can also be requested:

{"output":"get"}

Output state is included in every response to {"g":"a"} and is not persisted to flash.

In output mode the signal GPIO is a direct push-pull output. In output_safe mode the signal GPIO remains an input and the paired pull-control GPIO drives the signal only through the board's 10 kOhm resistor. Both modes use exactly the same HBUS commands and responses.

The resistor limits current during an output conflict or short circuit. It is not overvoltage protection because the signal GPIO remains physically connected to the port. The safe mode is intended for static logic inputs and MOSFET gates; it cannot directly supply meaningful load current. External circuitry must define a safe state while the module is starting or reset.

Rotary encoder

{"encoder_delta":{"B":{"2":-1}}}

The encoder channel is reported at index 2. Multiple movements accumulated during one interval are summed, so the delta can be less than -1 or greater than 1. The encoder push button is reported as button_press at index 3.

Device control

Read device settings

Request payload:

{"device":"get_settings"}

Response payload:

{
  "device_settings": {
    "address": "cf38542d",
    "alias": "Kitchen switches",
    "hbus_baudrate": 19200,
    "identify_interval": 5,
    "state_interval": "DISABLED",
    "bus_mode": "hbus",
    "active_bus_mode": "hbus",
    "modbus_address": 17
  }
}

identify_interval is expressed in minutes. state_interval is expressed in seconds. Either setting can contain DISABLED. Device settings are intentionally separate from the identify payload and are returned only after an explicit unicast request.

bus_mode is the protocol selected for the next boot. active_bus_mode is the protocol used by the current boot and can differ until the module is restarted. The debug DIP switch forces active_bus_mode to hbus without changing the persisted bus_mode. modbus_address is retained even in HBUS mode.

Restart

Request payload:

{"device":"restart"}

The module performs an immediate hardware reset after transport-level ACK. No application response is sent.

Factory reset

Request payload:

{"device":"factory_reset"}

The module removes persistent configuration, crash data and all filesystem firmware, then restarts into the frozen application. No application response is sent before reset.

Set device address

Request payload:

{"device":{"set_device_address":"12abcdef"}}

Successful response:

{"device":"DEVICE ADDRESS UPDATED"}

Failure response:

{"device":"DEVICE ADDRESS UPDATE FAILED"}

Clients must send exactly eight hexadecimal characters and must not send 00000000 or ffffffff. The current implementation evaluates the first eight characters, so longer strings are truncated rather than rejected. The address is normalized to lowercase and persisted immediately. The successful response already uses the new source address.

Set HBUS baud rate

Request payload:

{"device":{"set_hbus_baudrate":115200}}

Successful response returns the complete persisted settings:

{"device_settings":{"address":"cf38542d","alias":"Kitchen switches","hbus_baudrate":115200,"identify_interval":5,"state_interval":"DISABLED"}}

Failure response:

{"device":"HBUS BAUDRATE UPDATE FAILED"}

Accepted rates are 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200, 230400 and 460800. The response is transmitted at the current rate. The new rate is loaded from flash on the next restart; the controller must change its baud rate accordingly.

Set bus mode

Request payload:

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

mode must be hbus or modbus. modbus_address must be from 1 through 247. The command stores both values and returns the complete device_settings payload. The active transport changes only after restart. The configured UART baud rate is shared by HBUS and Modbus RTU.

Failure response:

{"device":"BUS MODE UPDATE FAILED"}

For recovery, starting with the debug DIP switch enabled forces HBUS at the stored baud rate. Factory reset restores HBUS at 19200 baud.

Set module alias

Request payload:

{"device":{"set_alias":"Kitchen switches"}}

The alias is trimmed, persisted immediately and included in every identify response. It may contain UTF-8 text, must not contain control characters and is limited to 64 encoded bytes. An empty string clears it. A successful update returns the complete identify payload by unicast. Failure response:

{"device":"ALIAS UPDATE FAILED"}

Set automatic identify interval

Request payload:

{"device":{"set_identify_interval":30}}

Accepted intervals are 1, 5, 10, 30 and 60 minutes. Automatic idle identify can be disabled with:

{"device":{"set_identify_interval":"DISABLED"}}

The value is persisted immediately. A successful update returns the complete device_settings payload by unicast, including the new identify_interval. Invalid values produce:

{"device":"IDENTIFY INTERVAL UPDATE FAILED"}

Disabling automatic identify does not suppress explicit unicast or broadcast identify requests. It also does not suppress the one identify scheduled with a random delay of up to ten seconds after a debug-mode startup.

Set automatic state broadcast interval

Request payload:

{"device":{"set_state_interval":30}}

Accepted intervals are 5, 30, 60 and 300 seconds. Automatic state broadcasting is disabled by default and can be disabled explicitly with:

{"device":{"set_state_interval":"DISABLED"}}

The value is persisted immediately. A successful update returns the complete device_settings payload by unicast, including the new state_interval. Invalid values produce:

{"device":"STATE INTERVAL UPDATE FAILED"}

At each enabled interval the module builds the same complete state payload as for {"g":"a"} and sends it to 00000000. This packet has r set to 0: no ACK is requested and no retransmission is performed. A failed or busy-line attempt is skipped until the next configured interval.

After startup or an interval change, the first send is delayed by one interval plus a random phase of up to one complete interval. Every following interval has a random jitter of plus or minus ten percent. Modules started together therefore do not remain synchronized.

Port configuration

Read mapping

Request payload:

{"port":"get"}

Example response:

{"port":{"A":"button_simple","B":"encoder_reverse"}}

An empty table is reported as:

{"port":"PORT TABLE EMPTY"}

Update mapping

Request payload:

{"port":{"A":"button_double","B":"encoder","C":"input_safe"}}

The supplied keys are merged into the existing mapping and the complete table is returned. Supported modes are button_simple, button_double, input, input_safe, impulse_counter, impulse_counter_safe, output, output_safe, encoder, encoder_reverse, pwm_50hz, pwm_1khz, pwm_25khz, adc_raw_passive, adc_raw_active, adc_pullup_passive and adc_pullup_active. PWM modes are accepted only for ports B and C; ADC modes are accepted only for port A. The mapping is persisted immediately. Restart the module to rebuild input handlers, outputs and sensor detection from the new mapping.

Clear mapping

Request payload:

{"port":"reset"}

Response:

{"port":"PORT TABLE TRUNCATED"}

The active runtime configuration remains in effect until restart.

Sensor compensation

Compensation values are additive deltas applied to matching fields in the response to {"g":"a"}. They do not overwrite the raw measurement cache.

Read compensation

{"compensation":"get"}

Example response:

{"compensation":{"ds":{"100000a3":{"tmp":-0.4}},"scd4x":{"0":{"co2":25}}}}

An empty table is reported as:

{"compensation":"COMPENSATION TABLE EMPTY"}

Update compensation

{"compensation":{"ds":{"100000a3":{"tmp":-0.4}}}}

The supplied top-level sensor groups are merged into the existing table, persisted immediately and returned in the response. Updating a group such as ds replaces that complete group; the merge is not recursive.

Clear compensation

{"compensation":"reset"}

Response:

{"compensation":"COMPENSATION TABLE TRUNCATED"}

SCD4x configuration

SCD4x devices are addressed by the zero-based index used in measurement data. One request can contain commands for multiple sensors, but each sensor record contains one command.

Request shape:

{
  "scd4x": {
    "0": {"cmd":"set_sensor_altitude","value":250}
  }
}

Response shape:

{
  "scd4x": {
    "0": {"set_sensor_altitude":250}
  }
}

Periodic measurement is stopped before each command and restarted afterward.

Command Request value Response value
persist_settings none true
get_temperature_offset none Degrees Celsius
set_temperature_offset Degrees Celsius Echoed numeric value
get_sensor_altitude none Metres
set_sensor_altitude Metres Echoed integer
get_ambient_pressure none Pascals
set_ambient_pressure Pascals Echoed integer
get_serial_number none Decimal string
get_sensor_variant none SCD40, SCD41, SCD43 or an unknown-code string

Ambient pressure is clamped internally to 70000–120000 Pa. Unknown sensor indexes and commands are ignored and do not produce an application response.

Firmware transfer

Firmware commands use the fw payload object. A sender should include a new eight-character lowercase hexadecimal request_id in every request. The module copies it into the corresponding response, allowing hbusd to reject late responses from a previous retry.

Firmware responses have this shape:

{
  "fw": {
    "request_id":"12ab34cd",
    "state":"receiving",
    "id":"4386db09768e458e",
    "offset":512,
    "size":43548
  }
}

The transfer ID is the first 16 characters of the complete archive SHA-256. The maximum decoded chunk size is 512 bytes.

Begin or resume

{
  "fw": {
    "command":"begin",
    "request_id":"12ab34cd",
    "size":43548,
    "sha256":"4386db09768e458e5f76cb9036dbbdf4f39d62d4906b76cfcf20fc8589e131fe"
  }
}

The response state is receiving, ready or installed. For an existing matching transfer, offset tells the sender where to resume. Starting a different archive discards an incomplete or ready archive, but never removes installed firmware slots.

Send chunk

{
  "fw": {
    "command":"chunk",
    "request_id":"23bc45de",
    "id":"4386db09768e458e",
    "offset":0,
    "data":"<Base64 data>"
  }
}

Chunks must be contiguous. Repeating a chunk that is already completely stored is accepted only when its bytes match. Overlapping, skipped, changed or oversized chunks are rejected.

Finish upload

{"fw":{"command":"finish","request_id":"34cd56ef","id":"4386db09768e458e"}}

finish verifies the complete size and SHA-256 before changing the state to ready. It does not install or activate the archive.

Query status

{"fw":{"command":"status","request_id":"45de67f0"}}

Possible states are:

State Meaning
idle No transfer metadata exists
receiving An incomplete archive is present
ready A complete validated transport archive awaits installation
installed The archive was installed and selected for the next boot
rolled_back Startup of the installed active firmware failed
error The request failed; see error

receiving, ready, installed and rolled_back responses include id, offset and size. Installed states also include version. rolled_back adds reason.

Install

{"fw":{"command":"install","request_id":"56ef7801","id":"4386db09768e458e"}}

Installation validates the protected HBE1 envelope, AES key ID, decrypted inner archive digest, manifest and every extracted file. It then rotates the complete filesystem slots and returns state installed with the manifest version. The module is not restarted automatically.

Abort

{"fw":{"command":"abort","request_id":"67f08912","id":"4386db09768e458e"}}

Abort removes an archive in receiving or ready state and returns idle. Firmware in installed or rolled_back state cannot be aborted because it is already part of the active/previous slot set.

Error response

Protocol, validation and state errors are returned as:

{
  "fw": {
    "request_id":"67f08912",
    "state":"error",
    "error":"firmware chunk offset mismatch"
  }
}

The error text is intended for diagnostics and may become more specific in a future firmware release. Clients should make decisions primarily from state, not by parsing the error string.

Legacy-command error behaviour

Firmware-transfer commands always return a structured success or error state. Older device, port, compensation and SCD4x commands do not share a common error envelope. Depending on the command, they return an uppercase status string, are silently ignored, or only write an exception to the debug log. Therefore:

  • treat the transport ACK only as proof of packet receipt;
  • wait for the documented response when one exists;
  • apply a timeout at the controller;
  • read a setting back after changing persistent configuration.