SEDSnet 4.0.0

A memory safe, no_std-capable networking stack with routing, discovery, reliability, and Rust/C/Python bindings.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
# SEDSnet

A Rust networking stack with compact packets, runtime schema, discovery, routing, reliability,
managed state sync, P2P service ports/streams, optional E2E payload cryptography, and C/Python
bindings for distributed embedded and host systems.

Crate API docs are published on [docs.rs](https://docs.rs/SEDSnet). The implementation-level wiki,
including wire/discovery formats and binding guides, is mirrored in
[docs/wiki](./docs/wiki/Home.md) and on the project wiki.

---

## Authors

- [@Rylan-Meilutis]https://github.com/rylan-meilutis (Original Author, Maintainer, and co-creator of the protocol)
- [@origami-yoda]https://github.com/origami-yoda (Co-creator of the protocol and co-author of the original C++
  implementation)

---

## About

This library started out as a Rust rewrite of the earlier SEDS telemetry C++ implementation.

After the initial rewrite, many improvements were made to the rust implementation including better safety, easier
extension, and improved performance.
This caused the C++ implementation to be rewritten to keep feature parity with the rust version.
After about of month of this, we decided that we were no longer going to use the C++ version, and thus the project was
archived and is no longer being maintained.
With the Rust version being the sole implementation, we have continued to improve it and add new features like python
bindings, packet compression, and a bitmap for endpoints to further reduce packet size.
This library is now being used in multiple projects including embedded code on the rocket and on the rust based ground
station. SEDSnet is now capable of acting as a new network, passing telemetry data to endpoints across hardware
and software networks (uart, can ethernet, etc.) and across differing platforms and protocols (tcp, udp, etc.).

---

## Overview

This library provides a safe and efficient way to handle telemetry data including serializing, routing, and converting
into strings for logging. The purpose of this library is to provide an easy and consistent way to create and transport
any data to any destination without needing to implement the core routing on every platform, the library handles the
creation and routing of the data while allowing for easy extension to support new data types and destinations. The user
of the library is responsible for implementing 2 core functions and one function per local data endpoint.
The core functions are as follows:

- A function to send raw bytes to the all other nodes (e.g. UART, SPI, I2C, CAN, etc.)
- A function to receive raw bytes from all other nodes and passes the packet to the router
  (e.g. UART, SPI, I2C, CAN, etc.)
- A function to handle local data endpoints (e.g. logging to console, writing to file, sending over radio, etc.)
  (Note: each local endpoint needs its own function)

SEDSnet also provides helpers to convert the telemetry data into strings for logging purposes.
The library also handles packing telemetry packets to wire frames and unpacking received frames.

SEDSnet is platform-agnostic and can be used on any platform that supports Rust. The library is primarily designed
to be used in embedded systems and used by a C program, but can also be used in desktop applications and other rust
codebases.

SEDSnet also supports python bindings via pyo3. to use you need maturin installed to build the python package.

With the optional `discovery` feature, routers and relays can exchange built-in discovery packets, learn which
endpoints are reachable through which sides, adapt the announce rate as the topology changes, and export a live topology
snapshot for inspection. When `timesync` is also enabled, discovery can advertise concrete time source sender IDs so
`SEDSNET_TIME_SYNC` requests prefer exact source paths instead of generic endpoint flooding. Once discovery topology exists,
unknown user-data routes are not blindly flooded across low-bandwidth links; discovery/control traffic still propagates,
and explicit route policy can intentionally select a side. `SEDSNET_DISCOVERY` and `SEDSNET_TIME_SYNC` are reserved internal router
endpoints: applications can use the discovery and time-sync APIs, but must not register local endpoint handlers for
those endpoints or try to override their built-in handling.

Discovery and time-sync maintenance also throttle themselves across measured slow links. Link-probe
or driver timing samples mark constrained sides, after which routers and relays send mostly minimal
reachability pings across those sides and reserve full schema/topology/time-source refreshes for a
much slower cadence. Router-managed time sync keeps its normal cadence on fast sides while each
measured slow side independently receives sparse time-sync traffic, keeping asymmetric or
time-sliced radio links available for user payloads.

Discovery also assigns and advertises compact node addresses and hostnames for point-to-point
service traffic. A router can bind a SEDSnet service port and receive opaque byte payloads targeted
by hostname or address, or open a P2P stream with connect/accept/data/close events. Protocols such
as HTTP can be carried over SEDSnet links without IP being the underlying transport. Broadcast
endpoint delivery remains available for telemetry data types; P2P service frames use discovery
identity and ports for board-to-board traffic.

