From d7a1ee86d0d3bc1c3677f80fbe79d9688c483c75 Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Mon, 11 May 2026 11:11:06 -0600 Subject: [PATCH] fix(radio): cache mod params on SET_CONFIG so TX works without prior RX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The TX state machine's `prep_for_cad_act` (TxCadPrep entry) and `prep_for_tx_act` (TxPrep entry) both read `ctx.mdltn` and emit `TransmitFailed` → `ERR(ERadio)` if it's None. But `ctx.mdltn` was only ever assigned on the RX path — `try_start_rx_hw` (RxArming entry) and `cache_rx_pkt` (RxSession entry). `reconfigure_radio` already called `to_lora_mod_params(lora, cfg)` during SET_CONFIG but discarded the result via `let _ = …?;` (comment claimed image calibration; the function is pure compute on SX126x). Result: a freshly-booted dongle that issues TX before ever calling RX_START gets `ERR(ERadio)` on the first CmdTx because the chart's TX-entry actions find `ctx.mdltn = None`. The bug was hidden in practice by any prior RX_START (ai-bot, ping_pong --role rx, etc.) leaving `ctx.mdltn` populated across sessions until the next reflash. Fix: thread the `ModulationParams` out of `reconfigure_radio` as its return value, and have `apply_set_config` cache it in `self.mdltn` alongside `self.config`. The failure path clears `self.mdltn` along with `self.config` so a SET_CONFIG that fails leaves a clean Unconfigured state. Surfaced while testing the matching sync-word fix on freshly-flashed dongles via `examples/sync_word_test.py` — the first TX failed with `ERR(ERadio)` until this path was wired up. --- src/radio.rs | 56 ++++++++++++++++++++++++++++------------------------ 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/src/radio.rs b/src/radio.rs index 06ddf25..0c80ded 100644 --- a/src/radio.rs +++ b/src/radio.rs @@ -1252,29 +1252,33 @@ impl RadioActions for RadioActionContext<'_> { Some(lora) => reconfigure_radio(lora, &cfg).await, None => Err(RadioError::Busy), }; - if recfg_result.is_err() { - // Hardware failure: drop to Unconfigured (spec §6.3). - // Clear all per-session state — including any in-flight - // RxStart tag — so the next CmdRxStart in Unconfigured - // is rejected cleanly (rather than racing a stale tag - // through respond_err_start_rx on an unrelated path). - self.config = None; - self.cad_retry_delay_us = 0; - self.pending_rx_start_tag = None; - info!("config cleared (apply_set_config failure tag={=u16})", tag); - self.send(tag, DeviceMessage::Err(ErrorCode::ERadio)).await; - // Wedge takes precedence over ConfigFailedRadio so the - // chart enters Recovering (NRESET + lora.init()) instead - // of bouncing back to Unconfigured with a still-wedged - // chip that the next SET_CONFIG would also fail on. - if take_wedge_flag() { - let _ = self.emit(RadioInput::WedgeDetected); - } else { - let _ = self.emit(RadioInput::ConfigFailedRadio); + let mdltn = match recfg_result { + Ok(m) => m, + Err(_) => { + // Hardware failure: drop to Unconfigured (spec §6.3). + // Clear all per-session state — including any in-flight + // RxStart tag — so the next CmdRxStart in Unconfigured + // is rejected cleanly (rather than racing a stale tag + // through respond_err_start_rx on an unrelated path). + self.config = None; + self.cad_retry_delay_us = 0; + self.pending_rx_start_tag = None; + info!("config cleared (apply_set_config failure tag={=u16})", tag); + self.send(tag, DeviceMessage::Err(ErrorCode::ERadio)).await; + // Wedge takes precedence over ConfigFailedRadio so the + // chart enters Recovering (NRESET + lora.init()) instead + // of bouncing back to Unconfigured with a still-wedged + // chip that the next SET_CONFIG would also fail on. + if take_wedge_flag() { + let _ = self.emit(RadioInput::WedgeDetected); + } else { + let _ = self.emit(RadioInput::ConfigFailedRadio); + } + return; } - return; - } + }; self.config = Some(cfg); + self.mdltn = Some(mdltn); info!("config set (apply_set_config success tag={=u16})", tag); self.cad_retry_delay_us = toa_us(&cfg, CAD_ASSUMED_PEER_PAYLOAD_B); if self.cad_retry_delay_us > CAD_BUDGET_US { @@ -2094,7 +2098,10 @@ fn to_lora_mod_params( lora.create_modulation_params(sf, bw, cr, cfg.freq_hz) } -async fn reconfigure_radio(lora: &mut LoRaDriver, cfg: &LoRaConfig) -> Result<(), RadioError> { +async fn reconfigure_radio( + lora: &mut LoRaDriver, + cfg: &LoRaConfig, +) -> Result { lora_setup("reconfigure_radio.enter_standby", lora.enter_standby()).await?; // Apply the host-supplied LoRa sync word. The protocol field is a // u16 holding the SX126x register pair in big-endian order @@ -2112,10 +2119,7 @@ async fn reconfigure_radio(lora: &mut LoRaDriver, cfg: &LoRaConfig) -> Result<() lora.set_lora_sync_word(sync_byte), ) .await?; - // Calibrate image etc. via a throwaway create_modulation_params call - // — inside the SET_CONFIG OK window so first TX/RX is fast. - let _ = to_lora_mod_params(lora, cfg)?; - Ok(()) + to_lora_mod_params(lora, cfg) } fn validate_lora(cfg: &LoRaConfig, info: &Info) -> Result<(), ErrorCode> {