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) <noreply@anthropic.com>
This commit is contained in:
1 file changed
+195
-52
+195
-52
@@ -1,10 +1,17 @@
|
|||||||
use defmt::error;
|
use defmt::{error, info, warn};
|
||||||
use embassy_executor::task;
|
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::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<RadioDriver, Delay>;
|
||||||
|
|
||||||
#[task]
|
#[task]
|
||||||
pub async fn radio_task(
|
pub async fn radio_task(
|
||||||
@@ -16,13 +23,11 @@ pub async fn radio_task(
|
|||||||
let mut state = RadioStatus::default();
|
let mut state = RadioStatus::default();
|
||||||
status.sender().send(state.clone());
|
status.sender().send(state.clone());
|
||||||
|
|
||||||
let lora = LoRa::new(parts.driver, false, parts.delay).await;
|
let mut lora = match LoRa::new(parts.driver, false, parts.delay).await {
|
||||||
let mut lora = match lora {
|
|
||||||
Ok(l) => l,
|
Ok(l) => l,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("radio init failed: {}", e);
|
error!("radio init failed: {}", e);
|
||||||
responses.send(Response::Error(ErrorCode::InvalidConfig)).await;
|
responses.send(Response::Error(ErrorCode::InvalidConfig)).await;
|
||||||
// Cannot proceed without radio — idle forever.
|
|
||||||
loop {
|
loop {
|
||||||
let cmd = commands.receive().await;
|
let cmd = commands.receive().await;
|
||||||
if let Command::Ping = cmd {
|
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 {
|
loop {
|
||||||
let cmd = commands.receive().await;
|
if state.state == RadioState::Receiving {
|
||||||
match cmd {
|
match select(rx_once(&mut lora, &state, &mut rx_buf), commands.receive()).await {
|
||||||
Command::Ping => {
|
Either::First(rx_result) => {
|
||||||
responses.send(Response::Pong).await;
|
match rx_result {
|
||||||
}
|
Ok((len, pkt_status)) => {
|
||||||
Command::GetConfig => {
|
state.rx_count = state.rx_count.wrapping_add(1);
|
||||||
if let Some(cfg) = state.config {
|
let rssi = pkt_status.rssi;
|
||||||
responses.send(Response::Config(cfg)).await;
|
let snr = pkt_status.snr as i8;
|
||||||
} else {
|
state.last_rssi = Some(rssi);
|
||||||
responses.send(Response::Error(ErrorCode::NotConfigured)).await;
|
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) => {
|
} else {
|
||||||
state.config = Some(cfg);
|
let cmd = commands.receive().await;
|
||||||
// TODO: apply config to lora hardware
|
handle_cmd(cmd, &mut lora, &mut state, responses, status, &mut rx_buf).await;
|
||||||
status.sender().send(state.clone());
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
Command::SetConfig(cfg) => {
|
||||||
state.state = RadioState::Receiving;
|
state.config = Some(cfg);
|
||||||
// TODO: start continuous RX via lora
|
status.sender().send(state.clone());
|
||||||
status.sender().send(state.clone());
|
}
|
||||||
} else {
|
Command::StartRx => {
|
||||||
responses.send(Response::Error(ErrorCode::NotConfigured)).await;
|
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;
|
Command::StopRx => {
|
||||||
// TODO: put lora radio into standby
|
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());
|
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
|
match do_tx(lora, &cfg, &payload).await {
|
||||||
let _ = &mut lora;
|
Ok(()) => {
|
||||||
let _ = payload;
|
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);
|
state.state = if was_receiving {
|
||||||
responses.send(Response::TxDone).await;
|
if start_rx(lora, state).await.is_ok() {
|
||||||
|
|
||||||
state.state = if was_receiving {
|
|
||||||
RadioState::Receiving
|
RadioState::Receiving
|
||||||
} else {
|
} else {
|
||||||
RadioState::Idle
|
RadioState::Idle
|
||||||
};
|
}
|
||||||
status.sender().send(state.clone());
|
|
||||||
} else {
|
} 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(())
|
||||||
|
}
|
||||||
Reference in new issue
Block a user