Queue memory is bounded by the compile-time `MAX_QUEUE_BUDGET`. Router and relay internals share that budget
dynamically across RX work, TX work, reliable replay/out-of-order buffers, recent packet ID tracking, and learned
discovery topology state. The recent packet ID cache preallocates its final storage because it is expected to fill
during normal operation, so its reserved bytes come out of the shared budget immediately. If one active queue area is
idle, another can use more of the remaining budget; if several areas fill at once, older queued state is evicted so
total queue-owned memory stays bounded.

Reliable delivery uses internal ACK/request control packets. Ordered reliable receivers buffer out-of-order packets,
partial-ACK packets that arrived after a gap, request the missing sequence, and then release the buffered run as soon as
the gap is filled. A partial ACK suppresses timeout retransmission for that exact packet, but an explicit packet request
can still retransmit it later.

Routers can also cache selected data types as managed network variables. A board that restarts can request the current
cached value and receive it through the normal endpoint handler path instead of waiting for the next publisher update.
For sensitive state or commands, the default `cryptography` feature lets data types prefer or require end-to-end payload
cryptography while the application supplies a C provider, Rust provider, OS/hardware crypto wrapper, or registered software key.

When topology or schema changes are propagating, packets already on the wire now carry a compact frozen delivery and
decode contract. New packets immediately use the latest schema-default endpoint view, while in-flight packets continue to
route only to their original intended holders and remain decodable against the shape they were packed with. The
contract is encoded compactly with bitmap-oriented metadata and sender hashes so header growth stays small.

Packed packets use compact varint fields, schema-derived endpoints, a compact source address instead of a repeated
sender hostname, and optional per-side header templates. Hostnames and endpoint holders are learned through discovery
and network configuration; the packet header carries only the source address needed for identity/routing. Per-side
templates can then replace repeated header fields with a compact template ID on small-packet transports, keeping the
header-to-payload ratio reasonable for small payloads such as three floats or a few `u8` values.

---

## Recent changelog milestones

## Version 4.0.0 highlights

- User telemetry schema is now runtime-only. `build.rs` no longer generates Rust enum variants or
  binding constants from `telemetry_config.json`.
- Endpoints and data types can be registered, looked up by name, exported, synced through
  discovery, and removed at runtime.
- Rust code can use readable runtime references such as `DataEndpoint::named("RADIO")` and
  `DataType::named("GPS_DATA")` instead of raw numeric IDs.
- C and Python expose matching schema register/info/info-by-name/remove APIs.
- Managed network variables let restarted boards request the latest cached state through normal
  endpoint handlers, and update callbacks can run when an inbound network update changes the cache.
- Discovery-backed P2P service ports support datagrams and lightweight stream sessions with
  connect/accept/data/close/reset events for byte protocols that need board-to-board sessions.
- End-to-end payload cryptography is enabled by default and can be preferred or required per data type;
  routers choose whether to encrypt required, preferred, or all user data.
- Crypto providers can be supplied as C callbacks, Rust providers, or registered software fallback
  keys, and compact managed credentials support master-root deployments without user-managed cert
  files.
- Fixed-size packed sides transparently split/reassemble packets for CAN, I2C, and fixed-frame
  radios, while link-probe samples seed adaptive routing for asymmetric or time-sliced links.
- Discovery-aware forwarding avoids blind unknown-route user-data flooding once topology exists,
  protecting low-bandwidth sides unless explicit route policy selects them.
- Discovery and time-sync control traffic is dynamically throttled per measured slow link, using
  minimal reachability pings between infrequent full topology/schema refreshes.
- Routers and relays export topology as a board graph with named endpoint fields, side names,
  deduplicated `links`, and filtered SEDSnet control endpoints. They also expose memory-layout and
  per-client stats snapshots for profiling and diagnostics.
- Routers and relays announce `SEDSNET_DISCOVERY_LEAVE` during explicit leave/shutdown so peers can
  prune topology immediately instead of waiting for expiry.
- Optional JSON config is applied at runtime through env/path/bytes APIs. The repo does not carry a
  default user schema. Embedded users that want compiled-in seed bytes must provide their own local
  `telemetry_config.json` before building.
- Schema registry memory counts against the same shared `MAX_QUEUE_BUDGET` as other router/relay
  queue-backed state.
- Optional C and C++ convenience wrappers can be enabled from CMake, while raw-ABI users can keep
  the static runtime-schema header only.
- Full changelog: [CHANGELOG.md]./CHANGELOG.md

## Version 3.12.0 highlights

- Router and relay queue-backed state now shares one dynamic `MAX_QUEUE_BUDGET` instead of using
  isolated per-queue caps.
- `MAX_QUEUE_SIZE` has been renamed to `MAX_QUEUE_BUDGET`; the old environment name remains
  accepted as a legacy alias.
- Recent packet ID caches, reliable replay/out-of-order buffers, and discovery topology state now
  count against the same shared budget, with topology eviction warnings in `std` builds.
- Ordered reliable receive paths now partial-ACK out-of-order packets to reduce timeout
  retransmission traffic while still allowing explicit packet requests.
