fix(radio): cache mod params on SET_CONFIG so TX works without prior RX

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.
This commit is contained in:
swaits committed 2026-05-11 11:15:30 -06:00
1 parent 0b9f9ee882
commit d7a1ee86d0
1 file changed
+30 -26
+30 -26
View File
@@ -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_phy::mod_params::ModulationParams, RadioError> {
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> {