Skip to content

fw: NET/ROM sweep + RF origination + kiss_serial pump + Tait CCDI transport (fixes fw-build) - #60

Merged
M0LTE merged 4 commits into
mainfrom
feat/fw-netrom-radio-transports
Jul 12, 2026
Merged

fw: NET/ROM sweep + RF origination + kiss_serial pump + Tait CCDI transport (fixes fw-build)#60
M0LTE merged 4 commits into
mainfrom
feat/fw-netrom-radio-transports

Conversation

@M0LTE

@M0LTE M0LTE commented Jul 12, 2026

Copy link
Copy Markdown
Collaborator

Also fixes a red main: #58's new NinoTncInboundEvent::{StatusReport,RssiReading} variants made the fw crate's classify match non-exhaustive (the core-only test gate doesn't build the excluded fw crate, so it slipped past local checks — the fw-build CI job catches it).

fw wave (all compile-validated only — no hardware run this session; both cargo build --release --locked and cargo test --release --locked --no-run pass, Cargo.lock unchanged):

  1. NODES obsolescence-sweep bugNetRomService::sweep() existed in core but no fw caller drove it (routes never aged); now wired into the NODES-interval tick before origination.
  2. NODES origination + RX observe on RF — the node was AXUDP/LAN-only + invisible on real RF; origination + observe-tap now on kiss_tcp and kiss_serial.
  3. kiss_serial pump + spawn — replaced the stubbed pump; spawned on UART1 GP20/21 past the callsign gate.
  4. Tait CCDI transport — new transports/tait_ccdi.rs driving core radio::tait over a second UART (RSSI poll + carrier/PTT/SDM PROGRESS demux).

Each item's bench-validation checklist is in the branch commits. Compile-only; behaviour needs the hardware-bringup session.

🤖 Generated with Claude Code

Tom Fanning and others added 4 commits July 12, 2026 18:30
Gap A (recon netrom.md §3): `NetRomService::sweep()` existed in core but no
fw transport ever called it, so the routing table never aged — obsolescence
never decremented, dead routes never purged, and the OBSMIN advertise-gate
never engaged. The node advertised stale routes indefinitely.

Wire a `next_sweep_at` cadence into the axudp beacon-tick loop that calls
`netrom.sweep()` once per NODES interval, BEFORE origination so a broadcast
reflects the freshly-aged table — the C# `NetRomService.OnInterval` order
(Sweep() then BroadcastNodes()). Runs whether or not we originate: aging is a
property of the table, and OBSINIT is calibrated to one broadcast period per
decrement. First sweep is one interval after boot (never age a freshly
flash-restored table immediately).

Also restores the crate build: the merged core added `StatusReport` /
`RssiReading` variants to `NinoTncInboundEvent`, which left kiss_serial's
classify match non-exhaustive. Add the two arms (log-only telemetry; the full
kiss_serial pump is rewritten in a later commit).

Compile-validated only (no hardware): `cargo build --release --locked` +
`cargo test --release --locked --no-run` both green. Sweep timing / actual
purge behaviour needs bench validation with a live NODES feed.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Gap B (recon netrom.md §3): NODES origination was AXUDP/LAN-only, so the node
was invisible in peers' nodes tables on the RF-shaped paths. Wire a
`NetRomOriginator` into kiss_tcp's per-connection `serve` loop — build our
broadcasts from the live routing table, wrap each payload as a UI frame (dest
`NODES`, PID 0xCF), KISS-frame it and send — plus the obsolescence sweep on the
same NODES interval (Gap A parity with axudp: age before advertise). The
observe-tap was already present on this path.

The header alias is the node mnemonic (`cfg.identity.alias`), falling back to
the callsign base, matching the axudp originator. The routing table is
per-connection (recreated on reconnect, as `netrom` already was), so a fresh
connection announces itself with a header-only "I'm here" frame until it hears
its first NODES broadcast — a pre-existing property of kiss_tcp's per-serve
netrom, noted for bench validation.

main.rs: pass `cfg.netrom` + `cfg.identity.alias` to the kiss_tcp spawn.