- Router and relay side-TX contention is now retried as transient backpressure instead of surfacing
  intermittent handler failures.
- Full changelog: [CHANGELOG.md]./CHANGELOG.md

## Version 3.11.1 highlights

- Discovery now propagates a full router graph with `SEDSNET_DISCOVERY_TOPOLOGY`, so routers and relays
  keep track of which sender IDs own which endpoints and how remote routers connect to each other.
- `export_topology()` now includes router-level topology plus per-side announcer detail instead of
  only aggregated reachable endpoint lists.
- Topology export is now available to clients across all supported surfaces:
  Rust `export_topology()`, Python `Router.export_topology()` / `Relay.export_topology()`, and C
  JSON exports via `seds_router_export_topology*` / `seds_relay_export_topology*`.
- Full changelog: [CHANGELOG.md]./CHANGELOG.md

## Version 3.11.0 highlights

- Removed `RouterMode` from the active router model. Routers now use the same runtime routing-rule
  model as relays, and with no explicit route rules they default to a full forwarding mesh.
- Discovery-driven multi-path routing now defaults to adaptive load balancing for normal traffic,
  while reliable discovered traffic still fans out across all known candidate paths.
- Reliable delivery is now end-to-end verified in addition to the existing per-link ACK/retransmit
  layer. Destination routers emit directed end-to-end acknowledgements, and discovery-informed
  return-path learning routes those ACKs only toward the source instead of flooding them.
- When a discovered destination holder disappears from topology, the source retires that holder
  from the in-flight obligation set instead of replaying forever toward a vanished board.
- Relays also prune their learned holder-ACK state against discovery expiry so stale confirmations
  do not keep affecting later discovered routing choices.
- Reliable streams still stay non-blocking while those end-to-end acknowledgements are outstanding.
- Added expanded testing documentation covering unit tests, Rust system tests, C system tests,
  local coverage reporting, and the new end-to-end reliability regression tests.
- Full changelog: [CHANGELOG.md]./CHANGELOG.md

## Version 3.10.0 highlights

- Reliable delivery in both `Router` and `Relay` now uses built-in internal
  `RELIABLE_ACK` and `RELIABLE_PACKET_REQUEST` packet types instead of wire-only ACK-only frames.
- Reliable streams no longer block on one missing reliable packet. Ordered gaps are requested
  explicitly, out-of-order packets are buffered, and retransmits are requeued with elevated
  priority.
- This improves recovery on asymmetric links and multi-destination fanout where different boards
  progress at different rates.
- Full changelog: [CHANGELOG.md]./CHANGELOG.md

## Version 3.9.1 highlights

- Reserved the built-in `SEDSNET_DISCOVERY` and `SEDSNET_TIME_SYNC` endpoints for router-owned control traffic.
- User handlers can no longer shadow internal discovery or time-sync behavior through Rust or C
  configuration APIs.
- Queue timeout handling was tightened so TX/RX work shares nonzero budgets more predictably.
- Full changelog: [CHANGELOG.md]./CHANGELOG.md

## Version 3.0.0 highlights

- Introduced internal router-side tracking so most applications can use the plain RX APIs and only
  opt into side-aware ingress when they actually need it.
- Added TCP-like reliable delivery for schema types marked `reliable` or `reliable_mode`, with
  ACKs, retransmits, and optional ordering.
- This established the modern router/reliability model that later releases expanded.
- Full changelog: [v2.4.0...v3.0.0]https://github.com/Rylan-Meilutis/sedsnet/compare/v2.4.0...v3.0.0

## Version 1.0.0 highlights

- First stable release with routing, packet packing, and packet creation across Rust, C, and Python.
- Marked the API as stable and established the base wire-format and packet model the later versions
  built on.

---

## Building

To build the library in a C project, just include the library as a submodule or subtree and link it in your
cmakelists.txt as shown below.
For other build systems, you can build the library as a static or dynamic library using cargo and link it to your
project.

Building with python bindings can be done with the build script on posix systems:

```
./build.py release maturin-develop
```

When building in an embedded environment the library will compile to a static library that can be linked to your C code.
this library takes up about 100kb of flash and does require heap allocation to be available through either freertos, or
by creating providers that expose pvPortMalloc and vPortFree.


## Embedded hooks

For embedded integrations, treat the router or relay as a single owner context. The normal application loop should call
`periodic(...)` and feed ingress bytes into the queued RX APIs.

Rules that matter in practice:

- Do not call router, relay, or packet-to-string APIs from an ISR. Queue the bytes and hand them to a task or worker.
- If you wrap router calls with a lock, that lock must be recursive-safe because side TX callbacks can trigger deferred
  queue work in the same logical call chain.
