Status: Stable (protocol v1 extensions)
Source of truth:firmware_core/protocol.h,rivr_layer/rivr_svc.h,rivr_layer/rivr_svc.c
Depends on: routing layer (ACK/retry, §3), RIVR engine (§4)
The application services layer sits immediately above the routing stack. It provides four standardised packet types that cover the most common embedded mesh use-cases without dynamic allocation and within the 231-byte maximum payload constraint of the RIVR wire format.
| Service | PKT type | ID | Direction | Fixed payload | Max text |
|---|---|---|---|---|---|
| CHAT | PKT_CHAT |
1 | any–any | — (free-form) | 224 B |
| TELEMETRY | PKT_TELEMETRY |
8 | sensor → gateway | 11 B | — |
| MAILBOX | PKT_MAILBOX |
9 | any → any | 7-byte header + text | RIVR_PKT_MAX_PAYLOAD − 7 |
| ALERT | PKT_ALERT |
10 | any → all | 7 B | — |
All constants are defined in firmware_core/protocol.h.
The C-layer dispatch lives in rivr_layer/rivr_sources.c, step 5d,
immediately after route-reply processing and before the frame is injected into
the RIVR engine. This ordering guarantees:
- Routing tables are up to date before service handlers run.
- Service handlers can compare
recipient_idagainst the freshly-updatedg_my_node_id. - The RIVR engine receives every frame regardless of whether a service handler consumed it (non-exclusive dispatch).
/* ── rivr_sources.c, step 5d ── */
if (pkt_hdr.pkt_type == PKT_CHAT)
handle_chat_message(&pkt_hdr, payload, len, frame.rssi_dbm);
if (pkt_hdr.pkt_type == PKT_TELEMETRY)
handle_telemetry_publish(&pkt_hdr, payload, len);
if (pkt_hdr.pkt_type == PKT_MAILBOX)
handle_mailbox_store(&pkt_hdr, payload, len, now_ms);
if (pkt_hdr.pkt_type == PKT_ALERT)
handle_alert_event(&pkt_hdr, payload, len);Each handler logs a structured @-prefixed JSON record to UART (see §7) and
then returns; relay/ACK decisions are made by the surrounding C-layer, not the
handler.
PKT_CHAT predates the application services layer and retains its original free-
form payload. handle_chat_message() provides a uniform log record and calls
the role-specific UI hook.
Free-form UTF-8 text, 0–RIVR_PKT_MAX_PAYLOAD bytes. No length prefix.
- Emits a
@CHTlog record (§7.1) on every reception. - Calls
rivr_cli_on_chat_rx(), which is a zero-cost inline stub on non-client builds (#ifndef RIVR_ROLE_CLIENT).
dst_id = 0→ broadcast (all nodes receive and relay).dst_id = <id>→ unicast via route cache; ACK/retry applies.
Compact fixed-size frame for periodic numeric sensor readings from low-power
nodes. Designed to be generated without printf or sprintf.
┌─────────┬──────┬────────────┬───────────────────────────────────────────────┐
│ Offset │ Size │ Type │ Description │
├─────────┼──────┼────────────┼───────────────────────────────────────────────┤
│ 0 │ 2 │ u16 LE │ sensor_id — application-defined sensor index │
│ 2 │ 4 │ i32 LE │ value — scaled integer (unit-dependent) │
│ 6 │ 1 │ u8 │ unit_code — UNIT_* constant (see §4.1) │
│ 7 │ 4 │ u32 LE │ timestamp — seconds since node boot (0=unset) │
└─────────┴──────┴────────────┴───────────────────────────────────────────────┘
Total: SVC_TELEMETRY_PAYLOAD_LEN = 11
| Constant | Value | Meaning | Scale factor |
|---|---|---|---|
UNIT_NONE |
0 | dimensionless / raw | 1 |
UNIT_CELSIUS |
1 | temperature | ×100 (2500 = 25.00 °C) |
UNIT_PERCENT_RH |
2 | relative humidity | ×100 (5500 = 55.00 %) |
UNIT_MILLIVOLTS |
3 | voltage | ×1 (3300 = 3300 mV) |
UNIT_DBM |
4 | RSSI / TX power | ×1 signed (−80 = −80 dBm) |
UNIT_PPM |
5 | concentration | ×100 (40000 = 400.00 ppm) |
UNIT_CUSTOM |
255 | application-defined | 1 |
- Validates
payload_len >= SVC_TELEMETRY_PAYLOAD_LEN. - Supports bundled readings: a single
PKT_TELEMETRYframe may carryNconsecutive 11-byte readings (N = payload_len / SVC_TELEMETRY_PAYLOAD_LEN). Each reading is decoded independently and emits its own@TELlog record. - Decodes all fields with
memcpy(no alignment assumption). - Emits a
@TELlog record per reading (§7.2) and callsrivr_ble_companion_push_telemetry()for each BLE-connected companion.
The firmware sensor subsystem (firmware_core/sensors.c) assigns these fixed
sensor IDs when the corresponding feature flags are enabled in the variant config:
| Sensor ID | Feature flag | Sensor | Unit code |
|---|---|---|---|
| 1 | RIVR_FEATURE_DS18B20 |
DS18B20 temperature | UNIT_CELSIUS (×100) |
| 2 | RIVR_FEATURE_AM2302 |
AM2302 relative humidity | UNIT_PERCENT_RH (×100) |
| 3 | RIVR_FEATURE_AM2302 |
AM2302 temperature | UNIT_CELSIUS (×100) |
| 4 | RIVR_FEATURE_VBAT |
Battery voltage | UNIT_MILLIVOLTS |
All sensor features default to disabled (= 0) in every variant. Enable
them by overriding the feature flag in the variant's platformio.ini
build_flags (e.g. -DRIVR_FEATURE_VBAT=1).
Sensor IDs 5–65535 are available for application-defined sensors.
- Typically broadcast (
dst_id = 0) from sensor nodes. - Relay nodes gate re-transmission with
budget.toa_usin RIVR programs.
Store-and-forward primitive for nodes that are intermittently reachable. The C-layer maintains a static 8-entry ring-buffer for messages addressed to this node; the RIVR program controls when buffered frames are re-forwarded.
┌─────────┬──────┬────────────┬───────────────────────────────────────────────┐
│ Offset │ Size │ Type │ Description │
├─────────┼──────┼────────────┼───────────────────────────────────────────────┤
│ 0 │ 4 │ u32 LE │ recipient_id — final destination (0 = any) │
│ 4 │ 2 │ u16 LE │ msg_seq — per-source sequence counter │
│ 6 │ 1 │ u8 │ flags — MB_FLAG_* bitmask (see §5.1) │
│ 7 │ N │ UTF-8 │ text body (0..SVC_MAILBOX_MAX_TEXT bytes) │
└─────────┴──────┴────────────┴───────────────────────────────────────────────┘
Header: SVC_MAILBOX_HDR_LEN = 7
Max text: SVC_MAILBOX_MAX_TEXT = RIVR_PKT_MAX_PAYLOAD − SVC_MAILBOX_HDR_LEN
| Constant | Value | Meaning |
|---|---|---|
MB_FLAG_NEW |
0x01 | Message not yet delivered to recipient |
MB_FLAG_DELIVERED |
0x02 | Recipient ACK received by originator |
MB_FLAG_FORWARD |
0x04 | Frame is a relay copy, not the original |
/* rivr_layer/rivr_svc.h */
#define MB_STORE_CAP 8u
typedef struct {
uint32_t src_id, recipient_id;
uint16_t msg_seq;
uint8_t flags, text_len;
char text[SVC_MAILBOX_MAX_TEXT + 1u]; /* NUL-terminated */
uint32_t stored_at_ms;
bool valid;
} mailbox_entry_t;
typedef struct {
mailbox_entry_t entries[MB_STORE_CAP];
uint8_t head, count;
} mailbox_store_t;
extern mailbox_store_t g_mailbox_store; /* BSS, zero-initialised */- Capacity: 8 entries (
MB_STORE_CAP). - Eviction policy: LRU ring — head pointer advances and overwrites the oldest entry when the store is full.
- Allocation: entirely within the BSS segment; no heap (
malloc) used. - Persistence: not persisted across resets; a
deliveredflag sweep can be added by the application on reconnect.
- Validates
payload_len >= SVC_MAILBOX_HDR_LEN. - If
recipient_id == g_my_node_idorrecipient_id == 0: stores intog_mailbox_storeand setsstored = truein the log record. - Always emits a
@MAILlog record (§7.3) regardless of whether stored.
- Unicast (
dst_id= next-hop towards recipient) when the sender has a route; broadcast (dst_id = 0) when route is unknown. - Relay nodes buffer and re-forward under RIVR program gating (see
examples/store_forward_mailbox.rivr).
Priority event notification — fire once, propagate everywhere. The alert payload is compact enough to fit in a single slot even on lossy links.
┌─────────┬──────┬────────────┬───────────────────────────────────────────────┐
│ Offset │ Size │ Type │ Description │
├─────────┼──────┼────────────┼───────────────────────────────────────────────┤
│ 0 │ 1 │ u8 │ severity — ALERT_SEV_* constant (see §6.1) │
│ 1 │ 2 │ u16 LE │ alert_code — application-defined event code │
│ 3 │ 4 │ i32 LE │ value — associated numeric data (e.g. temp) │
└─────────┴──────┴────────────┴───────────────────────────────────────────────┘
Total: SVC_ALERT_PAYLOAD_LEN = 7
| Constant | Value | Colour | Behaviour |
|---|---|---|---|
ALERT_SEV_INFO |
1 | — | Log only |
ALERT_SEV_WARN |
2 | yellow | Log + human-readable [!] ALERT line |
ALERT_SEV_CRIT |
3 | red | Log + [!] ALERT + RIVR_LOGW() |
- Validates
payload_len >= SVC_ALERT_PAYLOAD_LEN. - Emits a
@ALERTlog record (§7.4) on every reception. - For
ALERT_SEV_WARNandALERT_SEV_CRIT: also prints a human-readable[!] ALERTline with decoded fields. - For
ALERT_SEV_CRIT: additionally callsRIVR_LOGW()to surface the event in the ESP-IDF log stream.
- Always broadcast (
dst_id = 0); routed alerts usePKT_DATAinstead. - Relay nodes assign elevated duty-cycle budget:
budget.toa_us(300000, 0.20, 280000)(20 %, seechat_relay.rivr).
Each handler prints a single-line JSON record to UART (stdout / USB serial)
prefixed with @ so host tools can filter it out of arbitrary debug output.
All records end with \r\n.
Log output is produced with printf() — no buffering, no heap.
@CHT {"src":"0x<HEX8>","dst":"0x<HEX8>","rssi":<dBm>,"len":<N>,"text":"<TEXT>"}
| Field | Type | Description |
|---|---|---|
src |
hex string | Sender node ID |
dst |
hex string | Destination node ID (0x00000000 = broadcast) |
rssi |
signed integer | Received signal strength in dBm |
len |
unsigned integer | Payload length in bytes |
text |
JSON string | Message body (" → \", \ → \\, control/non-ASCII → .) |
@TEL {"src":"0x<HEX8>","sid":<N>,"val":<V>,"unit":<U>,"unit_str":"<name>","ts":<T>}
| Field | Type | Description |
|---|---|---|
src |
hex string | Sensor node ID |
sid |
unsigned integer | Sensor ID (application-defined) |
val |
signed integer | Scaled sensor value |
unit |
unsigned integer | UNIT_* numeric code |
unit_str |
string | Human-readable unit abbreviation (e.g. "C*100", "mV", "ppm*100") |
ts |
unsigned integer | Node-boot-relative timestamp in seconds (0 = unset) |
@MAIL {"src":"0x<HEX8>","to":"0x<HEX8>","seq":<N>,"flags":<F>,"stored":<bool>,"text":"<TEXT>"}
| Field | Type | Description |
|---|---|---|
src |
hex string | Originating node ID |
to |
hex string | Intended recipient ID (0x00000000 = any store node) |
seq |
unsigned integer | Per-source message sequence number |
flags |
unsigned integer | MB_FLAG_* bitmask |
stored |
true / false |
Whether the frame was saved in g_mailbox_store |
text |
JSON string | Message body (same sanitisation as @CHT) |
@ALERT {"src":"0x<HEX8>","sev":<S>,"sev_str":"<name>","code":<C>,"val":<V>}
| Field | Type | Description |
|---|---|---|
src |
hex string | Originating node ID |
sev |
unsigned integer | ALERT_SEV_* numeric code |
sev_str |
string | "INFO", "WARN", "CRIT", or "?" for unknown |
code |
unsigned integer | Application-defined alert code |
val |
signed integer | Associated numeric value |
The RIVR engine sees every received frame (step 6 in rivr_sources.c),
including service frames. RIVR programs gate relay — not reception.
Relay all service traffic with duty-cycle gating:
source rf_rx @lmp = rf;
let tel = rf_rx |> filter.pkt_type(8) |> budget.toa_us(300000, 0.10, 280000) |> throttle.ticks(1);
let alert = rf_rx |> filter.pkt_type(10) |> budget.toa_us(300000, 0.20, 280000);
emit { io.lora.tx(tel); }
emit { io.lora.tx(alert); }
Store-and-forward mailbox with window buffering:
source rf_rx @lmp = rf;
let mail = rf_rx
|> filter.pkt_type(9)
|> window.ticks(12, 8, "drop_oldest")
|> delay.ticks(3)
|> budget.toa_us(600000, 0.04, 280000);
emit { io.lora.tx(mail); }
emit { io.usb.print(mail); }
Aggregate telemetry reception in a 30-second window:
source rf_rx @lmp = rf;
let count = rf_rx |> filter.pkt_type(8) |> fold.count();
let win = rf_rx |> filter.pkt_type(8) |> window.ms(30000);
emit { io.usb.print(count); } // running total
emit { io.usb.print(win); } // window events for outage detection
See examples/ for complete programs:
examples/chat_relay.rivr— all-services relayexamples/store_forward_mailbox.rivr— buffered mailboxexamples/telemetry_periodic.rivr— telemetry aggregation + heartbeat
- Reserve a
PKT_*constant infirmware_core/protocol.h(next is 12). - Define payload length, field layout, and any unit/flag constants there.
- Declare the handler in
rivr_layer/rivr_svc.h. - Implement the handler in
rivr_layer/rivr_svc.cfollowing the same BSS-only / no-mallocpattern. - Add dispatch in
rivr_sources.cstep 5d. - Update
firmware_core/CMakeLists.txtif a new.cfile is created. - Add
filter.pkt_type(N)usage in the relevant example program. - Document the payload format and log record in this file.