From 024b03c7adc45ce4b790ef77d35d1278efa0fb67 Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Fri, 27 Mar 2026 00:29:18 -0600 Subject: [PATCH] feat: implement LoRa radio operations (TX, RX, config) Wire up the lora-phy driver in radio_task: - SetConfig stores radio parameters - StartRx enters continuous receive mode via lora-phy - rx_once waits for a packet, reports RSSI/SNR/payload to host - StopRx returns to standby - Transmit sends a payload with the configured (or overridden) params - Concurrent RX: select between incoming packets and host commands - Automatic RX restart after receiving a packet or completing TX - Error handling: radio errors are reported to host, RX fallback to idle Co-Authored-By: Claude Opus 4.6 (1M context) --- src/radio/task.rs | 247 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 195 insertions(+), 52 deletions(-) diff --git a/src/radio/task.rs b/src/radio/task.rs index 67806ee6..17f92274 100644 --- a/src/radio/task.rs +++ b/src/radio/task.rs @@ -1,10 +1,17 @@ -use defmt::error; +use defmt::{error, info, warn}; use embassy_executor::task; -use lora_phy::LoRa; +use embassy_futures::select::{select, Either}; +use embassy_time::Delay; +use lora_phy::mod_params::{PacketStatus, RadioError}; +use lora_phy::{LoRa, RxMode}; -use crate::board::RadioParts; +use crate::board::{RadioDriver, RadioParts}; use crate::channel::{CommandChannel, RadioState, RadioStatus, ResponseChannel, StatusWatch}; -use crate::protocol::{Command, ErrorCode, Response}; +use crate::protocol::{self, Command, ErrorCode, RadioConfig, Response}; + +const MAX_PAYLOAD: usize = protocol::MAX_PAYLOAD; + +type Radio = LoRa; #[task] pub async fn radio_task( @@ -16,13 +23,11 @@ pub async fn radio_task( let mut state = RadioStatus::default(); status.sender().send(state.clone()); - let lora = LoRa::new(parts.driver, false, parts.delay).await; - let mut lora = match lora { + let mut lora = match LoRa::new(parts.driver, false, parts.delay).await { Ok(l) => l, Err(e) => { error!("radio init failed: {}", e); responses.send(Response::Error(ErrorCode::InvalidConfig)).await; - // Cannot proceed without radio — idle forever. loop { let cmd = commands.receive().await; if let Command::Ping = cmd { @@ -34,65 +39,203 @@ pub async fn radio_task( } }; + info!("radio initialized"); + let mut rx_buf = [0u8; MAX_PAYLOAD]; + loop { - let cmd = commands.receive().await; - match cmd { - Command::Ping => { - responses.send(Response::Pong).await; - } - Command::GetConfig => { - if let Some(cfg) = state.config { - responses.send(Response::Config(cfg)).await; - } else { - responses.send(Response::Error(ErrorCode::NotConfigured)).await; + if state.state == RadioState::Receiving { + match select(rx_once(&mut lora, &state, &mut rx_buf), commands.receive()).await { + Either::First(rx_result) => { + match rx_result { + Ok((len, pkt_status)) => { + state.rx_count = state.rx_count.wrapping_add(1); + let rssi = pkt_status.rssi; + let snr = pkt_status.snr as i8; + state.last_rssi = Some(rssi); + state.last_snr = Some(snr); + status.sender().send(state.clone()); + + let mut payload = heapless::Vec::new(); + let _ = payload.extend_from_slice(&rx_buf[..len as usize]); + responses + .send(Response::RxPacket { + rssi, + snr, + payload, + }) + .await; + + if let Err(e) = start_rx(&mut lora, &state).await { + warn!("restart RX failed: {}", e); + state.state = RadioState::Idle; + status.sender().send(state.clone()); + } + } + Err(e) => { + warn!("RX error: {}", e); + if start_rx(&mut lora, &state).await.is_err() { + state.state = RadioState::Idle; + status.sender().send(state.clone()); + } + } + } + } + Either::Second(cmd) => { + handle_cmd(cmd, &mut lora, &mut state, responses, status, &mut rx_buf).await; } } - Command::SetConfig(cfg) => { - state.config = Some(cfg); - // TODO: apply config to lora hardware - status.sender().send(state.clone()); + } else { + let cmd = commands.receive().await; + handle_cmd(cmd, &mut lora, &mut state, responses, status, &mut rx_buf).await; + } + } +} + +async fn handle_cmd( + cmd: Command, + lora: &mut Radio, + state: &mut RadioStatus, + responses: &ResponseChannel, + status: &StatusWatch, + _rx_buf: &mut [u8], +) { + match cmd { + Command::Ping => { + responses.send(Response::Pong).await; + } + Command::GetConfig => { + if let Some(cfg) = state.config { + responses.send(Response::Config(cfg)).await; + } else { + responses.send(Response::Error(ErrorCode::NotConfigured)).await; } - Command::StartRx => { - if state.config.is_some() { - state.state = RadioState::Receiving; - // TODO: start continuous RX via lora - status.sender().send(state.clone()); - } else { - responses.send(Response::Error(ErrorCode::NotConfigured)).await; + } + Command::SetConfig(cfg) => { + state.config = Some(cfg); + status.sender().send(state.clone()); + } + Command::StartRx => { + if state.config.is_some() { + match start_rx(lora, state).await { + Ok(()) => { + state.state = RadioState::Receiving; + status.sender().send(state.clone()); + } + Err(e) => { + warn!("StartRx failed: {}", e); + responses.send(Response::Error(ErrorCode::InvalidConfig)).await; + } } + } else { + responses.send(Response::Error(ErrorCode::NotConfigured)).await; } - Command::StopRx => { - state.state = RadioState::Idle; - // TODO: put lora radio into standby + } + Command::StopRx => { + let _ = lora.enter_standby().await; + state.state = RadioState::Idle; + status.sender().send(state.clone()); + } + Command::DisplayOn | Command::DisplayOff => {} + Command::Transmit { config, payload } => { + let tx_config = config.or(state.config); + if let Some(cfg) = tx_config { + let was_receiving = state.state == RadioState::Receiving; + state.state = RadioState::Transmitting; status.sender().send(state.clone()); - } - Command::DisplayOn | Command::DisplayOff => { - // Routed by usb_task to display — should never reach radio. - } - Command::Transmit { config, payload } => { - let tx_config = config.or(state.config); - if let Some(_cfg) = tx_config { - let was_receiving = state.state == RadioState::Receiving; - state.state = RadioState::Transmitting; - status.sender().send(state.clone()); - // TODO: transmit payload via lora - let _ = &mut lora; - let _ = payload; + match do_tx(lora, &cfg, &payload).await { + Ok(()) => { + state.tx_count = state.tx_count.wrapping_add(1); + responses.send(Response::TxDone).await; + } + Err(e) => { + warn!("TX failed: {}", e); + responses.send(Response::Error(ErrorCode::TxTimeout)).await; + } + } - state.tx_count = state.tx_count.wrapping_add(1); - responses.send(Response::TxDone).await; - - state.state = if was_receiving { + state.state = if was_receiving { + if start_rx(lora, state).await.is_ok() { RadioState::Receiving } else { RadioState::Idle - }; - status.sender().send(state.clone()); + } } else { - responses.send(Response::Error(ErrorCode::NotConfigured)).await; - } + RadioState::Idle + }; + status.sender().send(state.clone()); + } else { + responses.send(Response::Error(ErrorCode::NotConfigured)).await; } } } } + +// ── LoRa helpers ───────────────────────────────────────────────────── + +fn to_bw(bw: protocol::Bandwidth) -> lora_phy::mod_params::Bandwidth { + use lora_phy::mod_params::Bandwidth::*; + match bw { + protocol::Bandwidth::Khz7 => _7KHz, + protocol::Bandwidth::Khz10 => _10KHz, + protocol::Bandwidth::Khz15 => _15KHz, + protocol::Bandwidth::Khz20 => _20KHz, + protocol::Bandwidth::Khz31 => _31KHz, + protocol::Bandwidth::Khz41 => _41KHz, + protocol::Bandwidth::Khz62 => _62KHz, + protocol::Bandwidth::Khz125 => _125KHz, + protocol::Bandwidth::Khz250 => _250KHz, + protocol::Bandwidth::Khz500 => _500KHz, + } +} + +fn to_sf(sf: u8) -> lora_phy::mod_params::SpreadingFactor { + use lora_phy::mod_params::SpreadingFactor::*; + match sf { + 5 => _5, + 6 => _6, + 8 => _8, + 9 => _9, + 10 => _10, + 11 => _11, + 12 => _12, + _ => _7, + } +} + +fn to_cr(cr: protocol::CodingRate) -> lora_phy::mod_params::CodingRate { + use lora_phy::mod_params::CodingRate::*; + match cr { + protocol::CodingRate::Cr4_5 => _4_5, + protocol::CodingRate::Cr4_6 => _4_6, + protocol::CodingRate::Cr4_7 => _4_7, + protocol::CodingRate::Cr4_8 => _4_8, + } +} + +async fn start_rx(lora: &mut Radio, state: &RadioStatus) -> Result<(), RadioError> { + let cfg = state.config.unwrap(); + let mdltn = lora.create_modulation_params(to_sf(cfg.sf), to_bw(cfg.bw), to_cr(cfg.cr), cfg.freq_hz)?; + let pkt = lora.create_rx_packet_params(8, false, MAX_PAYLOAD as u8, true, false, &mdltn)?; + lora.prepare_for_rx(RxMode::Continuous, &mdltn, &pkt).await?; + Ok(()) +} + +async fn rx_once( + lora: &mut Radio, + state: &RadioStatus, + buf: &mut [u8], +) -> Result<(u8, PacketStatus), RadioError> { + let cfg = state.config.unwrap(); + let mdltn = lora.create_modulation_params(to_sf(cfg.sf), to_bw(cfg.bw), to_cr(cfg.cr), cfg.freq_hz)?; + let pkt = lora.create_rx_packet_params(8, false, MAX_PAYLOAD as u8, true, false, &mdltn)?; + lora.rx(&pkt, buf).await +} + +async fn do_tx(lora: &mut Radio, cfg: &RadioConfig, payload: &[u8]) -> Result<(), RadioError> { + let mdltn = lora.create_modulation_params(to_sf(cfg.sf), to_bw(cfg.bw), to_cr(cfg.cr), cfg.freq_hz)?; + let mut tx_pkt = lora.create_tx_packet_params(8, false, true, false, &mdltn)?; + lora.prepare_for_tx(&mdltn, &mut tx_pkt, cfg.tx_power_dbm as i32, payload).await?; + lora.tx().await?; + Ok(()) +}