- `SEDSNET_DISCOVERY` and `SEDSNET_TIME_SYNC` are reserved internal endpoints. Do not register local handlers for them and do not emit
  those packets manually from application code. Use the time-sync and discovery APIs instead.
- When the `timesync` feature is enabled, `periodic(...)` is the expected maintenance entry point. Only use
  `poll_timesync(...)` / `poll_discovery(...)` directly when you intentionally need manual phase control.

### build.py usage

```
./build.py [OPTIONS]

Options:
  release                 Build in release mode.
  check                   Run cargo clippy with -D warnings for default, python, and embedded builds.
  test                    Run the clippy checks, nextest/cargo tests, a stable Criterion benchmark smoke pass, and also validate python plus embedded builds when the cross C toolchain exists.
  embedded                Build for the embedded target (enables embedded feature).
  python                  Build with Python bindings (enables python feature).
  timesync                Build with time sync helpers (enables timesync feature).
  maturin-build           Run maturin build with the .pyi .gitignore hack.
  maturin-develop         Run maturin develop with the .pyi .gitignore hack.
  maturin-install         Build wheel and install it with uv pip install.
  target=<triple>         Set Rust compilation target (e.g. target=thumbv7em-none-eabihf).
  device_id=<id>          Set DEVICE_IDENTIFIER env var for the build.
  static_schema_path=<path>      Set SEDSNET_STATIC_SCHEMA_PATH for runtime registry seeding.
  static_ipc_schema_path=<path>  Set SEDSNET_STATIC_IPC_SCHEMA_PATH for a runtime IPC/link-local seed.
  max_queue_budget=<n>    Set MAX_QUEUE_BUDGET for the shared router/relay queue budget.
  max_recent_rx_ids=<n>   Set MAX_RECENT_RX_IDS for the preallocated recent-ID cache.
  max_stack_payload=<n>   Set MAX_STACK_PAYLOAD for define_stack_payload!(env="MAX_STACK_PAYLOAD", ...).
  env:KEY=VALUE           Set arbitrary environment variable(s) for the build (repeatable).
```

Examples:

```
./build.py release
./build.py check
./build.py check release
./build.py embedded release target=thumbv7em-none-eabihf device_id=FC
./build.py python
./build.py test release
./build.py maturin-install max_recent_rx_ids=256 env:MAX_STACK_PAYLOAD=128
```

## Dependencies

- Rust → https://rustup.rs/
- CMake
- A C++ compiler
- A C compiler

## Performance benchmarking

Criterion benchmarks are available through Cargo benches. The current benchmark targets exercise packet construction,
packing, header peeking, unpacking, and router/relay flows that mirror the Rust system-test path under the
default host feature set.

Run:

```bash
cargo bench --bench packet_paths
cargo bench --bench router_system_paths
```

If you want profiler-friendly output while iterating locally:

```bash
cargo bench --bench packet_paths -- --profile-time=5
```

`./build.py test` now starts with the same strict clippy checks as `./build.py check`, then runs:

- `cargo nextest run --features timesync` when `cargo-nextest` is installed, otherwise
  `cargo test --features timesync`, covering the unit tests in `src/tests.rs`, the Rust system tests under
  `tests/rust-system-test/`, and the C integration tests under `tests/c-system-test/`
- `cargo test --doc --features timesync` when nextest is used, since nextest does not run doctests
- a stable Criterion smoke pass for `packet_paths` and `router_system_paths`
- a `cargo build` validation for the `python` feature
- a `cargo build` validation for the `embedded` feature when a matching cross C toolchain is available

The benchmark smoke pass uses Cargo `--profile release`, saves into a dedicated `sedsnet_smoke` baseline, disables
plot generation, and uses a longer timing window plus a wider smoke-test noise threshold so host variance does not print
alternating regression/improvement noise. The C system-test harness waits for all asserted endpoint hits before exiting,
so it does not fail early while one side is still draining forwarded traffic.

Coverage is regression-oriented rather than percentage-gated in CI today. If you want a local line/branch coverage
number, the supported path is:

```bash
cargo llvm-cov --features timesync --workspace --html
```

That writes an HTML report under `target/llvm-cov/html/` when `cargo-llvm-cov` is installed.

More detail on the test layers, what each suite covers, and the intended commands is in
[docs/wiki/Testing.md](./docs/wiki/Testing.md).

## Usage

### Linking from a C/C++ CMake project

