feat: per-board TX power range, TX_POWER_MAX sentinel (-128)
- Each board exports TX_POWER_RANGE constant (min, max dBm) - RadioConfig::validate() uses the board's range, not hardcoded limits - TX_POWER_MAX sentinel (-128 on the wire) means "use board's maximum" — resolved to the actual max before storing config - GetConfig returns the resolved actual power, not the sentinel - Default configs in examples use TX_POWER_MAX - Protocol docs updated
This commit is contained in:
8 files changed
+46
-14
No files matched your search
+1
-1
@@ -67,7 +67,7 @@ Postcard is a compact binary format. The subset used by DongLoRa:
|
||||
| `sf` | `u8` | varint | 5 – 12 | Spreading factor |
|
||||
| `cr` | `u8` | varint | 5 – 8 | Coding rate denominator (5 = CR 4/5) |
|
||||
| `sync_word` | `u16` | varint | any | LoRa sync word (e.g. `0x3444`) |
|
||||
| `tx_power_dbm` | `i8` | zigzag varint | -9 – +22 | TX power in dBm |
|
||||
| `tx_power_dbm` | `i8` | raw signed byte | board-dependent | TX power in dBm. `-128` = max power for this board. |
|
||||
|
||||
## Bandwidth
|
||||
|
||||
|
||||
@@ -197,12 +197,15 @@ def connect(port: str | None = None, timeout: float = 2.0) -> serial.Serial:
|
||||
return ser
|
||||
|
||||
|
||||
# Default radio config: 915 MHz, 125 kHz BW, SF7, CR 4/5
|
||||
# Sentinel: set TX power to the board's maximum
|
||||
TX_POWER_MAX = -128 # i8::MIN on the wire
|
||||
|
||||
# Default radio config: 915 MHz, 125 kHz BW, SF7, CR 4/5, max power
|
||||
DEFAULT_CONFIG = {
|
||||
"freq_hz": 915_000_000,
|
||||
"bw": 7, # 125 kHz
|
||||
"sf": 7,
|
||||
"cr": 5, # CR 4/5
|
||||
"sync_word": 0x1424,
|
||||
"tx_power_dbm": 14,
|
||||
"tx_power_dbm": TX_POWER_MAX,
|
||||
}
|
||||
@@ -916,7 +916,7 @@ RADIO_CONFIG = {
|
||||
"sf": 7,
|
||||
"cr": 5, # CR 4/5 — denominator value
|
||||
"sync_word": 0x3444,
|
||||
"tx_power_dbm": 14,
|
||||
"tx_power_dbm": -128, # TX_POWER_MAX: use board's maximum
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -25,6 +25,9 @@ pub type UsbDriver = esp_hal::otg_fs::asynch::Driver<'static>;
|
||||
|
||||
pub type DisplayI2c = I2c<'static, esp_hal::Async>;
|
||||
|
||||
/// TX power range for this board's radio (SX1262).
|
||||
pub const TX_POWER_RANGE: (i8, i8) = (-9, 22);
|
||||
|
||||
// ── Peripheral bundles ───────────────────────────────────────────────
|
||||
|
||||
pub struct RadioParts {
|
||||
|
||||
@@ -25,6 +25,9 @@ pub type UsbDriver = esp_hal::otg_fs::asynch::Driver<'static>;
|
||||
|
||||
pub type DisplayI2c = I2c<'static, esp_hal::Async>;
|
||||
|
||||
/// TX power range for this board's radio (SX1262).
|
||||
pub const TX_POWER_RANGE: (i8, i8) = (-9, 22);
|
||||
|
||||
// ── Peripheral bundles ───────────────────────────────────────────────
|
||||
|
||||
pub struct RadioParts {
|
||||
|
||||
@@ -33,6 +33,9 @@ pub type UsbDriver = Driver<'static, USBD, &'static embassy_nrf::usb::vbus_detec
|
||||
/// I2C bus for an optional SSD1306 OLED.
|
||||
pub type DisplayI2c = Twim<'static, embassy_nrf::peripherals::TWISPI0>;
|
||||
|
||||
/// TX power range for this board's radio (SX1262).
|
||||
pub const TX_POWER_RANGE: (i8, i8) = (-9, 22);
|
||||
|
||||
// ── Peripheral bundles ───────────────────────────────────────────────
|
||||
|
||||
pub struct RadioParts {
|
||||
|
||||
+23
-5
@@ -20,6 +20,9 @@ pub enum Bandwidth {
|
||||
Khz500 = 9,
|
||||
}
|
||||
|
||||
/// Sentinel value for `tx_power_dbm`: use the board's maximum TX power.
|
||||
pub const TX_POWER_MAX: i8 = i8::MIN; // -128 on the wire
|
||||
|
||||
/// Complete LoRa radio configuration.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, defmt::Format)]
|
||||
pub struct RadioConfig {
|
||||
@@ -31,13 +34,17 @@ pub struct RadioConfig {
|
||||
/// Coding rate denominator (5-8). E.g. 5 = CR 4/5, 8 = CR 4/8.
|
||||
pub cr: u8,
|
||||
pub sync_word: u16,
|
||||
/// Transmit power in dBm (-9 to +22 for SX1262).
|
||||
/// Transmit power in dBm. Set to [`TX_POWER_MAX`] (-128) for the
|
||||
/// board's maximum. Actual range is board-dependent (e.g. -9 to +22
|
||||
/// for SX1262). Values above the board max are clamped by the radio
|
||||
/// driver; values below the board min are clamped too.
|
||||
pub tx_power_dbm: i8,
|
||||
}
|
||||
|
||||
impl RadioConfig {
|
||||
/// Validate all fields are within SX1262 hardware limits.
|
||||
pub fn validate(&self) -> Result<(), &'static str> {
|
||||
/// Validate fields against hardware limits. `power_range` is the
|
||||
/// board's (min, max) TX power in dBm.
|
||||
pub fn validate(&self, power_range: (i8, i8)) -> Result<(), &'static str> {
|
||||
if !(150_000_000..=960_000_000).contains(&self.freq_hz) {
|
||||
return Err("frequency out of range (150-960 MHz)");
|
||||
}
|
||||
@@ -47,11 +54,22 @@ impl RadioConfig {
|
||||
if !(5..=8).contains(&self.cr) {
|
||||
return Err("coding rate out of range (5-8)");
|
||||
}
|
||||
if !(-9..=22).contains(&self.tx_power_dbm) {
|
||||
return Err("TX power out of range (-9 to +22 dBm)");
|
||||
// TX_POWER_MAX sentinel is always valid (resolved to board max later)
|
||||
if self.tx_power_dbm != TX_POWER_MAX
|
||||
&& !(power_range.0..=power_range.1).contains(&self.tx_power_dbm)
|
||||
{
|
||||
return Err("TX power out of range for this board");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Resolve the TX_POWER_MAX sentinel to the board's actual maximum.
|
||||
pub fn resolve_power(mut self, power_range: (i8, i8)) -> Self {
|
||||
if self.tx_power_dbm == TX_POWER_MAX {
|
||||
self.tx_power_dbm = power_range.1;
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Host → firmware commands.
|
||||
|
||||
+7
-5
@@ -24,11 +24,12 @@ use embassy_time::Delay;
|
||||
use lora_phy::mod_params::RadioError;
|
||||
use lora_phy::{LoRa, RxMode};
|
||||
|
||||
use crate::board::{RadioDriver, RadioParts};
|
||||
use crate::board::{RadioDriver, RadioParts, TX_POWER_RANGE};
|
||||
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;
|
||||
|
||||
type Radio = LoRa<RadioDriver, Delay>;
|
||||
|
||||
@@ -173,13 +174,14 @@ async fn handle_cmd(
|
||||
}
|
||||
}
|
||||
Command::SetConfig(cfg) => {
|
||||
if let Err(reason) = cfg.validate() {
|
||||
if let Err(reason) = cfg.validate(POWER_RANGE) {
|
||||
warn!("SetConfig rejected: {}", reason);
|
||||
responses
|
||||
.send(Response::Error(ErrorCode::InvalidConfig))
|
||||
.await;
|
||||
} else {
|
||||
state.config = Some(cfg);
|
||||
// Resolve TX_POWER_MAX sentinel to board's actual max
|
||||
state.config = Some(cfg.resolve_power(POWER_RANGE));
|
||||
status.sender().send(state.clone());
|
||||
responses.send(Response::Ok).await;
|
||||
}
|
||||
@@ -215,9 +217,9 @@ async fn handle_cmd(
|
||||
}
|
||||
Command::DisplayOn | Command::DisplayOff => {}
|
||||
Command::Transmit { config, payload } => {
|
||||
let tx_config = config.or(state.config);
|
||||
let tx_config = config.map(|c| c.resolve_power(POWER_RANGE)).or(state.config);
|
||||
if let Some(cfg) = tx_config {
|
||||
if let Err(reason) = cfg.validate() {
|
||||
if let Err(reason) = cfg.validate(POWER_RANGE) {
|
||||
warn!("TX config rejected: {}", reason);
|
||||
responses
|
||||
.send(Response::Error(ErrorCode::InvalidConfig))
|
||||
|
||||
Reference in new issue
Block a user