-
Notifications
You must be signed in to change notification settings - Fork 1
Technical Architecture
This page explains how the core Rust library is organized, how telemetry moves through it, and why the data structures look the way they do. It assumes no prior knowledge of the codebase.
- Runtime schema: endpoints, message types, and payload layouts live in a runtime registry that can be seeded, queried, exported, and synced between nodes.
- Compact on-the-wire representation: a small header, endpoint bitmaps, and optional compression minimize bandwidth.
- Embedded-friendly: no_std support, bounded queues, and stack/heap tradeoffs.
- Multi-language: Rust, C, and Python expose the same runtime schema and packet APIs.
- Deterministic behavior: validation and dedupe rules are consistent across languages.
src/config.rs (source):
packaged configuration defaults, runtime tuning state, and the runtime schema registry for DataType and
DataEndpoint metadata.
src/lib.rs (source):
schema metadata (MessageMeta, MessageElement, MessageDataType, MessageClass).
src/packet.rs (source):
Packet validation, formatting, packet IDs, and migration-safe wire-contract state.
src/small_payload.rs (source):
inline-optimized payload storage (SmallPayload).
src/wire_format.rs (source): compact wire format, ULEB128 helpers, envelope peek, packet IDs from wire.
src/router.rs (source): router core, queues, endpoint handlers, side-based routing.
src/relay.rs (source): schema-agnostic fanout relay between sides.
src/queue.rs (source): bounded deque used by router and relay.
src/c_api.rs (source) and src/python_api.rs (source): FFI bindings (C ABI and pyo3).
The registry starts with built-in internal endpoints/types for telemetry errors, reliable control, discovery, and time sync. User endpoints and data types are added at runtime through direct APIs, optional JSON seeding, handler registration, or discovery schema sync.
Core schema types:
-
DataEndpoint: transparent runtime ID (u32) for logical destinations. -
DataType: transparent runtime ID (u32) for message types. -
MessageMeta: per-DataTypemetadata (name, element layout, allowed endpoints). -
MessageElement:Static(count, MessageDataType, MessageClass)orDynamic(MessageDataType, MessageClass). -
MessageDataType: primitive element type (Float32, UInt16, String, Binary, etc.). -
MessageClass: logical class (Data,Warning,Error).
Why these structures exist:
- MessageMeta is registry-backed so it can change as peers add or announce schema over time.
-
String lookup APIs (
DataEndpoint::named,DataType::named) keep application code readable while the wire format remains numeric. - MessageElement separates shape from data type so the same primitive type can be used for static and dynamic payloads.
- MessageClass is tied to element layout so formatting and error generation know the intent of the message.
- Discovery schema sync exports endpoint/type definitions, merges compatible remote schema, and resolves conflicts deterministically.
Packet is the validated, shareable container used by the router and FFI layers. It contains:
-
ty: DataType(logical message type). -
data_size: usize(cached payload length; must matchpayload.len()). -
sender: Arc<str>(logical sender identifier). -
endpoints: Arc<[DataEndpoint]>(destinations; must be non-empty). -
timestamp: u64(ms; treated as uptime below 1e12, or epoch ms otherwise). -
payload: StandardSmallPayload(inline optimized; see below). -
wire_shape: Option<MessageElement>when the frame carried inline decode metadata. -
wire_target_senders: Arc<[u64]>when the frame carried a frozen destination-holder set.
Validation rules enforced by Packet::new and Packet::validate:
- Endpoints list must be non-empty.
- For static layouts, payload length must be exactly
count * data_type_size. - For dynamic layouts:
- Numeric/bool types: payload length must be a multiple of element width.
- String types: trailing NULs are ignored and remaining bytes must be valid UTF-8.
- Binary types: no UTF-8 requirement (raw bytes).
Packet IDs are not packed. Packet::packet_id hashes:
- compact source address derived from the sender/discovery mapping
- message name (
DataTypeas string) - endpoint names (in order)
- timestamp + data_size (little-endian)
- payload bytes
This produces the same ID across different links, which enables de-duplication.
SmallPayload<INLINE> stores short payloads directly on the stack and spills to Arc<[u8]> when larger. The inline
size is controlled by MAX_STACK_PAYLOAD via define_stack_payload!, and the default inline capacity is 64 bytes.
That value is a compiled capacity because it affects the stack payload type layout; active static string/binary sizing,
compression threshold, retry counts, reliable queue caps, node identity, memory budgets, and time-sync roles are runtime
configuration.
Why this matters:
- Small telemetry values avoid heap allocation and
Arctraffic. - Larger payloads still have cheap clone semantics because they are
Arc-backed.
The compact v2 wire format is implemented in src/wire_format.rs (source). A packet is encoded as a compact v2 frame with schema-derived endpoints or an explicit endpoint bitmap, optional wire contract, optional reliable header, payload bytes, and a CRC32 trailer. See Technical-Wire-Format for the exact field order.
Design choices:
- ULEB128 varints minimize size for small values.
- Schema-derived endpoints omit endpoint bytes when a packet uses the data type's default endpoint set.
-
Endpoint bitmap represents custom endpoint sets; width is fixed from
MAX_VALUE_DATA_ENDPOINT, not from the current runtime registry contents. - Wire contract carries inline payload shape and frozen destination-holder sender hashes so in-flight packets stay decodable and correctly targeted across topology/schema churn.
- Sender/payload compression is applied independently only when it reduces the wire size.
- CRC32 trailer validates the entire frame before normal decode.
packet_id_from_wire parses only as much as needed to compute the same packet ID as Packet::packet_id. It
always hashes decompressed sender/payload bytes, so dedupe works across compressed and uncompressed links.
TelemetryEnvelope is a header-only view that can be obtained by peek_envelope without copying the payload.
Router and relay queue storage is built on BoundedDeque<T>:
- Byte-budgeted: each item reports a
byte_costvia theByteCosttrait. - Hard element cap: capacity never exceeds
max_elems(computed fromsize_of::<T>()). - Eviction policy: pop from the front until new item fits in byte budget.
- Growth policy: multiplicative growth controlled by
QUEUE_GROW_STEP.
The public MAX_QUEUE_BUDGET is shared dynamically across router/relay RX queues, TX queues,
recent-ID caches, reliable replay/out-of-order buffers, discovery topology state, and runtime
schema registry memory. Recent-ID caches preallocate their final storage at construction and
reserve that amount from the shared budget immediately. This design keeps memory use bounded and
avoids unbounded VecDeque growth in embedded targets while letting the active part of the system
use available queue budget when other internal queues are quiet.
The router is the main API for logging, receiving, dispatching, and (optionally) relaying.
Key structures:
-
RouterConfig: holds localEndpointHandlerdefinitions in anArc<[EndpointHandler]>. -
EndpointHandler: packet or packed handler for a specificDataEndpoint. -
RouterSideId: identifies a named side (UART/CAN/RADIO/etc.). -
RouterItem: eitherPacketor packed bytes.
Receive flow:
- RX bytes or packet are processed immediately or queued (
rx_*vsrx_*_queue). - Reliable side headers are handled first when the ingress side has reliable delivery enabled.
- Packet ID is computed. For packed bytes, the router tries
packet_id_from_wireand falls back to hashing raw bytes if needed. - If the packet ID is already in the recent cache, it is dropped.
- Local endpoint handlers are invoked.
- The router forwards the packet according to the current route rules, discovery/path-selection state, and any frozen destination-holder contract attached to the packet.
Forwarding is driven by:
- the active side ingress/egress policy
- route overrides and typed route overrides
- discovery-learned endpoint reachability
- link-local side scope
- route-selection policy (
Fanout,Weighted,Failover)
Transmit flow:
-
log*builds a newPacketfrom typed data, validates sizes, and packs it. -
tx*sends a pre-builtPacket(validated) or packed bytes. - Queue variants (
*_queue) defer the work untilprocess_*_queue.
Error handling:
- Local handler failures are retried using the active runtime
max_handler_retriesvalue. - If a handler ultimately fails, the packet ID is removed from the dedupe cache so a resend can be processed later.
- The router emits a
TelemetryErrorpacket for local handlers when possible.
Relay is schema-agnostic and purely forwards packets between named sides (UART/CAN/RADIO/etc.).
- Each side registers a TX handler for packed bytes or full packets.
- Each side can opt out of reliable sequencing/ACKs (useful for TCP links).
- The relay maintains RX/TX queues and a recent-ID cache like the router.
- Fanout clones the
Arcfor payload sharing; it does not decode unless needed for a handler. - Dedupe ignores the source side so duplicates from different links are dropped.
Routers and relays use an internal RouterMutex so that most APIs can take &self while still protecting shared state.
The clock source is injected via the Clock trait so embedded builds can supply their own timing source.
Producer Router/Relay Consumer
-------- ------------- --------
log()/tx() -> validate -> pack -> tx(bytes, link) -> transport -> rx_packed()
rx(bytes) -> unpack -> dedupe -> handlers -> (optional) relay forward
If you need build-time or schema details, see Build-and-Configure and Technical-Telemetry-Schema.