```
# Example: building for an embedded target
set(SEDSNET_TARGET "thumbv7m-none-eabi" CACHE STRING "" FORCE)
set(SEDSNET_EMBEDDED_BUILD ON CACHE BOOL "" FORCE)

# Optional: always build the Rust crate in release mode, even if the parent CMake
# configuration is Debug. Useful when your top-level project stays Debug but you
# want an optimized telemetry library.
# set(SEDSNET_FORCE_RELEASE ON CACHE BOOL "" FORCE)

# set the sender name
set(SEDSNET_DEVICE_IDENTIFIER "FC26_MAIN" CACHE STRING "" FORCE)

# optional compile-time env overrides
set(SEDSNET_MAX_STACK_PAYLOAD "256" CACHE STRING "" FORCE)
set(SEDSNET_MAX_QUEUE_BUDGET "65536" CACHE STRING "" FORCE)
set(SEDSNET_MAX_RECENT_RX_IDS "256" CACHE STRING "" FORCE)
set(SEDSNET_ENABLE_CRYPTOGRAPHY ON CACHE BOOL "" FORCE)

# optional wrapper targets; leave both OFF to use only the raw static ABI header
set(SEDSNET_ENABLE_C_WRAPPER ON CACHE BOOL "" FORCE)
set(SEDSNET_ENABLE_CPP_WRAPPER OFF CACHE BOOL "" FORCE)

# Use the provided CMake glue
add_subdirectory(${CMAKE_SOURCE_DIR}/sedsnet sedsnet_build)

# Optional: prefer static linking even on host builds
# set(SEDSNET_PREFER_DYNAMIC OFF CACHE BOOL "" FORCE)

# Link against the imported target
target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE sedsnet::sedsnet)

# Optional convenience wrappers when enabled above:
# target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE sedsnet::c_wrapper)
# target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE sedsnet::cpp_wrapper)
```

Host CMake builds now prefer the shared Rust library when supported. Embedded builds still use the static library.
If you want the Rust crate to use the release profile regardless of the parent CMake config,
set `SEDSNET_FORCE_RELEASE=ON` before `add_subdirectory(...)`. Otherwise the wrapper follows
`CMAKE_BUILD_TYPE` for single-config generators and defaults to debug for Debug builds.

- Configure telemetry schema at runtime. The default build contains only built-in internal
  endpoints/types for telemetry errors, reliable control, discovery, and time sync. Applications
  add user endpoints/types through the runtime APIs, by passing a JSON schema path/bytes to the
  registry, or by letting discovery sync schema entries from peers.
- The raw C ABI is always available through the static runtime-schema header. The C and C++
  convenience wrappers are opt-in CMake targets for projects that want global router/relay helper
  APIs without passing raw handles through every application function.

---

## Setting the device / platform name

Each build of `sedsnet` embeds a **device identifier** which appears in every telemetry packet header.

Rust resolves it using:

```
pub const DEVICE_IDENTIFIER: &str = match option_env!("DEVICE_IDENTIFIER") {
    Some(v) => v,
    None => "TEST_PLATFORM",
};
```

### Set it globally using `.cargo/config.toml` (recommended)

Create:

```
# .cargo/config.toml
[env]
DEVICE_IDENTIFIER = "GROUND_STATION_26"
```

After this, any `cargo build`, `cargo run`, or CI build will embed `"GROUND_STATION_26"` automatically.

No build script changes required.

---

### Setting the name from CMake

```
set(SEDSNET_DEVICE_IDENTIFIER "FC26_MAIN" CACHE STRING "" FORCE)
```

Note: This must be set **before** including the sedsnet CMake as a subdirectory.

Typical examples:

```cmake
# Flight computer firmware
set(SEDSNET_DEVICE_IDENTIFIER "FC26_MAIN" CACHE STRING "" FORCE)
set(SEDSNET_TARGET "thumbv7em-none-eabihf" CACHE STRING "" FORCE)
set(SEDSNET_EMBEDDED_BUILD ON CACHE BOOL "" FORCE)
set(SEDSNET_MAX_STACK_PAYLOAD "256" CACHE STRING "" FORCE)
set(SEDSNET_MAX_QUEUE_BUDGET "65536" CACHE STRING "" FORCE)
set(SEDSNET_MAX_RECENT_RX_IDS "256" CACHE STRING "" FORCE)

# or

# Ground station app
set(SEDSNET_DEVICE_IDENTIFIER "GS26" CACHE STRING "" FORCE)
set(SEDSNET_TARGET "" CACHE STRING "" FORCE)
set(SEDSNET_EMBEDDED_BUILD OFF CACHE BOOL "" FORCE)
set(SEDSNET_MAX_QUEUE_BUDGET "65536" CACHE STRING "" FORCE)
set(SEDSNET_MAX_RECENT_RX_IDS "256" CACHE STRING "" FORCE)
```

### Manually via build.py

```bash
# Host build
./build.py release device_id=GROUND_STATION
# Embedded build
./build.py embedded release target=thumbv7em-none-eabihf device_id=FC
```

---

## Runtime telemetry schema

In v4, user telemetry schema is runtime state. `DataEndpoint` and `DataType` are stable numeric IDs
on the wire, but applications should normally look them up by string:

