Created
July 4, 2025 13:48
-
-
Save juanqui/624ed77d0b9c5d10428fd6733aa23629 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Task - Implement a dynamic LoRa power tuning algorithm | |
| Your task is to implement a dynamic LoRa power tuning algoirithm that automatically determines the optimal LoRa radio transmit power to use in order to balance both reliable signal reception by the gateway and power effeciency of our sensor. | |
| ## Requirements | |
| - Our STM32WL5M is configured for for RFO_LP with a maximum power output of 15dBm. We should never exceed 15dBm. | |
| - The lowst possible power setting is 0dBm. | |
| - Power can be increased in increments of 1dBm. | |
| - At boot, we should start with a power of 8dBm. | |
| - We need to update ne0xlink protocol to make sure we have a Ping message that we can use to send test packets as part of our algorithm. | |
| - The implementation is in `lib/ne0xlink`. | |
| - We need to also update the design in `lib/ne0xlink/docs/design.md`. | |
| - The Ping packet should be as small as possible. | |
| - The Gateway will respond to the Ping with an ACK. | |
| - The ACK packet already contains the signal strength of our message as received by the Gateway. This is how we will know whether to increase or decrease our power. | |
| - We should strive to achieve an SNR of 2 with the Gateway. This value should be easily configurable in our software. | |
| - The high level logic for the algorithm is: | |
| - At boot we set our power to 8dBm and send one Ping packet. We will wait 100ms for an ACK response from the gateway. | |
| - If we receive an ACK response, then we look at the received signal strength. If the strength is within 2dBm of 2dBm then we keep the power level. | |
| - If the SNR as received by the gateway is < 0dBm then we increase our power by 1dBm | |
| - If the SNR as received by the gateway is > 4dBm then we decreasae our power by 1dBm | |
| - If we do not receive an ACK response, then we increase our power by 2dBm and try again. | |
| - Maximum power is 15dBm. If the gateway is still not responding at 15dBm then we back-off and try sending packets every 1 minute at 15dBm. | |
| - We wait 1 second between sending Ping messages. | |
| - We will not send any actual LoRa messages with sensor data until the initial power calibration is done. | |
| - After calibration, we need to send the LoRa messages in our queue that we did not send while calibrating or while the gateway was offline. | |
| - Even after calibration, we need to continously monitor the singla strength as received by the gateway for all messages we send (the SNR in the ACK packet from the gateway). | |
| - We should dynamically increase/decrease power as necessary. | |
| - We need to update our e-ink display UI (`fw/src/ui.rs` and the logic) to display the current transmit power next to the signal strength. | |
| - Ideally right after both signal strength indicators we can display a vertical pipe as a separator and then the power like "| 8 dbM". | |
| ## Design | |
| ### Overview | |
| The dynamic LoRa power tuning algorithm will automatically adjust transmit power based on feedback from the gateway, optimizing for both reliability and power efficiency. The system targets an SNR of 2dB at the gateway while operating within the 0-15dBm power range of the STM32WL5M's RFO_LP output. | |
| ### System Architecture | |
| #### 1. Protocol Enhancement (ne0xlink) | |
| **Ping Message Structure:** | |
| - Message type: New variant `Ping` in the protocol enum | |
| - Payload: Minimal - only device ID (4 bytes) | |
| - Total size: ~8 bytes including CBOR encoding overhead | |
| - Purpose: Power calibration and link quality monitoring | |
| **ACK Enhancement:** | |
| - Already contains SNR field from gateway reception | |
| - No modification needed to ACK structure | |
| - SNR field will drive power adjustment decisions | |
| #### 2. Power Management Module | |
| **State Machine:** | |
| ``` | |
| States: | |
| - Calibrating: Initial power discovery phase | |
| - Calibrated: Normal operation with dynamic adjustments | |
| - Backoff: Gateway unreachable, periodic retry mode | |
| Transitions: | |
| - Boot → Calibrating (start at 8dBm) | |
| - Calibrating → Calibrated (when SNR within target range) | |
| - Calibrating → Backoff (no response at 15dBm) | |
| - Backoff → Calibrating (retry every 60s) | |
| - Calibrated → Calibrating (connection lost) | |
| ``` | |
| **Power Controller:** | |
| - Current power level: u8 (0-15 dBm) | |
| - Target SNR: configurable constant (default: 2dB) | |
| - SNR tolerance: ±2dB window | |
| - Adjustment strategy: | |
| - Fine adjustment (±1dBm) when connected | |
| - Coarse adjustment (+2dBm) when connection lost | |
| - Bounds checking to stay within 0-15dBm range | |
| #### 3. Message Queue Management | |
| **Queue Behavior During Calibration:** | |
| - Sensor messages accumulate in existing message queue | |
| - Queue processing suspended until calibrated state | |
| - On calibration complete: flush all queued messages | |
| - Maximum queue size enforced to prevent memory issues | |
| **Priority Handling:** | |
| - Ping messages bypass normal queue | |
| - Sensor messages queued during calibration | |
| - ACK reception triggers immediate processing | |
| #### 4. Task Coordination | |
| **Power Tuning Task:** | |
| - Runs concurrently with existing LoRa TX/RX tasks | |
| - Coordinates via shared state (Mutex<PowerState>) | |
| - Timing: | |
| - Calibration phase: 1s between pings | |
| - Calibrated phase: piggyback on normal messages | |
| - Backoff phase: 60s between retry attempts | |
| **Integration with Existing Tasks:** | |
| - TX task checks calibration state before sending | |
| - RX task forwards ACKs to power controller | |
| - Power adjustments applied before each transmission | |
| ### Implementation Details | |
| #### 1. Data Structures | |
| ```rust | |
| #[derive(Clone, Copy)] | |
| pub struct PowerConfig { | |
| pub target_snr: i8, // Target SNR in dB (default: 2) | |
| pub snr_tolerance: u8, // Tolerance window in dB (default: 2) | |
| pub initial_power: u8, // Starting power in dBm (default: 8) | |
| pub max_power: u8, // Maximum power in dBm (default: 15) | |
| pub min_power: u8, // Minimum power in dBm (default: 0) | |
| } | |
| #[derive(Clone, Copy)] | |
| pub enum PowerState { | |
| Calibrating { current_power: u8, attempts: u8 }, | |
| Calibrated { current_power: u8 }, | |
| Backoff { last_attempt: Instant }, | |
| } | |
| pub struct PowerController { | |
| config: PowerConfig, | |
| state: PowerState, | |
| last_snr: Option<i8>, | |
| } | |
| ``` | |
| #### 2. Algorithm Implementation | |
| **Power Adjustment Logic:** | |
| ```rust | |
| fn adjust_power(&mut self, received_snr: Option<i8>) -> u8 { | |
| match (self.state, received_snr) { | |
| // ACK received during calibration | |
| (PowerState::Calibrating { current_power, .. }, Some(snr)) => { | |
| if (snr - self.config.target_snr).abs() <= self.config.snr_tolerance { | |
| // Within target range, transition to calibrated | |
| self.state = PowerState::Calibrated { current_power }; | |
| current_power | |
| } else if snr < self.config.target_snr - self.config.snr_tolerance { | |
| // Too weak, increase power | |
| let new_power = (current_power + 1).min(self.config.max_power); | |
| self.state = PowerState::Calibrating { current_power: new_power, attempts: 0 }; | |
| new_power | |
| } else { | |
| // Too strong, decrease power | |
| let new_power = current_power.saturating_sub(1); | |
| self.state = PowerState::Calibrating { current_power: new_power, attempts: 0 }; | |
| new_power | |
| } | |
| } | |
| // No ACK during calibration | |
| (PowerState::Calibrating { current_power, attempts }, None) => { | |
| if current_power >= self.config.max_power { | |
| // Already at max, enter backoff | |
| self.state = PowerState::Backoff { last_attempt: Instant::now() }; | |
| self.config.max_power | |
| } else { | |
| // Increase by 2dBm for faster convergence | |
| let new_power = (current_power + 2).min(self.config.max_power); | |
| self.state = PowerState::Calibrating { | |
| current_power: new_power, | |
| attempts: attempts + 1 | |
| }; | |
| new_power | |
| } | |
| } | |
| // Dynamic adjustment during normal operation | |
| (PowerState::Calibrated { current_power }, Some(snr)) => { | |
| if snr < 0 { | |
| // Signal too weak, increase power | |
| (current_power + 1).min(self.config.max_power) | |
| } else if snr > 4 { | |
| // Signal too strong, decrease power | |
| current_power.saturating_sub(1) | |
| } else { | |
| // Within acceptable range | |
| current_power | |
| } | |
| } | |
| // Connection lost during normal operation | |
| (PowerState::Calibrated { .. }, None) => { | |
| // Return to calibration | |
| self.state = PowerState::Calibrating { | |
| current_power: self.config.initial_power, | |
| attempts: 0 | |
| }; | |
| self.config.initial_power | |
| } | |
| _ => self.get_current_power() | |
| } | |
| } | |
| ``` | |
| #### 3. UI Integration | |
| **Display Format:** | |
| - Location: After signal strength indicators | |
| - Format: "| XX dBm" where XX is current transmit power | |
| - Update trigger: On power level change | |
| - Example: "📶 -85 dBm | 12 dBm" | |
| **Display Update Logic:** | |
| - Power level stored in shared state | |
| - UI task reads current power when updating display | |
| - Only refreshes e-ink when power changes to minimize updates | |
| ### Testing Strategy | |
| 1. **Unit Tests:** | |
| - Power adjustment algorithm with various SNR inputs | |
| - State machine transitions | |
| - Boundary conditions (0/15 dBm limits) | |
| 2. **Integration Tests:** | |
| - Message queue behavior during calibration | |
| - Task coordination and mutex handling | |
| - Protocol message encoding/decoding | |
| 3. **Field Testing:** | |
| - Various distances from gateway | |
| - Different environmental conditions | |
| - Power consumption measurements | |
| - Convergence time to optimal power | |
| ### Configuration | |
| **Compile-time Constants:** | |
| ```rust | |
| const DEFAULT_TARGET_SNR: i8 = 2; | |
| const DEFAULT_SNR_TOLERANCE: u8 = 2; | |
| const DEFAULT_INITIAL_POWER: u8 = 8; | |
| const CALIBRATION_INTERVAL_MS: u32 = 1000; | |
| const BACKOFF_RETRY_INTERVAL_S: u32 = 60; | |
| ``` | |
| **Runtime Configuration:** | |
| - Target SNR adjustable via PowerConfig struct | |
| - Could be exposed via future configuration interface | |
| - Stored in non-volatile memory if needed | |
| ### Performance Considerations | |
| 1. **Power Consumption:** | |
| - Calibration phase: ~10-20 seconds typical | |
| - Minimal overhead during normal operation | |
| - Backoff mode prevents battery drain | |
| 2. **Memory Usage:** | |
| - PowerController: ~16 bytes | |
| - No additional heap allocation | |
| - Reuses existing message queue | |
| 3. **Latency Impact:** | |
| - Initial delay for calibration | |
| - No impact on message latency post-calibration | |
| - ACK processing in existing RX path |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment