feat: initial lora-dongle firmware scaffold
Transparent LoRa-over-USB dongle firmware supporting Heltec V3 (ESP32-S3) and RAK 4631 (nRF52840) boards. Actor-model architecture with typed channels between USB, radio, and display tasks. - Protocol: Command/Response enums with COBS+postcard framing design - Board abstraction: auto-discovered via build.rs Jinja2 codegen - Radio task: SX1262 state machine (Idle → Receiving ↔ Transmitting) - Display task: optional SSD1306 dashboard via StatusWatch - RAK 4631: fully wired SPI/USB/I2C peripherals, clippy-clean - Heltec V3: structure in place, awaits espup Xtensa toolchain
This commit is contained in:
20 files changed
+2599
No files matched your search
@@ -0,0 +1,95 @@
|
||||
use defmt::error;
|
||||
use embassy_executor::task;
|
||||
use lora_phy::LoRa;
|
||||
|
||||
use crate::board::RadioParts;
|
||||
use crate::channel::{CommandChannel, RadioState, RadioStatus, ResponseChannel, StatusWatch};
|
||||
use crate::protocol::{Command, ErrorCode, Response};
|
||||
|
||||
#[task]
|
||||
pub async fn radio_task(
|
||||
parts: RadioParts,
|
||||
commands: &'static CommandChannel,
|
||||
responses: &'static ResponseChannel,
|
||||
status: &'static StatusWatch,
|
||||
) {
|
||||
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 {
|
||||
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 {
|
||||
responses.send(Response::Pong).await;
|
||||
} else {
|
||||
responses.send(Response::Error(ErrorCode::InvalidConfig)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
Command::SetConfig(cfg) => {
|
||||
state.config = Some(cfg);
|
||||
// TODO: apply config to lora hardware
|
||||
status.sender().send(state.clone());
|
||||
}
|
||||
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::StopRx => {
|
||||
state.state = RadioState::Idle;
|
||||
// TODO: put lora radio into standby
|
||||
status.sender().send(state.clone());
|
||||
}
|
||||
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;
|
||||
|
||||
state.tx_count = state.tx_count.wrapping_add(1);
|
||||
responses.send(Response::TxDone).await;
|
||||
|
||||
state.state = if was_receiving {
|
||||
RadioState::Receiving
|
||||
} else {
|
||||
RadioState::Idle
|
||||
};
|
||||
status.sender().send(state.clone());
|
||||
} else {
|
||||
responses.send(Response::Error(ErrorCode::NotConfigured)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user