```rust
use sedsnet::{DataEndpoint, DataType};

let radio = DataEndpoint::named("RADIO");
let gps = DataType::named("GPS_DATA");
```

You can add schema entries directly:

```rust
use sedsnet::{
    config::{register_data_type_id_with_description, register_endpoint_id_with_description},
    DataEndpoint, DataType, MessageClass, MessageDataType, MessageElement, ReliableMode,
};

let radio = register_endpoint_id_with_description(
    DataEndpoint(100),
    "RADIO",
    "Downlink radio",
    false,
)?;

register_data_type_id_with_description(
    DataType(100),
    "GPS_DATA",
    "Latitude, longitude, altitude",
    MessageElement::Static(3, MessageDataType::Float32, MessageClass::Data),
    &[radio],
    ReliableMode::Ordered,
    80,
)?;
```

Or seed from JSON at runtime. Host builds can use:

- `SEDSNET_STATIC_SCHEMA_PATH=/path/to/telemetry_config.json`
- `SEDSNET_STATIC_IPC_SCHEMA_PATH=/path/to/ipc_config.json`
- Rust `register_schema_json_path(...)` / `register_schema_json_bytes(...)`
- C `seds_schema_register_json_file(...)` / `seds_schema_register_json_bytes(...)`
- Python `register_schema_json_file(...)` / `register_schema_json_bytes(...)`

Default builds do not compile application JSON into the crate. Embedded builds can include
`telemetry_config.json` bytes only when the application provides that file locally before building;
those bytes are decoded through the normal runtime JSON parser.

The GUI editor still edits JSON schema files:

```bash
./telemetry_config_editor.py
```

For board-local IPC/software-bus endpoints, seed a second JSON file with
`SEDSNET_STATIC_IPC_SCHEMA_PATH` or the explicit JSON registration API. IPC seed entries are
applied at runtime and treated as link-local when loaded through the IPC path.

Built-in internal endpoint/type names for telemetry errors, reliable control, discovery, and time
sync are reserved. Do not register user handlers for `SEDSNET_DISCOVERY` or `SEDSNET_TIME_SYNC`.

### Network variables and E2E payloads

Network variables are latest-value caches keyed by data type. After registering or seeding schema,
enable a type on the router and use the getter/setter pair:

```rust
let flight_state = DataType::named("FLIGHT_STATE");
router.enable_network_variable(flight_state, NetworkVariablePermissions::READ_WRITE)?;
router.set_network_variable(pkt)?;
let cached = router.get_network_variable(flight_state, Some(1_000))?;
```

The setter commits the value to the network when permissions allow. The getter reads the cached copy
and internally requests the value if the cache has never seen it or is stale; user code does not
register a separate endpoint for network variables. 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. If a peer has the value,
the current packet is replayed through the normal endpoint handler so resync still looks like an
ordinary update. Register `on_network_variable_update(...)` when code needs a callback whenever an
inbound network update changes a variable cache.

Data types can also choose an E2E payload cryptography policy:

- `PreferOff`: plaintext unless the router forces cryptography
- `PreferOn`: encrypt when router crypto support is available
- `RequireOn`: reject send/subscribe paths unless cryptography support is available

Router E2E modes are `Disabled`, `RequiredOnly`, `Preferred`, and `ForceAll`. With the default
`cryptography` feature, the default is `Preferred`; minimal builds that explicitly omit it default
to `Disabled`. Crypto providers are tried in this order: registered C provider, registered Rust provider, then
registered software fallback key. The provider path is intended for OS crypto, hardware accelerators,
secure elements, and Rust-only embedded projects. Compact managed credential helpers are available
for deployments where a master/root node issues short-lived board credentials instead of users
managing certificate files.

Routers and relays can export JSON memory-layout snapshots to profile queue pressure at runtime,
including shared allocation, per-queue used/allocated bytes, reliable buffers, schema/discovery
state, and network-variable cache bytes.

Topology exports describe the board graph directly: `routers` contains each discovered board and
its connected peers, while `links` contains deduplicated `{source, target}` edges for visual graph
rendering. SEDSnet-owned control endpoints such as `SEDSNET_TIME_SYNC`, `SEDSNET_DISCOVERY`, and
`SEDSNET_ERROR` are filtered out of user endpoint reachability fields.

Routers and relays can also export per-client stats by sender ID. The snapshot reports whether the
client is still connected, which sides currently reach it, last-seen/age timing, named reachable
endpoints, and packet/byte counters aggregated from the side(s) used to reach that client.
Explicit `announce_leave(...)` calls, and best-effort C `free` paths, send a
`SEDSNET_DISCOVERY_LEAVE` control packet so peers can remove a departing sender from topology right
away.

Note: The editor uses Tkinter. On some Linux distros you may need to install it
(e.g. `sudo apt install python3-tk`).

Example application schema JSON:

```json
{
  "endpoints": [
    {
      "rust": "Radio",
      "name": "RADIO",
      "doc": "Downlink radio"
    },
    {
      "rust": "SdCard",
      "name": "SD_CARD",
      "doc": "Onboard logging"
    }
  ],
  "types": [
    {
      "rust": "GpsData",
      "name": "GPS_DATA",
      "doc": "Lat/Lon/Alt",
      "class": "Data",
      "element": {
        "kind": "Static",
        "data_type": "Float32",
        "count": 3
      },
      "endpoints": [
        "Radio",
        "SdCard"
      ]
    }
  ]
}
```

---

## Example CMakeLists.txt

```cmake
cmake_minimum_required(VERSION 3.22)
project(my_app C CXX)

add_executable(my_app
    src/main.c
)

# ---- sedsnet configuration ----
set(SEDSNET_DEVICE_IDENTIFIER "FC26_MAIN" CACHE STRING "" FORCE)
set(SEDSNET_TARGET "thumbv7em-none-eabihf" CACHE STRING "" FORCE)
set(SEDSNET_EMBEDDED_BUILD ON CACHE BOOL "" FORCE)
set(SEDSNET_MAX_STACK_PAYLOAD "256" CACHE STRING "" FORCE)
set(SEDSNET_MAX_QUEUE_BUDGET "65536" CACHE STRING "" FORCE)
set(SEDSNET_MAX_RECENT_RX_IDS "256" CACHE STRING "" FORCE)

# Add the submodule/subtree root (adjust path as needed)
add_subdirectory(${CMAKE_SOURCE_DIR}/sedsnet sedsnet_build)

target_link_libraries(my_app PRIVATE sedsnet::sedsnet)
```

---

## Using this repo as a subtree

```
git remote add sedsnet-upstream https://github.com/Rylan-Meilutis/sedsnet.git
git fetch sedsnet-upstream

git config subtree.sedsnet.remote sedsnet-upstream
git config subtree.sedsnet.branch main

git subtree add --prefix=sedsnet sedsnet-upstream main
```

To Switch branches:

```bash
git config subtree.sedsnet.branch <the-new-branch>
```

Update:

```bash
git subtree pull --prefix=sedsnet sedsnet-upstream main \
    -m "Merge sedsnet upstream main"
```

Helper scripts:

```bash
./scripts/subtree_update_no_stash.py
./scripts/subtree_update.py            # stash → update → stash-pop
```

---

## Using this repo as a submodule

If you prefer a **submodule** instead of a subtree:

```bash
git submodule add -b main https://github.com/Rylan-Meilutis/sedsnet.git sedsnet

git config submodule.sedsnet.branch main   # (or dev, etc.)
```

Initialize:

```bash
git submodule update --init --recursive
```

Update using helper scripts:

The scripts:

- read `submodule.sedsnet.branch`
- fetch `origin/<branch>`
- fast-forward the submodule repo
- stage & commit the updated submodule pointer in the parent repo

---

## Embedded allocator + lock hook examples (C)

For embedded (`--features embedded`) builds, provide these symbols:

- `void *telemetryMalloc(size_t)`
- `void telemetryFree(void *)`
- `void telemetry_lock(void)`
- `void telemetry_unlock(void)`
- `void seds_error_msg(const char *, size_t)`
- `void telemetry_panic_hook(const char *, size_t)`

Notes:

- `telemetry_lock`/`telemetry_unlock` must be recursive-safe.
- Do not call router/logging APIs from ISR context (hooks may block).
- Keep allocator non-blocking/fail-fast on RTOS targets (`NO_WAIT` style).

```C
// telemetry_hooks.c
#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>

/*
 * Rust expects these functions to exist for heap allocations:
 *
 *   void *telemetryMalloc(size_t);
 *   void telemetryFree(void *);
 *   void telemetry_lock(void);
 *   void telemetry_unlock(void);
 *   void seds_error_msg(const char *, const size_t);
 *   void telemetry_panic_hook(const char *, const size_t);
 *
 */

void telemetry_lock(void)
{
    /* Optional on bare metal / single-threaded targets. */
}

void telemetry_unlock(void)
{
    /* Optional on bare metal / single-threaded targets. */
}

void *telemetryMalloc(size_t xSize)
{
    if (xSize == 0) {
        xSize = 1;
    }
    return malloc(xSize);
}

void telemetryFree(void *pv)
{
    free(pv);
}

void seds_error_msg(const char *str, const size_t len)
{
    // Implement your logging mechanism here, for example:
    fwrite(str, 1, len, stderr);
    fwrite("\n", 1, 1, stderr);
}

void telemetry_panic_hook(const char *str, const size_t len)
{
    // Called from Rust panic handler in embedded/no_std builds.
    fwrite("PANIC: ", 1, 7, stderr);
    fwrite(str, 1, len, stderr);
    fwrite("\n", 1, 1, stderr);
}
```

