-
Notifications
You must be signed in to change notification settings - Fork 1
Usage Rust
This is the primary API and the source of truth for the Rust-facing behavior.
sedsnet = { path = "path/to/sedsnet" }Or from git:
sedsnet = { git = "https://github.com/Rylan-Meilutis/sedsnet.git", branch = "main" }use sedsnet::config::{
register_data_type_id_with_description, register_endpoint_id_with_description,
};
use sedsnet::router::{EndpointHandler, Router, RouterConfig};
use sedsnet::{
DataEndpoint, DataType, MessageClass, MessageDataType, MessageElement, ReliableMode,
TelemetryResult,
};
fn main() -> TelemetryResult<()> {
let sd_card = register_endpoint_id_with_description(
DataEndpoint(100),
"SD_CARD",
"Local storage endpoint",
false,
)?;
let radio = register_endpoint_id_with_description(
DataEndpoint(101),
"RADIO",
"External radio link",
false,
)?;
register_data_type_id_with_description(
DataType(100),
"GPS_DATA",
"Three f32 GPS values",
MessageElement::Static(3, MessageDataType::Float32, MessageClass::Data),
&[sd_card, radio],
ReliableMode::None,
80,
)?;
let handler = EndpointHandler::new_packet_handler(DataEndpoint::named("SD_CARD"), |pkt| {
println!("rx: {pkt}");
Ok(())
});
let router = Router::new(RouterConfig::new([handler]));
router.add_side_packed("RADIO", |bytes| {
let _ = bytes;
Ok(())
});
router.log(DataType::named("GPS_DATA"), &[1.0_f32, 2.0, 3.0])?;
router.process_all_queues()?;
Ok(())
}On std builds, Router::new(...) uses an internal monotonic clock. For tests, simulation, or
no_std, use Router::new_with_clock(...).
User endpoints and data types are registered at runtime. There are no generated Rust variants for application schema entries in v4.0.0.
Common options:
- call
register_endpoint(...)/register_data_type(...)when the process starts - call the
_idvariants when you need stable numeric IDs on the wire - load a JSON seed with
register_schema_json_file(...),register_schema_json_path(...), orregister_schema_json_bytes(...) - use
DataEndpoint::named("GROUNDSTATION")andDataType::named("GPS_DATA")after registration - use
try_named(...)orendpoint_definition_by_name(...)/data_type_definition_by_name(...)when missing schema should be handled as a normal error path
Registering an endpoint handler for a missing endpoint auto-creates that endpoint in std builds
and broadcasts the new schema through discovery. Registering a data type or endpoint with the same
name/ID and a different shape returns an error.
Network variables are latest-value caches for user data types. A router that enables a network variable remembers the newest local or received packet for that type. User code uses a setter and getter rather than registering a special endpoint: the setter commits the value to the network when permissions allow, and the getter returns the cached value while internally requesting a refresh if the value has never been seen or is stale. Caches are tiered: any router that has enabled or seen the variable can answer the refresh from its local cache, so reconnecting boards can resync from a nearby node instead of always reaching the original producer/master.
Data types can also advertise an E2E cryptography preference:
use sedsnet::config::register_data_type_id_with_description_and_e2e_encryption;
use sedsnet::router::{
NetworkVariablePermissions, Router, RouterConfig, RouterE2eEncryptionMode,
};
use sedsnet::{
DataEndpoint, DataType, E2eEncryptionPolicy, MessageClass, MessageDataType, MessageElement,
ReliableMode,
};
let flight_state = register_data_type_id_with_description_and_e2e_encryption(
DataType(3100),
"FLIGHT_STATE",
"network-managed flight state",
MessageElement::Static(1, MessageDataType::UInt8, MessageClass::Data),
&[DataEndpoint::named("RADIO")],
ReliableMode::None,
90,
E2eEncryptionPolicy::RequireOn,
)?;
let router = Router::new(
RouterConfig::default()
.with_e2e_encryption(RouterE2eEncryptionMode::RequiredOnly)
.with_e2e_key_id(7),
);
router.enable_network_variable(flight_state, NetworkVariablePermissions::READ_WRITE)?;
router.on_network_variable_update(flight_state, |pkt| {
let state = pkt.data_as_u8()?;
Ok(())
})?;
router.set_network_variable(pkt)?;
let cached = router.get_network_variable(flight_state, Some(1_000))?;When a refresh request finds a peer with the value, the original value packet is replayed through
normal endpoint handlers. If the local router lacks read or write permission, the getter/setter
returns TelemetryError::PermissionDenied and peers answer refresh requests with a permission
error packet. on_network_variable_update(...) runs only for inbound updates and refresh replies
that change the local cache; local setters/seeds update the cache without firing that callback.
Router modes are:
-
Disabled: never encrypt; reject data types markedRequireOn -
RequiredOnly: encrypt only required data types -
Preferred: encrypt required and preferred data types -
ForceAll: encrypt every non-control user data type
RouterConfig::default() and RouterConfig::new(...) use Preferred automatically with the
default cryptography feature; minimal builds that explicitly omit cryptography default to
Disabled.
The cryptography feature uses this provider order:
- registered C provider, for C firmware, OS crypto, secure elements, or hardware accelerators
- registered Rust provider, for Rust-only firmware or std applications wrapping OS crypto
- built-in software fallback, only after the application registers a key for the packet
key_id
Packed side traffic carries visible routing metadata and an encrypted payload; the visible header is authenticated as AAD so header tampering fails during open. The built-in fallback uses the provisioned key for authenticated cryptography, but it does not create identity by itself.
#[cfg(feature = "cryptography")]
sedsnet::crypto::register_software_key(
7,
b"32-byte minimum deployment secret....",
)?;For MITM resistance, boards must authenticate the key source. Common deployments use either factory-provisioned PSKs or a network master that acts as the root authority. In the master-root model, boards ship with the master public key or a join PSK, the master signs short-lived board credentials, and peer/session keys are accepted only when the key exchange transcript validates back to that root. Without that authenticated root, an active attacker can substitute keys before the AEAD layer ever sees a packet.
The cryptography feature includes a compact 80-byte managed credential helper for this
master-root model. The master issues a ManagedCredential containing subject id, key id, epoch,
validity window, and permission bits; peers verify it against the provisioned root key before
accepting issued session/group keys.
For board-to-board deployments, run your board-owned quantum-resistant asynchronous key exchange
when discovery learns a peer, derive a low-cost symmetric traffic key, and pass that key through the
provider by key_id. Multi-drop endpoint traffic can use a shared group traffic key when every holder of
that endpoint must decode the same message; AEAD authentication still prevents a receiver from
modifying the frame for downstream boards without detection.
Fragmented links should encrypt the original message before splitting it. Fragments then carry a message id, fragment index/count, source epoch, and route metadata; the receiver reassembles and opens the original authenticated payload. On reconnect, routers should discard incomplete fragments from older master epochs, refresh time/topology, and use network-variable getters to refresh any state that is missing or stale. If the master epoch changed, resync from the current snapshot.
Discovery-enabled routers do not flood unknown user-data routes by fallback. They forward user data
only when discovery or explicit route policy identifies a path; discovery/control traffic still
propagates so the network can recover after partitions. For time-sliced radios, have the TX callback
return TelemetryError::Io("side tx busy") while the radio is in an RX window. Queued work will be
retried later, and measured bring-up/slot throughput can be fed into
note_side_link_probe_sample(side, bytes, duration_ms) to seed adaptive path selection and
control-plane throttling. Once a side is measured as slow, discovery sends minimal reachability
pings between infrequent full schema/topology/time-source refreshes, and router-managed time sync
throttles only that measured slow side while fast sides keep the configured normal cadence.
Routers and relays use named sides such as UART, CAN, or RADIO.
-
add_side_packed(...)andadd_side_packet(...)register egress handlers -
remove_side(...)tombstones a side without renumbering the remaining side ids -
set_side_ingress_enabled(...)andset_side_egress_enabled(...)control directional policy -
set_route(...)andset_typed_route(...)define runtime forwarding rules
There is no RouterMode anymore.
-
Routernow defaults to rule-driven full-mesh forwarding between eligible sides -
Relaykeeps the same full-mesh default - if you want sink-like behavior, disable the specific routes you do not want rather than choosing a separate constructor mode
Example:
use sedsnet::router::Router;
let router = Router::new(RouterConfig::default());
let side_a = router.add_side_packed("A", tx_a);
let side_b = router.add_side_packed("B", tx_b);
let side_c = router.add_side_packed("C", tx_c);
router.set_route(None, side_b, false)?; // local TX does not go to B
router.set_route(Some(side_a), side_b, true)?; // allow A -> B
router.set_route(Some(side_b), side_a, false)?;// block B -> A
router.set_typed_route(None, DataType::named("GPS_DATA"), side_c, true)?;
router.set_side_egress_enabled(side_c, false)?; // ingress onlyWith the discovery feature enabled, routers and relays learn which endpoints are reachable
through which sides.
- known paths are used directly
- unknown user-data paths are not flooded by fallback; discovery/control traffic still bootstraps route learning
- measured slow links receive minimal discovery pings most of the time, with full refreshes spaced out to preserve bandwidth
- link-local-only endpoints stay on sides marked
link_local_enabled - local plus source-side route rules still gate what discovery is allowed to use
- discovery also carries a transitive router graph, so exported topology keeps sender ownership and router-to-router connections instead of only flattening reachability per side
When discovery reports multiple candidate paths:
- normal traffic defaults to adaptive load balancing based on observed transmit bandwidth
- reliable traffic still fans out across all discovered candidates so one weak path does not hide a successful delivery on another path
-
set_source_route_mode(...),set_route_weight(...), andset_route_priority(...)can still override the defaults
Packets already in flight also carry a compact internal wire contract: a frozen destination holder set and enough payload-shape metadata to stay decodable while schema and topology updates are still propagating. Applications do not build that contract manually; routers and relays attach and honor it automatically.
Reliable delivery has two switches:
- the schema type itself must be marked reliable
- the router/relay side must opt in with
reliable_enabled: true
That side option is per hop, not global. It controls what happens between the router/relay and that side's TX callback.
use sedsnet::router::{Router, RouterConfig, RouterSideOptions};
let router = Router::new(RouterConfig::default());
router.add_side_packed_with_options(
"RADIO",
tx,
RouterSideOptions {
reliable_enabled: true,
link_local_enabled: false,
..RouterSideOptions::default()
},
);If the underlying transport is already reliable, disable the router-level reliable layer with
RouterConfig::with_reliable_enabled(false).
What reliable_enabled means on a side:
-
reliable_enabled: trueon a packed side wraps reliable schema traffic in the router/relay's hop-level reliable framing for that side only - that hop-level framing adds sequence numbers, ACKs, packet requests, and retransmits
-
reliable_enabled: falsesends the application packet once on that side without the router's hop-level reliable wrapper - packet-output sides (
add_side_packet*) receive decodedPacketvalues, so they cannot carry the packed hop-level reliable wrapper even ifreliable_enabledis set
For routers specifically:
- hop-level side reliability is separate from the source router's end-to-end reliable tracking
- a reliable packet can still be tracked end-to-end across the network even if one specific egress side is configured without hop-level reliability
- when discovery reports multiple candidate holders, reliable traffic still fans out across all of them unless you explicitly restrict routes
As of 3.11.0, reliability has two layers:
- per-link reliable sequencing, ACKs, packet requests, and retransmits
- end-to-end verification from the source router to every currently discovered destination holder
The end-to-end path works like this:
- the source router tracks reliable packets it originated
- when a destination router delivers a reliable packet to a local handler, it emits an end-to-end acknowledgement tagged with its identity
- routers and relays learn the return path from the reliable packet’s ingress side and route that acknowledgement only where it needs to go
- the source keeps the packet in flight until all currently discovered holders have acknowledged
- if one end-to-end acknowledgement is lost, the source retransmits only toward the holders that are still outstanding until they respond or the retry limit is reached
- if discovery later expires one holder, the source removes that holder from the pending set and finishes once the remaining discovered holders are satisfied
That means reliable delivery is now verified at the application-destination boundary, not just per hop, while still keeping reliable send non-blocking for newer packets on the same side/type lane.
For ordered reliable links, a receiver that gets packets after a gap buffers those later packets, emits partial ACKs for them, and requests the missing sequence. Partial ACKs stop timeout-based retransmits for packets the receiver already has, but explicit packet requests can still replay them. When the missing sequence arrives, the buffered packets are dispatched immediately in order.
Common receive APIs:
rx_packed(bytes)rx_packed_queue(bytes)rx(packet)rx_queue(packet)
Meaning of the variants:
-
rx_*processes immediately in the current call -
rx_*_queueonly enqueues work for a laterprocess_*/periodiccall -
*_from_side(..., side_id)tags the ingress with an explicit side id for route/discovery logic - the non-
from_sidevariants treat the input as locally-originated rather than arriving from a registered side
If an immediate router receive/transmit API is called from inside a side TX callback, the router
now defers that work onto its queue instead of recursively re-entering forwarding on the same
stack. Internal SEDSNET_DISCOVERY and SEDSNET_TIME_SYNC traffic stays router-owned;
applications should use the public discovery/time-sync APIs instead of constructing those packets
directly.
Use side-aware ingress only when you need to override the ingress side explicitly:
rx_packed_from_side(bytes, side_id)rx_from_side(packet, side_id)
The common maintenance calls are:
process_rx_queue()process_tx_queue()process_all_queues()periodic(timeout_ms)-
periodic_no_timesync(timeout_ms)whentimesyncis enabled but you want to skip it for one loop
What each one does:
-
process_rx_queue()drains queued receives only -
process_tx_queue()drains queued transmits only -
process_all_queues()drains both queues without a time budget -
process_*_with_timeout(timeout_ms)runs the same phase with a millisecond budget;0means drain fully -
periodic(timeout_ms)is the normal main-loop entry point because it also polls built-in discovery and, when enabled, time sync before draining queues
For relays, nested process_tx_queue* / process_all_queues* calls made from inside a side TX
callback are intentionally turned into no-ops so a side callback cannot recursively drive relay TX
on the same stack.
Router and relay queue-backed state shares the compile-time MAX_QUEUE_BUDGET dynamically.
That includes RX work, TX work, recent packet IDs, reliable buffers/replay state, and discovery
topology. Recent packet ID caches preallocate their final storage and reserve that byte cost
immediately. If the remaining budget is exhausted, older queued state is evicted; discovery
topology eviction emits a warning in std builds.
Use router.export_memory_layout_json() or relay.export_memory_layout_json() when profiling a
running node. The JSON reports shared allocated/used bytes plus per-area used/allocated bytes for
RX, ISR RX, TX, replay queues, reliable buffers, discovery, schema, and the network-variable cache.
Use router.export_runtime_stats() / relay.export_runtime_stats() or the matching C/Python
exports when profiling constrained links. Each side reports whether header-template compaction is
enabled, its effective side-transport profile, fixed-frame size, the compact-header target,
full/compact/chunk side-transport frame counts, emitted bytes, bytes saved versus canonical frames,
timestamp-delta and unchanged-timestamp compact frame counts, and the observed compact follow-up
overhead. Small-packet transport defaults to a 40-byte IPv6-like overhead target; call
with_ipv4_like_compact_header_target() on the side options when a stable tiny telemetry stream
should be held to a 20-byte IPv4-like target with unchanged compact timestamps omitted. Python
exposes the same profile selection with add_side_packed_profile(..., profile="ipv4_like"); C
callers use seds_router_add_side_packed_profile(...) or
seds_relay_add_side_packed_profile(...) with SEDS_SIDE_TRANSPORT_PROFILE_IPV4_LIKE.
For mixed links, keep absolute/delta timestamps for most traffic and omit unchanged timestamps only for selected data types:
let opts = RouterSideOptions::default()
.with_ipv6_like_compact_header_target()
.with_omitted_unchanged_compact_timestamps_for_type(DataType::named("GPS_DATA"));Discovery assigns and advertises compact node addresses and unique hostnames. Configure identity on the router config:
let router = Router::new(
RouterConfig::default()
.with_hostname("http-service")
.with_static_address(0x1020_3040),
);Use with_dynamic_address(), with_requested_address(address), or
with_static_address(address). When segmented networks reunite, routers deconflict duplicate
addresses and hostnames deterministically. Static addresses are preserved first; if two static
nodes collide, the older identity keeps the address and newer identities move. Register
on_address_change(...) to be notified when the local address or hostname changes.
P2P service traffic is separate from endpoint broadcast telemetry. A service binds a port and receives opaque bytes:
router.bind_p2p_port(80, |msg| {
assert_eq!(msg.destination_port, 80);
let http_request = msg.payload;
Ok(())
})?;Clients send by hostname so address changes do not break them, or by address when an application needs explicit address targeting:
client.send_p2p_to_hostname(
"http-service",
80,
49152,
b"GET /status HTTP/1.1\r\nHost: http-service\r\n\r\n",
)?;
client.send_p2p_to_address(0x1020_3040, 80, 49152, b"GET / HTTP/1.1\r\n\r\n")?;The service payload can carry protocols such as HTTP over SEDSnet links; SEDSnet supplies the addressing, routing, reliability, and discovery layer instead of IP.
For protocols that want a connection lifecycle instead of standalone datagrams, bind a stream port and open a stream:
router.bind_p2p_stream_port(8080, |event| {
match event.kind {
sedsnet::router::P2pStreamEventKind::Accepted => {
// Store event.stream_id if this service wants to write a response.
}
sedsnet::router::P2pStreamEventKind::Data => {
let bytes = event.payload;
}
sedsnet::router::P2pStreamEventKind::Closed
| sedsnet::router::P2pStreamEventKind::Reset => {}
sedsnet::router::P2pStreamEventKind::Connected => {}
}
Ok(())
})?;
let stream = client.open_p2p_stream_to_hostname("http-service", 8080, 49152)?;
client.send_p2p_stream(stream, b"GET /stream HTTP/1.1\r\n\r\n")?;
client.close_p2p_stream(stream)?;Stream frames are carried inside SEDSNET_P2P_MESSAGE, so they use the same discovery routing,
compact addresses, target-sender contracts, and ordered reliable control path as P2P datagrams.
With discovery enabled, export_topology() returns the router's current learned view.
-
topology.routerscontains the top-level discovered router graph - each router entry includes the sender ID, owned endpoints, owned time-sync source IDs, and connected router sender IDs
-
topology.linksis a deduplicated board-to-board edge list (source,target) for direct graph rendering - exported JSON/Python dictionaries use
reachable_endpointsandadvertised_endpointsfor schema-advertised names, withreachable_endpoint_idsandadvertised_endpoint_idsavailable when code needs stable numeric validation - SEDSnet-owned control endpoints (
SEDSNET_TIME_SYNC,SEDSNET_DISCOVERY,SEDSNET_ERROR) are filtered out of user endpoint reachability fields - each side route also includes
announcers, so you can see which upstream router advertised the exported topology
Use router.client_stats("BOARD_ID") or relay.client_stats("BOARD_ID") to inspect one
discovered client. The snapshot includes connected/disconnected state, side IDs and side names,
last-seen/age timing, named reachable endpoints, reachable time-sync sources, and packet/byte
counters aggregated from the side(s) currently reaching that client.
Use router.announce_leave() or relay.announce_leave() before a planned shutdown or disconnect.
That queues a SEDSNET_DISCOVERY_LEAVE control packet so peers can prune topology immediately
instead of waiting for the discovery TTL. The C ABI also attempts this on router/relay free, but an
explicit leave is preferred when shutdown order matters.
Do not register user handlers for:
DataEndpoint::Discovery-
DataEndpoint::TimeSyncwhen thetimesyncfeature is enabled
Those endpoints are owned by the router’s built-in control traffic.
When the timesync feature is enabled, the router maintains an internal network clock and handles
SEDSNET_TIME_SYNC traffic internally.
For ordinary loops, prefer periodic(timeout_ms) so time sync, discovery, and queue draining run
together.
See Time-Sync for the protocol details.