feat: RGB LED support — blinks green on RX, red on TX

- Added RgbLed trait with impl on () for boards without LED
- Added WS2812 driver (driver/ws2812.rs) using ESP32-S3 RMT peripheral
  - 80MHz clock, 12.5ns resolution, 24-bit GRB encoding
  - Zero extra crate dependencies
- Heltec V3/V3-UART/V4: WS2812B on GPIO38 via RMT channel 0
- RAK WisBlock/Wio Tracker: type LedDriver = (), led = None (no-op)
- Display task: green blink on RX packet, red blink on TX packet
- LED auto-off on timer tick (~1s), off when display is off/reset
- LoRaBoard trait gains type LedDriver: RgbLed associated type
- BoardParts gains led: Option<L> field
This commit is contained in:
swaits committed 2026-04-07 11:18:30 -06:00
1 parent 2b9e236f32
commit 5ac4b73739
11 files changed
+157 -20

No files matched your search

+3
View File
@@ -1,2 +1,5 @@
#[cfg(feature = "wio_tracker_l1")]
pub mod sh1106;
#[cfg(any(feature = "heltec_v3", feature = "heltec_v3_uart", feature = "heltec_v4"))]
pub mod ws2812;
+48
View File
@@ -0,0 +1,48 @@
//! WS2812B single-LED driver using the ESP32-S3 RMT peripheral.
//!
//! Encodes RGB values as GRB PulseCodes and transmits via RMT.
//! At 80 MHz with divider=1, each tick is 12.5 ns.
use esp_hal::gpio::Level;
use esp_hal::rmt::{Channel, PulseCode, Tx};
use crate::board::RgbLed;
/// WS2812 0-bit: High 400ns (32 ticks), Low 850ns (68 ticks).
const BIT_0: PulseCode = PulseCode::new(Level::High, 32, Level::Low, 68);
/// WS2812 1-bit: High 800ns (64 ticks), Low 450ns (36 ticks).
const BIT_1: PulseCode = PulseCode::new(Level::High, 64, Level::Low, 36);
/// WS2812B single-LED driver over RMT.
pub struct Ws2812 {
channel: Channel<'static, esp_hal::Async, Tx>,
}
impl Ws2812 {
pub fn new(channel: Channel<'static, esp_hal::Async, Tx>) -> Self {
Self { channel }
}
/// Encode 3 bytes (GRB order) + end marker into PulseCodes.
fn encode(r: u8, g: u8, b: u8) -> [PulseCode; 25] {
let mut data = [PulseCode::end_marker(); 25];
let bytes = [g, r, b]; // WS2812 expects GRB order
let mut idx = 0;
for byte in bytes {
for bit in (0..8).rev() {
data[idx] = if byte & (1 << bit) != 0 { BIT_1 } else { BIT_0 };
idx += 1;
}
}
// data[24] is already end_marker
data
}
}
impl RgbLed for Ws2812 {
async fn set_rgb(&mut self, r: u8, g: u8, b: u8) {
let data = Self::encode(r, g, b);
let _ = self.channel.transmit(&data).await;
}
}