### FreeRTOS example

```C
// telemetry_hooks_freertos.c
#include "FreeRTOS.h"
#include "semphr.h"
#include <stddef.h>
#include <stdio.h>

/* Example allocator backend; replace with heap_4/5 or your own allocator. */
void *pvPortMalloc(size_t xSize);
void vPortFree(void *pv);

static SemaphoreHandle_t g_telemetry_lock = NULL;

void telemetry_init_lock(void)
{
    if (g_telemetry_lock == NULL) {
        g_telemetry_lock = xSemaphoreCreateRecursiveMutex();
    }
}

void telemetry_lock(void)
{
    if (g_telemetry_lock != NULL && xPortIsInsideInterrupt() == pdFALSE) {
        (void)xSemaphoreTakeRecursive(g_telemetry_lock, portMAX_DELAY);
    }
}

void telemetry_unlock(void)
{
    if (g_telemetry_lock != NULL && xPortIsInsideInterrupt() == pdFALSE) {
        (void)xSemaphoreGiveRecursive(g_telemetry_lock);
    }
}

void *telemetryMalloc(size_t xSize)
{
    if (xSize == 0) {
        xSize = 1;
    }
    return pvPortMalloc(xSize);
}

void telemetryFree(void *pv)
{
    vPortFree(pv);
}

void seds_error_msg(const char *str, size_t len)
{
    (void)len;
    printf("%s\r\n", str);
}

void telemetry_panic_hook(const char *str, size_t len)
{
    (void)len;
    printf("PANIC: %s\r\n", str ? str : "(null)");
    taskDISABLE_INTERRUPTS();
    for (;;)
    {
    }
}
```

### ThreadX example

```C
// telemetry_hooks_threadx.c
#include "tx_api.h"
#include <stddef.h>
#include <stdio.h>

static TX_BYTE_POOL *rust_byte_pool_external = NULL;
static TX_MUTEX g_telemetry_mutex;
static UINT g_telemetry_mutex_ready = 0U;
static TX_THREAD *g_telemetry_mutex_owner = TX_NULL;
static UINT g_telemetry_mutex_recursion = 0U;

void telemetry_set_byte_pool(TX_BYTE_POOL *pool)
{
    rust_byte_pool_external = pool;
}

void telemetry_init_lock(void)
{
    if (g_telemetry_mutex_ready == 0U) {
        if (tx_mutex_create(&g_telemetry_mutex, "telemetry_mutex", TX_INHERIT) == TX_SUCCESS) {
            g_telemetry_mutex_ready = 1U;
        }
    }
}

void telemetry_lock(void)
{
    if (g_telemetry_mutex_ready == 0U) {
        return;
    }

    TX_THREAD *self = tx_thread_identify();
    if (self == TX_NULL) {
        /* Not in thread context; do not block in ISR/startup contexts. */
        return;
    }

    if (g_telemetry_mutex_owner == self) {
        g_telemetry_mutex_recursion++;
        return;
    }

    if (tx_mutex_get(&g_telemetry_mutex, TX_WAIT_FOREVER) == TX_SUCCESS) {
        g_telemetry_mutex_owner = self;
        g_telemetry_mutex_recursion = 1U;
    }
}

void telemetry_unlock(void)
{
    if (g_telemetry_mutex_ready == 0U) {
        return;
    }

    TX_THREAD *self = tx_thread_identify();
    if (self == TX_NULL) {
        return;
    }

    if (g_telemetry_mutex_owner != self) {
        return;
    }

    if (g_telemetry_mutex_recursion > 1U) {
        g_telemetry_mutex_recursion--;
        return;
    }

    g_telemetry_mutex_owner = TX_NULL;
    g_telemetry_mutex_recursion = 0U;
    (void)tx_mutex_put(&g_telemetry_mutex);
}

void *telemetryMalloc(size_t xSize)
{
    void *ptr = NULL;
    if (rust_byte_pool_external == NULL) {
        return NULL;
    }

    if (xSize == 0U) {
        xSize = 1U;
    }

    if (tx_byte_allocate(rust_byte_pool_external, &ptr, xSize, TX_NO_WAIT) != TX_SUCCESS) {
        return NULL;
    }
    return ptr;
}

void telemetryFree(void *pv)
{
    if (pv != NULL) {
        (void)tx_byte_release(pv);
    }
}

void seds_error_msg(const char *str, size_t len)
{
    (void)len;
    printf("%s\r\n", str);
}

void telemetry_panic_hook(const char *str, size_t len)
{
    (void)len;
    printf("PANIC: %s\r\n", str ? str : "(null)");
    for (;;)
    {
    }
}
```

Call `telemetry_init_lock()` and `telemetry_set_byte_pool(...)` before any telemetry/router API usage.