refactor: LoRaBoard trait for compile-time board completeness verification

Every board's Board struct now implements the LoRaBoard trait, which
enforces that NAME, TX_POWER_RANGE, init(), and mac_address() are all
provided. Missing any of these is a compile error — no "good intentions"
needed.

The trait verifies metadata and initialization. Tasks use the board's
concrete types directly (Embassy tasks must be concrete). Board-specific
type aliases (RadioDriver, UsbDriver, DisplayI2c) and struct definitions
(RadioParts, UsbParts, DisplayParts) remain in each board file.

- New: src/board/traits.rs (LoRaBoard trait definition)
- Modified: all 3 board files (implement trait, remove standalone constants)
- Modified: main.rs (import trait for init)
- Modified: radio/task.rs (use Board::TX_POWER_RANGE via trait)
- Modified: display/task.rs (use Board::NAME via trait)
- Modified: build.rs (exclude traits.rs from board discovery)
- Removed: embedded-hal-async direct dep (not needed for trait approach)
This commit is contained in:
swaits committed 2026-03-30 08:44:39 -06:00
1 parent 99a01b867e
commit 2633daf437
11 files changed
+107 -55

No files matched your search

+6 -6
View File
@@ -24,12 +24,12 @@ use embassy_time::Delay;
use lora_phy::mod_params::RadioError;
use lora_phy::{LoRa, RxMode};
use crate::board::{RadioDriver, RadioParts, TX_POWER_RANGE};
use crate::board::{Board, LoRaBoard, RadioDriver, RadioParts};
use crate::channel::{CommandChannel, RadioState, RadioStatus, ResponseChannel, StatusWatch};
use crate::protocol::{self, Command, ErrorCode, RadioConfig, Response};
const MAX_PAYLOAD: usize = protocol::MAX_PAYLOAD;
const POWER_RANGE: (i8, i8) = TX_POWER_RANGE;
// TX power range comes from the board trait: Board::TX_POWER_RANGE
type Radio = LoRa<RadioDriver, Delay>;
@@ -178,14 +178,14 @@ async fn handle_cmd(
}
}
Command::SetConfig(cfg) => {
if let Err(reason) = cfg.validate(POWER_RANGE) {
if let Err(reason) = cfg.validate(Board::TX_POWER_RANGE) {
warn!("SetConfig rejected: {}", reason);
responses
.send(Response::Error(ErrorCode::InvalidConfig))
.await;
} else {
// Resolve TX_POWER_MAX sentinel to board's actual max
state.config = Some(cfg.resolve_power(POWER_RANGE));
state.config = Some(cfg.resolve_power(Board::TX_POWER_RANGE));
status.sender().send(state.clone());
responses.send(Response::Ok).await;
}
@@ -221,9 +221,9 @@ async fn handle_cmd(
}
Command::DisplayOn | Command::DisplayOff => {}
Command::Transmit { config, payload } => {
let tx_config = config.map(|c| c.resolve_power(POWER_RANGE)).or(state.config);
let tx_config = config.map(|c| c.resolve_power(Board::TX_POWER_RANGE)).or(state.config);
if let Some(cfg) = tx_config {
if let Err(reason) = cfg.validate(POWER_RANGE) {
if let Err(reason) = cfg.validate(Board::TX_POWER_RANGE) {
warn!("TX config rejected: {}", reason);
responses
.send(Response::Error(ErrorCode::InvalidConfig))