Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion crates/ax25-node-fw/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ pub struct NodeConfig {
pub axudp: AxudpConfig,
pub kiss_tcp: KissTcpConfig,
pub kiss_serial: KissSerialConfig,
pub tait: TaitConfig,
pub telnet: TelnetConfig,
pub netrom: NetRomConfig,
/// Optional `host[:port]` of an MQTT broker to publish logs/status to
Expand Down Expand Up @@ -88,6 +89,26 @@ pub struct KissTcpConfig {
#[derive(Clone)]
pub struct KissSerialConfig {
pub baud: u32,
/// Optional NinoTNC operating mode to set at boot via KISS SETHW (RAM-only —
/// spares flash). `None` (the default) leaves the modem's own configured mode
/// untouched. From the build env `NINOTNC_MODE`; a §policy knob so the node can
/// force a known modem mode at startup. Values > 15 are rejected by SETHW.
pub startup_mode: Option<u8>,
}

/// CCDI-controlled Tait radio on a second UART (radio integration).
#[derive(Clone)]
pub struct TaitConfig {
/// CCDI serial rate. The radio's programmed rate wins; default
/// [`ax25_node_core::radio::tait::DEFAULT_BAUD`] (28 800). Overridable at build
/// time via `TAIT_BAUD`.
pub baud: u32,
/// Optional programmed channel to select at boot (GO_TO_CHANNEL). `None` leaves
/// the radio on its current channel. From the build env `TAIT_CHANNEL`.
pub channel: Option<u16>,
/// Seconds between RSSI polls — also the cadence at which interleaved
/// carrier-sense / PTT PROGRESS edges are drained.
pub rssi_poll_secs: u64,
}

/// Telnet command console (capability 4).
Expand Down Expand Up @@ -140,7 +161,18 @@ pub fn load() -> NodeConfig {
kiss_tcp: KissTcpConfig {
target: option_env!("KISS_TCP_TARGET"),
},
kiss_serial: KissSerialConfig { baud: 57600 },
kiss_serial: KissSerialConfig {
baud: 57600,
startup_mode: option_env!("NINOTNC_MODE").and_then(|s| s.parse::<u8>().ok()),
},
tait: TaitConfig {
baud: parse_u32(
option_env!("TAIT_BAUD"),
ax25_node_core::radio::tait::DEFAULT_BAUD,
),
channel: option_env!("TAIT_CHANNEL").and_then(|s| s.parse::<u16>().ok()),
rssi_poll_secs: 5,
},
telnet: TelnetConfig { port: 8023 },
netrom: NetRomConfig {
originate: true,
Expand Down
32 changes: 31 additions & 1 deletion crates/ax25-node-fw/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,9 @@ mod firmware {
spawner.spawn(defmt::unwrap!(transports::kiss_tcp::task(
stack,
cfg.kiss_tcp.clone(),
cfg.netrom.clone(),
cfg.identity.callsign,
cfg.identity.alias,
)));

// --- mDNS: make the node discoverable as <hostname>.local + _telnet._tcp ---
Expand All @@ -371,7 +373,35 @@ mod firmware {
},
)));

// GATE 6+ returns kiss_serial (needs a NinoTNC) + the session supervisor.
// --- GATE 6 (HW-BRINGUP.md §4): KISS-over-UART to a NinoTNC (capability 3).
// UART1 on GP20(TX)/GP21(RX), the NinoBLE Rev5 link pins. The task always
// spawns (there is no build-env target for a physical UART); it reads
// nothing until a NinoTNC is wired, but the read-only NET/ROM tap, NODES
// origination, obsolescence sweep and beacon all run. COMPILE-VALIDATED
// ONLY — the live exchange needs the NinoTNC on the bench (no hardware here).
spawner.spawn(defmt::unwrap!(transports::kiss_serial::task(
p.UART1,
p.PIN_20,
p.PIN_21,
cfg.kiss_serial.clone(),
cfg.netrom.clone(),
cfg.identity.callsign,
cfg.identity.alias,
)));

// --- Tait CCDI radio control on a SECOND UART: UART0 GP0(TX)/GP1(RX),
// distinct from the NinoTNC KISS link on UART1. Drives the core CCDI driver
// (RSSI / PTT / channel / carrier-sense). Always spawns (no build-env target
// for a physical UART); it self-quiets if no Tait radio answers. COMPILE-
// VALIDATED ONLY — the live exchange needs a Tait radio on GP0/GP1. ---
spawner.spawn(defmt::unwrap!(transports::tait_ccdi::task(
p.UART0,
p.PIN_0,
p.PIN_1,
cfg.tait.clone(),
)));

// The session supervisor (shared Sessions across transports) is the next seam.

let mut ticker = Ticker::every(Duration::from_secs(10));
loop {
Expand Down
23 changes: 23 additions & 0 deletions crates/ax25-node-fw/src/transports/axudp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,12 @@ pub async fn task(
netrom_cfg.nodes_interval_secs
);
}
// Obsolescence sweep cadence: age/purge the routing table once per NODES
// interval (the C# `NetRomService.OnInterval` sweep — see NetRomService.cs).
// Runs whether or not WE originate: obsolescence aging is about the table, and
// OBSINIT is calibrated to one broadcast period per decrement. First sweep after
// one interval (never age a freshly booted/flash-restored table immediately).
let mut next_sweep_at = Instant::now() + nodes_interval;

// The connected-mode session layer for this port + per-peer link state.
let mut sessions = session::new_sessions(my_call);
Expand Down Expand Up @@ -484,6 +490,23 @@ pub async fn task(
}
}

// Obsolescence sweep — age/purge routes once per NODES interval,
// BEFORE origination so a broadcast advertises the freshly-aged
// table (the C# `NetRomService.OnInterval` order: Sweep() then
// BroadcastNodes()). Drives the correctness path the recon flagged:
// without this, obsolescence never ages, dead routes never purge,
// and the OBSMIN advertise-gate never engages.
if Instant::now() >= next_sweep_at {
next_sweep_at = Instant::now() + nodes_interval;
let purged = netrom.sweep();
if purged > 0 {
defmt::info!(
"netrom: obsolescence sweep purged {=usize} stale route(s)",
purged
);
}
}

// NODES origination rides the beacon tick (10 s granularity is
// plenty against minutes-scale intervals).
if netrom_cfg.originate && Instant::now() >= next_nodes_at {
Expand Down
Loading
Loading