Usage Python - Rylan-Meilutis/SEDSnet GitHub Wiki
Python Usage
Python bindings are built with pyo3 and maturin. The module name is sedsnet.
Build and install
Recommended in this repo:
./build.py python
Direct maturin is also supported:
maturin develop
Minimal example
import sedsnet as seds
def tx(bytes_buf):
pass
def on_packet(pkt):
print(pkt)
schema = b"""
{
"endpoints": [
{"rust": "SdCard", "name": "SD_CARD", "description": "Local storage endpoint"},
{"rust": "Radio", "name": "RADIO", "description": "External radio link"}
],
"types": [
{
"rust": "GpsData",
"name": "GPS_DATA",
"description": "Three f32 GPS values",
"priority": 80,
"class": "Data",
"element": {"kind": "Static", "data_type": "Float32", "count": 3},
"endpoints": ["Radio", "SdCard"]
}
]
}
"""
seds.register_schema_json_bytes(schema)
sd_card = seds.endpoint_info_by_name("SD_CARD")["id"]
gps_data = seds.data_type_info_by_name("GPS_DATA")["id"]
router = seds.Router(
handlers=[(sd_card, on_packet, None)],
)
router.add_side_packed("RADIO", tx, reliable_enabled=True)
router.log_f32(gps_data, [1.0, 2.0, 3.0])
router.process_all_queues()
If you need a custom monotonic source for tests or simulation, pass now_ms=....
Runtime configuration
Prebuilt wheels are not locked to their packaged defaults. Configure the active node at runtime:
import sedsnet as seds
seds.set_runtime_device_identifier("GROUND_STATION")
seds.set_runtime_tuning_config(
payload_compress_threshold=24,
static_string_length=512,
static_hex_length=512,
string_precision=6,
max_handler_retries=4,
reliable_retransmit_ms=300,
reliable_max_retries=10,
reliable_max_pending=96,
reliable_max_return_routes=96,
reliable_max_end_to_end_pending=96,
reliable_max_end_to_end_ack_cache=256,
)
router = seds.Router(
hostname="FC26_MAIN",
address_mode=2, # 0=dynamic, 1=requested, 2=static
requested_address=0x10203040,
timesync_enabled=True,
timesync_role=2, # 0=consumer, 1=source, 2=auto
max_queue_budget=65536,
max_recent_rx_ids=256,
)
router.configure_timesync(enabled=True, role=1, priority=10)
router.configure_address(address_mode=1, requested_address=0x10203041)
runtime_tuning_config() returns the active process-wide values. Router/relay memory limits are per
instance, while identity/address and time-sync settings are per router except for the process-wide
default device identifier used by packet helper constructors.
Runtime schema
Python exposes the same runtime registry as Rust and C:
register_endpoint(...)andregister_data_type(...)add explicit entriesregister_schema_json_file(...)andregister_schema_json_bytes(...)seed entries from JSONendpoint_info_by_name(...)anddata_type_info_by_name(...)return IDs and metadataremove_endpoint_by_name(...)andremove_data_type_by_name(...)remove user entries
The DataType and DataEndpoint enums only contain built-in control IDs. Application schema IDs
should be looked up by string name after registration or JSON seeding.
Network Variables and E2E Policy
Network variables cache the latest value packet for a data type. User code uses a setter and getter; the getter returns the cached value and internally requests a refresh when the value has never been seen or is stale. No separate endpoint registration is needed for the network-variable machinery. Routers with write permission are authoritative and can answer refresh requests. Read-only replicas forward requests toward a writer instead of replying with possibly stale persisted state. Updates from one writer use its sender-local timestamp to reject older delivery. Equal-timestamp updates retain arrival order, and another writer can replace a local restored seed; nonce and packet ID are deduplication identities rather than revisions.
import sedsnet as seds
RADIO = 101
FLIGHT_STATE = 3100
seds.register_endpoint(RADIO, "RADIO")
seds.register_data_type(
FLIGHT_STATE,
"FLIGHT_STATE",
True,
1,
0, # UInt8
0, # Data
[RADIO],
priority=90,
e2e_encryption=2, # RequireOn
)
router = seds.Router(e2e_mode=1, e2e_key_id=7) # RequiredOnly
router.enable_network_variable(FLIGHT_STATE, True, True)
router.on_network_variable_update(FLIGHT_STATE, on_flight_state_update)
router.seed_managed_variable(initial_packet)
router.set_network_variable(packet)
router.request_managed_variable(FLIGHT_STATE)
cached = router.get_network_variable(FLIGHT_STATE, 1000)
router.process_all_queues()
If the router lacks read or write permission, getters/setters raise the normal SEDSnet Python
exception for PermissionDenied. Peers answer denied refreshes with a telemetry error packet. The
update callback runs only for inbound updates and refresh replies that change the local cache; local
setters/seeds update the cache without firing that callback.
E2E policy values are 0=PreferOff, 1=PreferOn, and 2=RequireOn. Router E2E modes are
0=Disabled, 1=RequiredOnly, 2=Preferred, and 3=ForceAll. The constructor default
e2e_mode=255 means "build default": Preferred for normal Python builds because cryptography
is part of the python feature. Custom extensions built without cryptography default to Disabled
and reject RequireOn traffic instead of silently downgrading it.
Key exchange is board/application owned. Run your quantum-resistant asynchronous exchange when
discovery learns a peer, derive symmetric traffic keys, and have the cryptography provider select those keys by
e2e_key_id. For three boards advertising the same endpoint, use an endpoint/group traffic key so
all intended boards can open the same message; authenticated payloads reject header or ciphertext
changes before handlers see data.
Routing model
There is no Python RouterMode anymore.
Routernow uses the same rule-driven forwarding model as the Rust API- routers and relays both default to a full forwarding mesh across eligible sides
- runtime route rules are how you restrict forwarding
Useful controls:
set_side_ingress_enabled(...)set_side_egress_enabled(...)set_route(...)clear_route(...)set_typed_route(...)clear_typed_route(...)set_source_route_mode(...)set_route_weight(...)set_route_priority(...)
Use None for src_side_id when controlling locally-originated traffic.
Discovery and reliability
With discovery enabled:
- routers and relays learn endpoint reachability per side
- discovery also propagates a transitive router graph, not just flattened endpoint sets
- normal traffic defaults to adaptive discovered-path load balancing
- reliable traffic still fans out across all known discovered candidates
- topology changes send an immediate address update and a bounded three-send added/changed/removed node delta, while full graph snapshots are reserved for bootstrap and infrequent repair
- compact-header dictionaries are resynchronized on topology changes, making the first subsequent data frame self-describing for newly joined or restarted peers
- enabling or disabling a network variable schedules an immediate compact ownership update, including on slow links
export_topology() is available on both Router and Relay.
- it returns a Python
dict - the top-level
routerskey lists each discovered router, the endpoint names/source IDs it owns, and its connected router sender IDs - the top-level
linkskey lists deduplicated board-to-board graph edges as{source, target} - graph-facing endpoint fields such as
reachable_endpointsandadvertised_endpointscontain schema-advertised names; companion fields such asreachable_endpoint_idsandadvertised_endpoint_idspreserve the numeric IDs - SEDSnet-owned control endpoints (
SEDSNET_TIME_SYNC,SEDSNET_DISCOVERY,SEDSNET_ERROR) are not included in user endpoint reachability fields - each route entry also includes
announcersso you can see which upstream router advertised each piece of topology
Router.client_stats("BOARD_ID") and Relay.client_stats("BOARD_ID") return a dictionary for one
discovered client or None if that sender is unknown. The dictionary includes connected state,
side IDs/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.
Call announce_leave() before a planned shutdown or disconnect so peers receive
SEDSNET_DISCOVERY_LEAVE and remove that sender from topology immediately instead of waiting for
the discovery TTL.
Reliable delivery is enabled on a per-side basis with reliable_enabled=True for packed
sides. Packets already in flight also carry a compact internal wire contract so topology or
runtime-schema changes do not redirect them to the wrong holder or make them undecodable mid-flight.
Applications do not construct that contract directly; routers and relays manage it internally.
As of 3.11.0, reliable delivery is end-to-end verified:
- the source router tracks reliable packets it originated
- each discovered destination holder emits an end-to-end acknowledgement after local delivery
- routers and relays route that acknowledgement back only along the learned return path
- unrelated sides do not receive those end-to-end acknowledgements
- the source keeps retransmitting only toward holders that are still missing an acknowledgement
- if a discovered holder ages out of topology, the source removes it from the pending holder set
- newer reliable packets on the same side still do not block while those end-to-end ACKs are pending
For ordered reliable links, later packets that arrive after a missing sequence are buffered and partial-ACKed. Partial ACKs suppress timeout retransmit for packets already received, but explicit packet requests can still replay them. The buffered packets are dispatched as soon as the missing sequence arrives.
Queue processing
Useful maintenance calls:
process_rx_queue()process_tx_queue()process_all_queues()periodic(timeout_ms)periodic_no_timesync(timeout_ms)when time sync is enabled but should be skipped for one loop
Router and relay queue-backed state shares one dynamic budget. Prebuilt Python wheels use the packaged defaults unless you pass constructor overrides:
router = sedsnet.Router(max_queue_budget=262144, max_recent_rx_ids=512)
relay = sedsnet.Relay(max_queue_budget=131072, starting_queue_size=4096)
RX work, TX work, recent packet IDs, reliable buffers/replay state, discovery topology, and runtime
schema registry memory all count against the active budget. Recent packet ID caches preallocate
their final storage and reserve that byte cost immediately. Discovery topology eviction emits a
warning in std builds.
Use export_memory_layout_json() on a router or relay to profile queue pressure. The JSON includes
shared allocated/used bytes plus per-area queue, reliable-buffer, schema, discovery, and
network-variable-cache breakdowns.
Use add_side_packed_profile(...) to select a compact side-wire profile from Python:
router.add_side_packed_profile(
"RADIO",
tx,
reliable_enabled=True,
profile="ipv4_like",
max_frame_bytes=0,
max_side_transport_templates=64,
)
Profiles are canonical, template, ipv6_like, and ipv4_like. A
compact_header_target_bytes value of 0 uses the IPv6-like 40-byte or IPv4-like 20-byte default
target for the selected profile. Both profiles preserve absolute timestamps:
delta/omission could corrupt packet identity after frame loss. The same method
is available on Relay.
On Router, compact template state is self-healing on lossy links. An unknown compact frame is
dropped without raising a transport error, and the sender refreshes an active template in full
after every eight compact uses. Router transmit and receive also share the same deterministic
eviction rule when max_side_transport_templates is reached. Relay exposes the same profile
configuration but does not currently perform this automatic recovery. Discovery automatically
limits a router sender to the smallest template capacity advertised by a live peer on the side, so
Python hosts interoperate with smaller embedded dictionaries without a duplicate hard-coded limit.
P2P Service Ports
Routers expose discovery-backed service ports for byte protocols that should run over SEDSnet instead of IP:
router.bind_p2p_port(80, lambda meta, payload: handle_http(payload))
client.send_p2p_to_hostname("http-service", 80, 49152, b"GET / HTTP/1.1\r\n\r\n")
client.send_p2p_to_address(0x10203040, 80, 49152, b"GET / HTTP/1.1\r\n\r\n")
router.current_address returns the current compact address, and
router.resolve_hostname("name") returns discovered address metadata when known.
For byte protocols that need a session lifecycle, bind a stream port and open a stream:
router.bind_p2p_stream_port(8080, lambda meta, payload: handle_stream(meta, payload))
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)
The stream callback receives a metadata dictionary with kind, stream_id, peer_stream_id,
sequence, peer_hostname, peer_address, local_port, and peer_port.
Time sync
When built with timesync, Router keeps an internal network clock and handles SEDSNET_TIME_SYNC
traffic internally.
Construct Router(..., timesync_enabled=False) if the extension was built with timesync but you
do not want time sync for a particular instance. SEDSNET_TIME_SYNC, SEDSNET_DISCOVERY, and
SEDSNET_ERROR remain reserved internal endpoints; do not register handlers for them or try to emit
those packets manually.
See Time-Sync for protocol details.