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:
11 files changed
+157
-20
No files matched your search
@@ -1,8 +1,4 @@
|
|||||||
- get the code better organized
|
|
||||||
- firmware file naming, no "release", add version
|
|
||||||
- LED blinking on TX/RX? colored?
|
- LED blinking on TX/RX? colored?
|
||||||
- display having board specific stuff in it
|
|
||||||
- publish new client, mux, bridge (updates in client for UART crap)
|
- publish new client, mux, bridge (updates in client for UART crap)
|
||||||
- try to get heltec v3 back to the right USB port+VID:PID
|
|
||||||
- split out mux and client repos
|
- split out mux and client repos
|
||||||
- move to GH organization
|
- move to GH organization
|
||||||
@@ -7,10 +7,13 @@
|
|||||||
//! For stock (unmodified) V3 boards, use the `heltec_v3_uart` feature instead.
|
//! For stock (unmodified) V3 boards, use the `heltec_v3_uart` feature instead.
|
||||||
|
|
||||||
use esp_hal::gpio::{Level, Output, OutputConfig};
|
use esp_hal::gpio::{Level, Output, OutputConfig};
|
||||||
|
use esp_hal::rmt::{Rmt, TxChannelConfig, TxChannelCreator};
|
||||||
|
use esp_hal::time::Rate;
|
||||||
use static_cell::StaticCell;
|
use static_cell::StaticCell;
|
||||||
|
|
||||||
use super::esp32s3;
|
use super::esp32s3;
|
||||||
use super::traits::{BoardParts, LoRaBoard};
|
use super::traits::{BoardParts, LoRaBoard};
|
||||||
|
use crate::driver::ws2812::Ws2812;
|
||||||
use crate::hal::esp32s3 as mcu;
|
use crate::hal::esp32s3 as mcu;
|
||||||
|
|
||||||
pub use super::esp32s3::{
|
pub use super::esp32s3::{
|
||||||
@@ -18,6 +21,7 @@ pub use super::esp32s3::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub type UsbDriver = esp_hal::otg_fs::asynch::Driver<'static>;
|
pub type UsbDriver = esp_hal::otg_fs::asynch::Driver<'static>;
|
||||||
|
pub type LedDriver = Ws2812;
|
||||||
|
|
||||||
pub struct UsbParts {
|
pub struct UsbParts {
|
||||||
pub driver: UsbDriver,
|
pub driver: UsbDriver,
|
||||||
@@ -35,6 +39,7 @@ impl LoRaBoard for Board {
|
|||||||
type CommParts = UsbParts;
|
type CommParts = UsbParts;
|
||||||
type DisplayParts = DisplayParts;
|
type DisplayParts = DisplayParts;
|
||||||
type DisplayDriver = DisplayDriver;
|
type DisplayDriver = DisplayDriver;
|
||||||
|
type LedDriver = Ws2812;
|
||||||
|
|
||||||
fn init() -> Self {
|
fn init() -> Self {
|
||||||
let p = esp_hal::init(esp_hal::Config::default());
|
let p = esp_hal::init(esp_hal::Config::default());
|
||||||
@@ -45,7 +50,7 @@ impl LoRaBoard for Board {
|
|||||||
mcu::mac_address()
|
mcu::mac_address()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn into_parts(self) -> BoardParts<RadioParts, UsbParts, DisplayParts> {
|
fn into_parts(self) -> BoardParts<RadioParts, UsbParts, DisplayParts, Ws2812> {
|
||||||
let p = self.p;
|
let p = self.p;
|
||||||
|
|
||||||
mcu::start_timer(p.TIMG0);
|
mcu::start_timer(p.TIMG0);
|
||||||
@@ -79,10 +84,20 @@ impl LoRaBoard for Board {
|
|||||||
let i2c = mcu::init_i2c(p.I2C0, p.GPIO17, p.GPIO18);
|
let i2c = mcu::init_i2c(p.I2C0, p.GPIO17, p.GPIO18);
|
||||||
let display = Some(DisplayParts { i2c });
|
let display = Some(DisplayParts { i2c });
|
||||||
|
|
||||||
|
// WS2812B RGB LED on GPIO38 via RMT channel 0
|
||||||
|
let rmt = Rmt::new(p.RMT, Rate::from_mhz(80))
|
||||||
|
.expect("RMT init")
|
||||||
|
.into_async();
|
||||||
|
let led_channel = rmt.channel0
|
||||||
|
.configure_tx(p.GPIO38, TxChannelConfig::default().with_clk_divider(1).with_idle_output(true))
|
||||||
|
.expect("RMT TX channel init");
|
||||||
|
let led = Some(Ws2812::new(led_channel));
|
||||||
|
|
||||||
BoardParts {
|
BoardParts {
|
||||||
radio,
|
radio,
|
||||||
host,
|
host,
|
||||||
display,
|
display,
|
||||||
|
led,
|
||||||
mac: Self::mac_address(),
|
mac: Self::mac_address(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,12 @@
|
|||||||
//! use the `heltec_v3` feature instead for native USB CDC-ACM support.
|
//! use the `heltec_v3` feature instead for native USB CDC-ACM support.
|
||||||
|
|
||||||
use esp_hal::gpio::{Level, Output, OutputConfig};
|
use esp_hal::gpio::{Level, Output, OutputConfig};
|
||||||
|
use esp_hal::rmt::{Rmt, TxChannelConfig, TxChannelCreator};
|
||||||
|
use esp_hal::time::Rate;
|
||||||
|
|
||||||
use super::esp32s3;
|
use super::esp32s3;
|
||||||
use super::traits::{BoardParts, LoRaBoard};
|
use super::traits::{BoardParts, LoRaBoard};
|
||||||
|
use crate::driver::ws2812::Ws2812;
|
||||||
use crate::hal::esp32s3 as mcu;
|
use crate::hal::esp32s3 as mcu;
|
||||||
|
|
||||||
pub use super::esp32s3::{
|
pub use super::esp32s3::{
|
||||||
@@ -17,6 +20,7 @@ pub use super::esp32s3::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub type UartDriver = esp_hal::uart::Uart<'static, esp_hal::Async>;
|
pub type UartDriver = esp_hal::uart::Uart<'static, esp_hal::Async>;
|
||||||
|
pub type LedDriver = Ws2812;
|
||||||
|
|
||||||
pub struct UartParts {
|
pub struct UartParts {
|
||||||
pub driver: UartDriver,
|
pub driver: UartDriver,
|
||||||
@@ -34,6 +38,7 @@ impl LoRaBoard for Board {
|
|||||||
type CommParts = UartParts;
|
type CommParts = UartParts;
|
||||||
type DisplayParts = DisplayParts;
|
type DisplayParts = DisplayParts;
|
||||||
type DisplayDriver = DisplayDriver;
|
type DisplayDriver = DisplayDriver;
|
||||||
|
type LedDriver = Ws2812;
|
||||||
|
|
||||||
fn init() -> Self {
|
fn init() -> Self {
|
||||||
let p = esp_hal::init(esp_hal::Config::default());
|
let p = esp_hal::init(esp_hal::Config::default());
|
||||||
@@ -44,7 +49,7 @@ impl LoRaBoard for Board {
|
|||||||
mcu::mac_address()
|
mcu::mac_address()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn into_parts(self) -> BoardParts<RadioParts, UartParts, DisplayParts> {
|
fn into_parts(self) -> BoardParts<RadioParts, UartParts, DisplayParts, Ws2812> {
|
||||||
let p = self.p;
|
let p = self.p;
|
||||||
|
|
||||||
mcu::start_timer(p.TIMG0);
|
mcu::start_timer(p.TIMG0);
|
||||||
@@ -75,10 +80,20 @@ impl LoRaBoard for Board {
|
|||||||
let i2c = mcu::init_i2c(p.I2C0, p.GPIO17, p.GPIO18);
|
let i2c = mcu::init_i2c(p.I2C0, p.GPIO17, p.GPIO18);
|
||||||
let display = Some(DisplayParts { i2c });
|
let display = Some(DisplayParts { i2c });
|
||||||
|
|
||||||
|
// WS2812B RGB LED on GPIO38 via RMT channel 0
|
||||||
|
let rmt = Rmt::new(p.RMT, Rate::from_mhz(80))
|
||||||
|
.expect("RMT init")
|
||||||
|
.into_async();
|
||||||
|
let led_channel = rmt.channel0
|
||||||
|
.configure_tx(p.GPIO38, TxChannelConfig::default().with_clk_divider(1).with_idle_output(true))
|
||||||
|
.expect("RMT TX channel init");
|
||||||
|
let led = Some(Ws2812::new(led_channel));
|
||||||
|
|
||||||
BoardParts {
|
BoardParts {
|
||||||
radio,
|
radio,
|
||||||
host,
|
host,
|
||||||
display,
|
display,
|
||||||
|
led,
|
||||||
mac: Self::mac_address(),
|
mac: Self::mac_address(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
use esp_hal::gpio::{Level, Output, OutputConfig};
|
use esp_hal::gpio::{Level, Output, OutputConfig};
|
||||||
|
use esp_hal::rmt::{Rmt, TxChannelConfig, TxChannelCreator};
|
||||||
|
use esp_hal::time::Rate;
|
||||||
use static_cell::StaticCell;
|
use static_cell::StaticCell;
|
||||||
|
|
||||||
use super::esp32s3;
|
use super::esp32s3;
|
||||||
use super::traits::{BoardParts, LoRaBoard};
|
use super::traits::{BoardParts, LoRaBoard};
|
||||||
|
use crate::driver::ws2812::Ws2812;
|
||||||
use crate::hal::esp32s3 as mcu;
|
use crate::hal::esp32s3 as mcu;
|
||||||
|
|
||||||
pub use super::esp32s3::{
|
pub use super::esp32s3::{
|
||||||
@@ -11,6 +14,8 @@ pub use super::esp32s3::{
|
|||||||
|
|
||||||
pub type UsbDriver = esp_hal::otg_fs::asynch::Driver<'static>;
|
pub type UsbDriver = esp_hal::otg_fs::asynch::Driver<'static>;
|
||||||
|
|
||||||
|
pub type LedDriver = Ws2812;
|
||||||
|
|
||||||
pub struct UsbParts {
|
pub struct UsbParts {
|
||||||
pub driver: UsbDriver,
|
pub driver: UsbDriver,
|
||||||
}
|
}
|
||||||
@@ -27,6 +32,7 @@ impl LoRaBoard for Board {
|
|||||||
type CommParts = UsbParts;
|
type CommParts = UsbParts;
|
||||||
type DisplayParts = DisplayParts;
|
type DisplayParts = DisplayParts;
|
||||||
type DisplayDriver = DisplayDriver;
|
type DisplayDriver = DisplayDriver;
|
||||||
|
type LedDriver = Ws2812;
|
||||||
|
|
||||||
fn init() -> Self {
|
fn init() -> Self {
|
||||||
let p = esp_hal::init(esp_hal::Config::default());
|
let p = esp_hal::init(esp_hal::Config::default());
|
||||||
@@ -37,7 +43,7 @@ impl LoRaBoard for Board {
|
|||||||
mcu::mac_address()
|
mcu::mac_address()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn into_parts(self) -> BoardParts<RadioParts, UsbParts, DisplayParts> {
|
fn into_parts(self) -> BoardParts<RadioParts, UsbParts, DisplayParts, Ws2812> {
|
||||||
let p = self.p;
|
let p = self.p;
|
||||||
|
|
||||||
mcu::start_timer(p.TIMG0);
|
mcu::start_timer(p.TIMG0);
|
||||||
@@ -71,10 +77,25 @@ impl LoRaBoard for Board {
|
|||||||
let i2c = mcu::init_i2c(p.I2C0, p.GPIO17, p.GPIO18);
|
let i2c = mcu::init_i2c(p.I2C0, p.GPIO17, p.GPIO18);
|
||||||
let display = Some(DisplayParts { i2c });
|
let display = Some(DisplayParts { i2c });
|
||||||
|
|
||||||
|
// WS2812B RGB LED on GPIO38 via RMT channel 0
|
||||||
|
let rmt = Rmt::new(p.RMT, Rate::from_mhz(80))
|
||||||
|
.expect("RMT init")
|
||||||
|
.into_async();
|
||||||
|
let led_channel = rmt.channel0
|
||||||
|
.configure_tx(
|
||||||
|
p.GPIO38,
|
||||||
|
TxChannelConfig::default()
|
||||||
|
.with_clk_divider(1)
|
||||||
|
.with_idle_output(true),
|
||||||
|
)
|
||||||
|
.expect("RMT TX channel init");
|
||||||
|
let led = Some(Ws2812::new(led_channel));
|
||||||
|
|
||||||
BoardParts {
|
BoardParts {
|
||||||
radio,
|
radio,
|
||||||
host,
|
host,
|
||||||
display,
|
display,
|
||||||
|
led,
|
||||||
mac: Self::mac_address(),
|
mac: Self::mac_address(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ type RadioSpiDevice = SpiDevice<'static, NoopRawMutex, mcu::SpiBus, Nss>;
|
|||||||
pub type RadioDriver = Sx126x<RadioSpiDevice, Iv, Sx1262>;
|
pub type RadioDriver = Sx126x<RadioSpiDevice, Iv, Sx1262>;
|
||||||
pub type UsbDriver = mcu::UsbNrfDriver;
|
pub type UsbDriver = mcu::UsbNrfDriver;
|
||||||
pub type DisplayI2c = mcu::I2cBus;
|
pub type DisplayI2c = mcu::I2cBus;
|
||||||
|
pub type LedDriver = ();
|
||||||
|
|
||||||
pub type DisplayDriver = ssd1306::Ssd1306Async<
|
pub type DisplayDriver = ssd1306::Ssd1306Async<
|
||||||
ssd1306::prelude::I2CInterface<DisplayI2c>,
|
ssd1306::prelude::I2CInterface<DisplayI2c>,
|
||||||
@@ -77,6 +78,7 @@ impl LoRaBoard for Board {
|
|||||||
type CommParts = UsbParts;
|
type CommParts = UsbParts;
|
||||||
type DisplayParts = DisplayParts;
|
type DisplayParts = DisplayParts;
|
||||||
type DisplayDriver = DisplayDriver;
|
type DisplayDriver = DisplayDriver;
|
||||||
|
type LedDriver = ();
|
||||||
|
|
||||||
fn init() -> Self {
|
fn init() -> Self {
|
||||||
let p = embassy_nrf::init(Default::default());
|
let p = embassy_nrf::init(Default::default());
|
||||||
@@ -87,7 +89,7 @@ impl LoRaBoard for Board {
|
|||||||
mcu::mac_address()
|
mcu::mac_address()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn into_parts(self) -> BoardParts<RadioParts, UsbParts, DisplayParts> {
|
fn into_parts(self) -> BoardParts<RadioParts, UsbParts, DisplayParts, ()> {
|
||||||
let p = self.p;
|
let p = self.p;
|
||||||
|
|
||||||
// ── SPI bus for SX1262 ──────────────────────────────────
|
// ── SPI bus for SX1262 ──────────────────────────────────
|
||||||
@@ -135,6 +137,7 @@ impl LoRaBoard for Board {
|
|||||||
radio,
|
radio,
|
||||||
host,
|
host,
|
||||||
display,
|
display,
|
||||||
|
led: None,
|
||||||
mac: Self::mac_address(),
|
mac: Self::mac_address(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,23 @@
|
|||||||
//! Board abstraction trait.
|
//! Board abstraction trait.
|
||||||
//!
|
//!
|
||||||
//! Every board must implement [`LoRaBoard`] with concrete associated types
|
//! Every board must implement [`LoRaBoard`] with concrete associated types
|
||||||
//! for its radio, host communication, and display peripherals. The compiler
|
//! for its radio, host communication, display, and LED peripherals. The
|
||||||
//! enforces completeness — missing types or methods are compile errors.
|
//! compiler enforces completeness — missing types or methods are compile errors.
|
||||||
//!
|
//!
|
||||||
//! See `src/board/PORTING.md` for a step-by-step guide to adding a new board.
|
//! See `src/board/PORTING.md` for a step-by-step guide to adding a new board.
|
||||||
|
|
||||||
use embedded_graphics::draw_target::DrawTarget;
|
use embedded_graphics::draw_target::DrawTarget;
|
||||||
use embedded_graphics::pixelcolor::BinaryColor;
|
use embedded_graphics::pixelcolor::BinaryColor;
|
||||||
|
|
||||||
|
/// Async RGB LED control. Boards without an LED use `()` which is a no-op.
|
||||||
|
pub trait RgbLed {
|
||||||
|
fn set_rgb(&mut self, r: u8, g: u8, b: u8) -> impl core::future::Future<Output = ()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RgbLed for () {
|
||||||
|
async fn set_rgb(&mut self, _r: u8, _g: u8, _b: u8) {}
|
||||||
|
}
|
||||||
|
|
||||||
/// Compile-time contract for a DongLoRa board.
|
/// Compile-time contract for a DongLoRa board.
|
||||||
pub trait LoRaBoard: Sized {
|
pub trait LoRaBoard: Sized {
|
||||||
/// Human-readable board name (shown on display splash screen).
|
/// Human-readable board name (shown on display splash screen).
|
||||||
@@ -29,6 +38,9 @@ pub trait LoRaBoard: Sized {
|
|||||||
/// Concrete display driver type (must implement DrawTarget for rendering).
|
/// Concrete display driver type (must implement DrawTarget for rendering).
|
||||||
type DisplayDriver: DrawTarget<Color = BinaryColor>;
|
type DisplayDriver: DrawTarget<Color = BinaryColor>;
|
||||||
|
|
||||||
|
/// RGB LED driver. Boards without an LED use `()`.
|
||||||
|
type LedDriver: RgbLed;
|
||||||
|
|
||||||
/// Initialize the board hardware.
|
/// Initialize the board hardware.
|
||||||
fn init() -> Self;
|
fn init() -> Self;
|
||||||
|
|
||||||
@@ -36,13 +48,14 @@ pub trait LoRaBoard: Sized {
|
|||||||
fn mac_address() -> [u8; 6];
|
fn mac_address() -> [u8; 6];
|
||||||
|
|
||||||
/// Decompose initialized board into peripheral bundles for each task.
|
/// Decompose initialized board into peripheral bundles for each task.
|
||||||
fn into_parts(self) -> BoardParts<Self::RadioParts, Self::CommParts, Self::DisplayParts>;
|
fn into_parts(self) -> BoardParts<Self::RadioParts, Self::CommParts, Self::DisplayParts, Self::LedDriver>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Peripheral bundles for each firmware task, produced by [`LoRaBoard::into_parts`].
|
/// Peripheral bundles for each firmware task, produced by [`LoRaBoard::into_parts`].
|
||||||
pub struct BoardParts<R, C, D> {
|
pub struct BoardParts<R, C, D, L> {
|
||||||
pub radio: R,
|
pub radio: R,
|
||||||
pub host: C,
|
pub host: C,
|
||||||
pub display: Option<D>,
|
pub display: Option<D>,
|
||||||
|
pub led: Option<L>,
|
||||||
pub mac: [u8; 6],
|
pub mac: [u8; 6],
|
||||||
}
|
}
|
||||||
@@ -20,6 +20,7 @@ type RadioSpiDevice = SpiDevice<'static, NoopRawMutex, mcu::SpiBus, Nss>;
|
|||||||
pub type RadioDriver = Sx126x<RadioSpiDevice, Iv, Sx1262>;
|
pub type RadioDriver = Sx126x<RadioSpiDevice, Iv, Sx1262>;
|
||||||
pub type UsbDriver = mcu::UsbNrfDriver;
|
pub type UsbDriver = mcu::UsbNrfDriver;
|
||||||
pub type DisplayI2c = mcu::I2cBus;
|
pub type DisplayI2c = mcu::I2cBus;
|
||||||
|
pub type LedDriver = ();
|
||||||
|
|
||||||
pub type DisplayDriver = crate::driver::sh1106::Sh1106<DisplayI2c>;
|
pub type DisplayDriver = crate::driver::sh1106::Sh1106<DisplayI2c>;
|
||||||
|
|
||||||
@@ -66,6 +67,7 @@ impl LoRaBoard for Board {
|
|||||||
type CommParts = UsbParts;
|
type CommParts = UsbParts;
|
||||||
type DisplayParts = DisplayParts;
|
type DisplayParts = DisplayParts;
|
||||||
type DisplayDriver = DisplayDriver;
|
type DisplayDriver = DisplayDriver;
|
||||||
|
type LedDriver = ();
|
||||||
|
|
||||||
fn init() -> Self {
|
fn init() -> Self {
|
||||||
let mut config = embassy_nrf::config::Config::default();
|
let mut config = embassy_nrf::config::Config::default();
|
||||||
@@ -79,7 +81,7 @@ impl LoRaBoard for Board {
|
|||||||
mcu::mac_address()
|
mcu::mac_address()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn into_parts(self) -> BoardParts<RadioParts, UsbParts, DisplayParts> {
|
fn into_parts(self) -> BoardParts<RadioParts, UsbParts, DisplayParts, ()> {
|
||||||
let p = self.p;
|
let p = self.p;
|
||||||
|
|
||||||
// ── SPI bus for SX1262 ──────────────────────────────────
|
// ── SPI bus for SX1262 ──────────────────────────────────
|
||||||
@@ -133,6 +135,7 @@ impl LoRaBoard for Board {
|
|||||||
radio,
|
radio,
|
||||||
host,
|
host,
|
||||||
display,
|
display,
|
||||||
|
led: None,
|
||||||
mac: Self::mac_address(),
|
mac: Self::mac_address(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-6
@@ -6,11 +6,9 @@ use embassy_time::Timer;
|
|||||||
use embedded_graphics::draw_target::DrawTarget;
|
use embedded_graphics::draw_target::DrawTarget;
|
||||||
use embedded_graphics::pixelcolor::BinaryColor;
|
use embedded_graphics::pixelcolor::BinaryColor;
|
||||||
|
|
||||||
use crate::board::DisplayParts;
|
use crate::board::{Board, DisplayParts, LedDriver, LoRaBoard, RgbLed};
|
||||||
use crate::channel::{DisplayCommand, DisplayCommandChannel, RadioState, RadioStatus, StatusWatch};
|
use crate::channel::{DisplayCommand, DisplayCommandChannel, RadioState, RadioStatus, StatusWatch};
|
||||||
|
|
||||||
use crate::board::{Board, LoRaBoard};
|
|
||||||
|
|
||||||
use render::RSSI_HISTORY_LEN;
|
use render::RSSI_HISTORY_LEN;
|
||||||
|
|
||||||
const BOARD_NAME: &str = Board::NAME;
|
const BOARD_NAME: &str = Board::NAME;
|
||||||
@@ -103,6 +101,7 @@ fn render_current(
|
|||||||
#[task]
|
#[task]
|
||||||
pub async fn display_task(
|
pub async fn display_task(
|
||||||
parts: DisplayParts,
|
parts: DisplayParts,
|
||||||
|
mut led: Option<LedDriver>,
|
||||||
status: &'static StatusWatch,
|
status: &'static StatusWatch,
|
||||||
display_commands: &'static DisplayCommandChannel,
|
display_commands: &'static DisplayCommandChannel,
|
||||||
) {
|
) {
|
||||||
@@ -150,12 +149,15 @@ pub async fn display_task(
|
|||||||
if state.disconnected {
|
if state.disconnected {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
let rx_packet = radio_status.rx_count != state.last_status.rx_count;
|
||||||
|
let tx_packet = radio_status.tx_count != state.last_status.tx_count;
|
||||||
|
|
||||||
if let Some(rssi) = radio_status.last_rssi {
|
if let Some(rssi) = radio_status.last_rssi {
|
||||||
if radio_status.rx_count != state.last_status.rx_count {
|
if rx_packet {
|
||||||
state.record_rssi(rssi);
|
state.record_rssi(rssi);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if radio_status.tx_count != state.last_status.tx_count {
|
if tx_packet {
|
||||||
state.record_tx();
|
state.record_tx();
|
||||||
}
|
}
|
||||||
state.last_status = radio_status;
|
state.last_status = radio_status;
|
||||||
@@ -163,6 +165,15 @@ pub async fn display_task(
|
|||||||
if state.display_on {
|
if state.display_on {
|
||||||
render_current(&mut display, &state, &board_info);
|
render_current(&mut display, &state, &board_info);
|
||||||
let _ = display.flush().await;
|
let _ = display.flush().await;
|
||||||
|
|
||||||
|
// Blink LED: green on RX, red on TX
|
||||||
|
if let Some(ref mut led) = led {
|
||||||
|
if tx_packet {
|
||||||
|
led.set_rgb(32, 0, 0).await;
|
||||||
|
} else if rx_packet {
|
||||||
|
led.set_rgb(0, 32, 0).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Either3::Second(cmd) => match cmd {
|
Either3::Second(cmd) => match cmd {
|
||||||
@@ -171,6 +182,9 @@ pub async fn display_task(
|
|||||||
state.display_on = false;
|
state.display_on = false;
|
||||||
render::blank(&mut display);
|
render::blank(&mut display);
|
||||||
let _ = display.flush().await;
|
let _ = display.flush().await;
|
||||||
|
if let Some(ref mut led) = led {
|
||||||
|
led.set_rgb(0, 0, 0).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
DisplayCommand::On => {
|
DisplayCommand::On => {
|
||||||
state.disconnected = false;
|
state.disconnected = false;
|
||||||
@@ -185,11 +199,17 @@ pub async fn display_task(
|
|||||||
state = DisplayState::new();
|
state = DisplayState::new();
|
||||||
render::splash(&mut display, &board_info);
|
render::splash(&mut display, &board_info);
|
||||||
let _ = display.flush().await;
|
let _ = display.flush().await;
|
||||||
|
if let Some(ref mut led) = led {
|
||||||
|
led.set_rgb(0, 0, 0).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Either3::Third(()) => {
|
Either3::Third(()) => {
|
||||||
// Timer tick: advance sparkline slot
|
// Timer tick: advance sparkline slot, turn off LED blink
|
||||||
state.advance_slot();
|
state.advance_slot();
|
||||||
|
if let Some(ref mut led) = led {
|
||||||
|
led.set_rgb(0, 0, 0).await;
|
||||||
|
}
|
||||||
if state.display_on && state.is_active() {
|
if state.display_on && state.is_active() {
|
||||||
render_current(&mut display, &state, &board_info);
|
render_current(&mut display, &state, &board_info);
|
||||||
let _ = display.flush().await;
|
let _ = display.flush().await;
|
||||||
|
|||||||
@@ -1,2 +1,5 @@
|
|||||||
#[cfg(feature = "wio_tracker_l1")]
|
#[cfg(feature = "wio_tracker_l1")]
|
||||||
pub mod sh1106;
|
pub mod sh1106;
|
||||||
|
|
||||||
|
#[cfg(any(feature = "heltec_v3", feature = "heltec_v3_uart", feature = "heltec_v4"))]
|
||||||
|
pub mod ws2812;
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -96,7 +96,7 @@ async fn run(spawner: Spawner) {
|
|||||||
|
|
||||||
if let Some(dp) = parts.display {
|
if let Some(dp) = parts.display {
|
||||||
spawner
|
spawner
|
||||||
.spawn(display::display_task(dp, &STATUS, &DISPLAY_COMMANDS))
|
.spawn(display::display_task(dp, parts.led, &STATUS, &DISPLAY_COMMANDS))
|
||||||
.expect("spawn display_task");
|
.expect("spawn display_task");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user