RIVR is a stream-graph runtime and domain-specific language designed for resource-constrained LoRa mesh nodes, implemented in Rust and C.
LoRa mesh devices must process radio frames under strict constraints:
| Constraint | Consequence |
|---|---|
| Duty cycle (≤ 1 % or ≤ 10 %) | Cannot retransmit every received frame |
| No heap allocator on bare metal | Fixed-size buffers only |
| No RTOS thread isolation | All processing in a single main loop |
| Binary on-air format | Frames must be decoded and classified efficiently |
Writing this logic in plain C leads to deeply nested if/else trees that are
hard to test and impossible to replay offline.
RIVR lets you describe the policy as a declarative stream pipeline:
source rf_rx @lmp = rf_rx;
let chat = rf_rx
|> filter.pkt_type(1)
|> budget.toa_us(360000, 0.10, 360000)
|> throttle.ticks(1);
emit { rf_tx(chat); }
This program is parsed, compiled to a DAG, and executed by the RIVR runtime — either inside the ESP32 firmware (via a C FFI bridge) or on a host machine for testing and replay.
rivr/
├── rivr_core/ Rust library — parser, compiler, runtime
│ └── src/
│ ├── ast.rs Abstract Syntax Tree
│ ├── parser.rs Recursive-descent parser
│ ├── compiler.rs AST → Node DAG compiler
│ ├── ffi.rs C-ABI exports (#[no_mangle])
│ └── runtime/
│ ├── engine.rs Scheduler + top-level run loop
│ ├── node.rs All operator NodeKind variants + process()
│ ├── value.rs Value enum (Int / Bool / Str / Bytes / Unit)
│ ├── event.rs Event = Stamp + Value + tag + seq
│ ├── fixed.rs FixedText<N> and FixedBytes<N> for no-alloc
│ └── opcode.rs Compact OpCode enum (introspection)
├── rivr_host/ Rust binary — demos + Replay 2.0
│ └── src/
│ ├── main.rs 8 demos (window, filter, budget, merge, …)
│ └── replay.rs Record / replay / assert trace engine
├── firmware_core/ C hardware layer (ESP32 + SX1262)
│ ├── main.c app_main, main loop, RIVR_SIM_MODE
│ ├── radio_sx1262.c/.h SX1262 LoRa driver
│ ├── timebase.c/.h Monotonic + Lamport clocks
│ ├── dutycycle.c/.h C-layer duty-cycle guard
│ ├── platform_esp32.c/.h GPIO / SPI / LED init
│ ├── ringbuf.h Lock-free SPSC ring buffer
│ ├── protocol.c/.h Binary on-air packet format (encode/decode/CRC)
│ ├── routing.c/.h Dedupe cache, TTL, jitter, forward-budget caps, loop-guard
│ ├── route_cache.c/.h Unicast reverse-path cache; `route_cache_best_hop()` three-tier next-hop
│ ├── neighbor_table.c/.h 16-slot EWMA link-quality table; `neighbor_update/best/expire`
│ ├── pending_queue.c/.h 16-slot pending queue (ACK-awaiting unicast frames)
│ ├── rivr_policy.c/.h Runtime @PARAMS policy, role enforcement, origination gate, HMAC sig
│ ├── rivr_ota_core.c/.h Signed PKT_PROG_PUSH gate (Ed25519 + anti-replay, platform-agnostic)
│ ├── rivr_ota_platform.c OTA NVS backend (ESP-IDF); nrf52 + rp2040 variants alongside
│ └── crypto/ Self-contained SHA-256 + HMAC-SHA-256 (no heap)
└── rivr_layer/ RIVR ↔ firmware glue (C)
├── rivr_embed.c/.h Engine init, rivr_tick(), rivr_register_sink()
├── rivr_sinks.c/.h rf_tx, usb_print, log sinks
├── rivr_sources.c/.h rf_rx, CLI, timer sources; step-5d service dispatch
├── rivr_svc.c/.h Application service handlers (CHAT, TELEMETRY, MAILBOX, ALERT)
├── rivr_cli.c/.h Serial CLI chat interface (client builds only)
└── rivr_programs/
└── default_program.h Built-in RIVR program string
Every event carries a Stamp { clock: u8, tick: u64 }.
Two built-in clocks are defined:
| Clock id | Name | Description |
|---|---|---|
| 0 | mono |
Monotonic millisecond wall-clock (ESP32 timer) |
| 1 | lmp |
Lamport logical clock (LoRa mesh send/receive) |
Sources are annotated with @clock_name:
source rf_rx @lmp = rf_rx; // events carry Stamp { clock=1 }
source usb @mono = usb; // events carry Stamp { clock=0 }
Each event's payload is one of:
| Variant | Rust type (alloc) | No-alloc type |
|---|---|---|
Int(i64) |
i64 |
i64 |
Bool(bool) |
bool |
bool |
Str(s) |
String |
FixedText<64> |
Bytes(b) |
Vec<u8> |
FixedBytes<64> |
Unit |
— | — |
Window(v) |
Vec<String> |
(alloc only) |
Operators are chained with the pipe operator:
let clean = source |> op1 |> op2 |> op3;
Each operator transforms or drops events flowing through it.
Processed events are delivered to sinks:
emit {
rf_tx(chat);
usb_print(chat);
}
Sinks call back into C via rivr_emit_dispatch().
| Component | Language | Role |
|---|---|---|
rivr_core |
Rust (no_std + alloc) |
Language runtime — parse, compile, run |
rivr_host |
Rust (std) |
Desktop demos, Replay 2.0, rivrc CLI |
firmware_core |
C | Hardware drivers, simulation mode, OLED display |
rivr_layer |
C | Glue between RIVR runtime and C firmware |
rivr_svc |
C | Application services — CHAT, TELEMETRY, MAILBOX, ALERT handlers |
| Protocol / Routing | C | Binary packet format, mesh flooding |
tools/vscode-rivr |
JSON/TS | VS Code syntax highlighting + snippets |
# Run all host demos (no hardware needed)
cargo run -p rivr_host
# Check / inspect a .rivr source file
cargo run -p rivr_host --bin rivrc -- my_program.rivr
cargo run -p rivr_host --bin rivrc -- --check my_program.rivr # CI mode
# Build the Rust static library for ESP32
cargo build --features ffi
# See docs/en/build-guide.md for the full firmware build procedure