The kiss_serial observe-tap + origination (also Gap B) is folded into the
next commit, because on that transport it is inseparable from the task
signature/spawn/pump changes.

Compile-validated only (no hardware): `cargo build --release --locked` +
`cargo test --release --locked --no-run` both green. On-air NODES visibility,
sweep timing and the reconnect table-reset behaviour need bench validation
against a live KISS-TCP/net-sim peer.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Services Gap D + netrom Gap B: the KISS-over-UART transport was stubbed
(`let _ = ax25;` dropped every inbound frame) and never spawned. Fill it,
mirroring the KISS-TCP transport precisely:

- Pump: `select(modem.read_frame(), ticker.next())` — wake on an inbound KISS
  frame or the periodic tick. `read_frame` is cancel-safe (its only await is the
  UART read; decode state lives in the modem), so a tick that pre-empts the read
  loses no buffered bytes.
- Inbound: classify with NinoTNC awareness, then run the read-only NET/ROM tap
  (`session::observe_inbound`) on every AX.25 frame BEFORE any address filter, so
  NODES broadcasts are heard on real RF. Connected-mode session routing stays the
  deferred supervisor seam (as kiss_tcp leaves it).
- Outbound drain to the UART on the tick: beacon UI frame, obsolescence sweep
  (Gap A parity), and NODES origination (Gap B) built from the live table.

Wiring:
- main.rs: spawn `kiss_serial::task` past the callsign gate on UART1 GP20/21,
  passing netrom config + callsign + alias.
- config.rs: extend `KissSerialConfig` with `startup_mode: Option<u8>` (a boot
  NinoTNC SETHW mode, RAM-only; `None` default preserves behaviour; from
  `NINOTNC_MODE`).

COMPILE-VALIDATED ONLY (no hardware): `cargo build --release --locked` +
`cargo test --release --locked --no-run` both green. Everything from the UART
byte exchange onward — SETHW at boot, the read pump under real KISS/NinoTNC
traffic, beacon/NODES TX timing, cancel-safety of read_frame under the live
UART — needs bench validation with a NinoTNC on GP20/21 @ 57600.

Co-Authored-By: Claude Code <noreply@anthropic.com>
New `transports/tait_ccdi.rs` — an Embassy task owning a SECOND UART (UART0
GP0/GP1, distinct from the NinoTNC KISS link on UART1) that drives the core
`radio::tait::driver::TaitCcdiRadio` CCDI driver. Mirrors kiss_serial's
structure precisely (reuses its `UartByteStream` byte-source seam and the same
`configure_uart` / `bind_interrupts` shape; a UART0 interrupt binding).

The drive loop:
- enables unsolicited PROGRESS output at boot (FUNCTION 0/4) — required before
  carrier-sense (DCD) / PTT edges are reported;
- optionally retunes to a configured channel at boot (GO_TO_CHANNEL);
- polls RSSI (integer tenths-of-dBm) on a ticker, then drains the carrier-sense
  / PTT / SDM PROGRESS edges the driver demuxes out of each transaction (and
  maintains `channel_busy` from). Self-quiets on NoResponse so an absent radio
  doesn't spam warnings.

Wiring: `transports/mod.rs` registers the module; `config.rs` adds `TaitConfig`
(baud default = core `tait::DEFAULT_BAUD` 28800, optional boot channel, RSSI
poll cadence) via `TAIT_BAUD` / `TAIT_CHANNEL`; `main.rs` spawns it on
UART0/GP0/GP1.

COMPILE-VALIDATED ONLY (no hardware): `cargo build --release --locked` +
`cargo test --release --locked --no-run` both green; no new deps (Cargo.lock
unchanged). Needs bench validation with a Tait TM8100/TM8200 on GP0/GP1: the
CCDI transact/demux under a live radio, PROGRESS-enable acknowledgement, RSSI
values, carrier-sense/PTT edge timing, and the UART0 pin/baud choice against
the real wiring. Note the driver's documented parity caveat — a command ERROR
split into a later read than its prompt is not awaited (deferred grace window).

Co-Authored-By: Claude Code <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant