feat(mesh): operator-configurable Meshcore LoRa PHY params (freq/bw/sf/cr)

Archy set no radio params on Meshcore devices — SF/CR/BW/freq lived only in
device firmware, so a radio on the wrong preset could not be fixed from the
node (it hears RF energy but demodulates nothing; the fleet hit exactly this
on an RNode: docs/RETICULUM-TRANSPORT-PROGRESS.md item 4).

- protocol.rs: build_set_radio_params — wire format verified against the
  MeshCore companion firmware handler (CMD_SET_RADIO_PARAMS=11:
  [11][freq:u32 LE MHz*1000][bw:u32 LE kHz*1000][sf][cr]); byte-layout test
  pinned to the Portugal preset 869.618 MHz / 62.5 kHz / SF8 / CR8.
- serial.rs: MeshcoreDevice::set_radio_params (device reboots on OK).
- MeshConfig.lora_radio_params: Option — None (default) leaves the radio
  untouched, so only explicitly-configured deployments are affected.
- session.rs: provision on connect, gated on a persisted last-applied marker
  (SELF_INFO offsets shift across firmware versions) + the same attempt cap
  as region/channel so a refusing radio never reboot-loops.
- mesh.configure RPC: lora_radio_params arm with firmware-range validation.

mesh tests: 114/114 pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-07-21 06:10:45 -04:00
co-authored by Claude Fable 5
parent c1dfc6672c
commit 4c3cd4b6ad
6 changed files with 173 additions and 0 deletions
+23
View File
@@ -322,6 +322,22 @@ pub(crate) async fn seed_federation_peers_into_mesh(
}
}
/// Operator-configured LoRa PHY parameters for a Meshcore radio, in the
/// firmware's own field units: `freq_khz` = MHz×1000 (869618 → 869.618 MHz),
/// `bw_hz` = kHz×1000 (62500 → 62.5 kHz), `sf` 5..=12, `cr` 5..=8. These are
/// region/deployment-specific (e.g. the Portugal preset 869618/62500/8/8) and
/// MUST match every radio on the local mesh — a mismatched radio hears RF
/// energy but demodulates nothing. None (the default) leaves the device's own
/// settings untouched, so nodes outside the configured deployment are never
/// affected.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub struct LoraRadioParams {
pub freq_khz: u32,
pub bw_hz: u32,
pub sf: u8,
pub cr: u8,
}
/// Mesh configuration (persisted to disk).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MeshConfig {
@@ -340,6 +356,11 @@ pub struct MeshConfig {
/// unset/None.
#[serde(default)]
pub lora_region: Option<String>,
/// Meshcore LoRa PHY parameters (freq/bw/sf/cr). Provisioned onto the
/// radio on connect when set; None leaves the device untouched. Ignored
/// for Meshtastic (region/preset covers it) and Reticulum.
#[serde(default)]
pub lora_radio_params: Option<LoraRadioParams>,
/// Whether to periodically broadcast our identity.
#[serde(default)]
pub broadcast_identity: bool,
@@ -422,6 +443,7 @@ impl Default for MeshConfig {
device_path: None,
channel_name: Some("archipelago".to_string()),
lora_region: None,
lora_radio_params: None,
broadcast_identity: true,
advert_name: None,
mesh_only_mode: None,
@@ -722,6 +744,7 @@ impl MeshService {
self.our_x25519_pubkey_hex.clone(),
self.server_name.clone(),
self.config.lora_region.clone(),
self.config.lora_radio_params,
self.config.channel_name.clone(),
self.config.device_kind,
self.config.reticulum_tcp.clone(),