feat(display): OLED dims a few seconds after last activity
Splash holds bright for 10 s on (re-)entry, then drops to dim while LearnMore/Info keep alternating. The dashboard stays bright for 3 s after the last packet or mode change, then dims in place without disturbing the Listening/Transmitting state or the 1 s sparkline timer. Cross-leaf TX↔RX hops stay bright throughout — `set_dim` is fired by the 3 s timer handler as an action rather than as a sibling `exit:` action, so mode-change leaf-crossings no longer flash dim→bright on every swap. Two driver families (`ssd1306::Ssd1306Async` and the in-tree `Sh1106`) wrapped behind a single `DisplayBrightness` trait in `src/driver/mod.rs`; the board trait's `DisplayDriver` associated type now requires it. Dim/bright contrast values centralized as `CONTRAST_BRIGHT` / `CONTRAST_DIM` constants — tune in one place. `On.on(CmdReset)` now targets `LearnMore` directly rather than the ancestor `Splash`. Under hsmc 0.2 up-transition semantics, targeting an already-active ancestor unwinds only below it without re-descent; the USB-disconnect reset path was previously relying on the old re-enter-and-descend behavior and would have parked at `Splash` with a dead alternation timer. Bumps `hsmc` 0.1.0 → 0.2 for the up-transition semantic fix.
This commit is contained in:
6 files changed
+185
-14
No files matched your search
+5
-2
@@ -9,6 +9,8 @@
|
||||
use embedded_graphics::draw_target::DrawTarget;
|
||||
use embedded_graphics::pixelcolor::BinaryColor;
|
||||
|
||||
use crate::driver::DisplayBrightness;
|
||||
|
||||
/// 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 = ()>;
|
||||
@@ -35,8 +37,9 @@ pub trait LoRaBoard: Sized {
|
||||
/// Display peripheral bundle (I2C bus for display init).
|
||||
type DisplayParts;
|
||||
|
||||
/// Concrete display driver type (must implement DrawTarget for rendering).
|
||||
type DisplayDriver: DrawTarget<Color = BinaryColor>;
|
||||
/// Concrete display driver type (must implement DrawTarget for rendering
|
||||
/// and DisplayBrightness for the two-level dim control).
|
||||
type DisplayDriver: DrawTarget<Color = BinaryColor> + DisplayBrightness;
|
||||
|
||||
/// RGB LED driver. Boards without an LED use `()`.
|
||||
type LedDriver: RgbLed;
|
||||
|
||||
+66
-7
@@ -37,6 +37,7 @@ use embassy_time::Timer;
|
||||
use hsmc::statechart;
|
||||
|
||||
use crate::board::{self, Board, DisplayDriver, DisplayParts, LedDriver, LoRaBoard, RgbLed};
|
||||
use crate::driver::DisplayBrightness;
|
||||
use crate::channel::{DisplayCommand, DisplayCommandChannel, RadioEvent, RadioEventChannel};
|
||||
use crate::protocol::LoRaConfig;
|
||||
|
||||
@@ -220,6 +221,14 @@ impl DisplayContext {
|
||||
Timer::after_millis(50).await;
|
||||
self.peripherals.led.set_rgb(0, 0, 0).await;
|
||||
}
|
||||
|
||||
async fn oled_bright(&mut self) {
|
||||
self.peripherals.display.set_bright().await;
|
||||
}
|
||||
|
||||
async fn oled_dim(&mut self) {
|
||||
self.peripherals.display.set_dim().await;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Statechart ─────────────────────────────────────────────────────
|
||||
@@ -243,16 +252,26 @@ statechart! {
|
||||
on(CmdOff) => Off;
|
||||
// `CmdReset` + `RadioIdle` race on USB disconnect; this handler
|
||||
// makes either ordering land in the same place. Reset the run
|
||||
// state, turn off any blink-in-progress, and re-enter Splash —
|
||||
// descend_defaults lands us in LearnMore with a fresh 5 s timer.
|
||||
on(CmdReset) => reset_run_state, turn_led_off, Splash;
|
||||
// state, turn off any blink-in-progress, and land in LearnMore
|
||||
// with a fresh 5 s timer. We target `LearnMore` (not `Splash`)
|
||||
// because under hsmc 0.2 up-transition semantics, targeting a
|
||||
// state that's already on the active path would only unwind
|
||||
// below it — we'd park at `Splash` with no active child and
|
||||
// the alternation timer would never restart.
|
||||
on(CmdReset) => reset_run_state, turn_led_off, LearnMore;
|
||||
// Config can arrive before the dashboard is up; cache it anywhere.
|
||||
on(ConfigChanged(cfg: LoRaConfig)) => save_new_config;
|
||||
|
||||
// Splash is bright for the first 10 s of (re-)entry, then the
|
||||
// action-only `on(after 10s)` handler drops the OLED to dim.
|
||||
// LearnMore ↔ Info keep alternating underneath — their own
|
||||
// timers are independent of the 10 s window.
|
||||
state Splash {
|
||||
entry: set_bright;
|
||||
default(LearnMore);
|
||||
on(EnteredRx) => Listening;
|
||||
on(EnteredTx) => Transmitting;
|
||||
on(after Duration::from_secs(10)) => set_dim;
|
||||
|
||||
state LearnMore {
|
||||
entry: paint_learn_more;
|
||||
@@ -265,24 +284,56 @@ statechart! {
|
||||
}
|
||||
}
|
||||
|
||||
// Dashboard brightness pattern: each mode-leaf holds a `Bright`
|
||||
// substate. `entry: set_bright` owns the backlight going on;
|
||||
// `set_dim` is fired **as an action** by the 3 s timeout
|
||||
// handler, immediately before the up-transition that parks
|
||||
// the machine at the leaf. The symmetric `exit: set_dim`
|
||||
// would also fire on mode-change leaf-crossings (e.g.
|
||||
// ListeningBright → TransmittingBright via EnteredTx), and
|
||||
// the subsequent default descent would fire `set_bright`
|
||||
// again — producing a visible dim/bright flash on every
|
||||
// TX↔RX swap. Anchoring dim to the *timer* handler instead
|
||||
// means leaf-crossings go bright→bright with no intermediate
|
||||
// dim action. CmdOff/CmdReset exit paths don't fire set_dim
|
||||
// either, which is fine — the display is blanked or walked
|
||||
// back to Splash, which re-asserts its own brightness.
|
||||
state Dashboard {
|
||||
default(Listening);
|
||||
on(RadioIdle) => Splash;
|
||||
|
||||
// Packet events are pure data + one-shot LED feedback.
|
||||
on(PacketRx { rssi: i16, snr: Option<i16> }) => record_rx_packet, blink_rx_led;
|
||||
on(PacketTx) => record_tx_packet, blink_tx_led;
|
||||
|
||||
state Listening {
|
||||
entry: paint_listening;
|
||||
default(ListeningBright);
|
||||
on(EnteredTx) => Transmitting;
|
||||
on(every Duration::from_millis(1000)) => advance_sparkline, paint_listening;
|
||||
// Split from the transition below because hsmc rejects
|
||||
// payload bindings on transition-bearing handlers.
|
||||
on(PacketRx { rssi: i16, snr: Option<i16> }) => record_rx_packet, blink_rx_led;
|
||||
on(PacketRx) => ListeningBright;
|
||||
on(PacketTx) => record_tx_packet, blink_tx_led;
|
||||
on(PacketTx) => ListeningBright;
|
||||
|
||||
state ListeningBright {
|
||||
entry: set_bright;
|
||||
on(after Duration::from_secs(3)) => set_dim, Listening;
|
||||
}
|
||||
}
|
||||
|
||||
state Transmitting {
|
||||
entry: paint_transmitting;
|
||||
default(TransmittingBright);
|
||||
on(EnteredRx) => Listening;
|
||||
on(every Duration::from_millis(1000)) => advance_sparkline, paint_transmitting;
|
||||
on(PacketRx { rssi: i16, snr: Option<i16> }) => record_rx_packet, blink_rx_led;
|
||||
on(PacketRx) => TransmittingBright;
|
||||
on(PacketTx) => record_tx_packet, blink_tx_led;
|
||||
on(PacketTx) => TransmittingBright;
|
||||
|
||||
state TransmittingBright {
|
||||
entry: set_bright;
|
||||
on(after Duration::from_secs(3)) => set_dim, Transmitting;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -355,6 +406,14 @@ impl DisplayActions for DisplayActionContext<'_> {
|
||||
async fn advance_sparkline(&mut self) {
|
||||
self.run.advance_slot();
|
||||
}
|
||||
|
||||
async fn set_bright(&mut self) {
|
||||
self.oled_bright().await;
|
||||
}
|
||||
|
||||
async fn set_dim(&mut self) {
|
||||
self.oled_dim().await;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Task entry point ───────────────────────────────────────────────
|
||||
|
||||
@@ -10,3 +10,56 @@ pub mod sh1106;
|
||||
feature = "waveshare_rp2040_lora"
|
||||
))]
|
||||
pub mod simple_led;
|
||||
|
||||
// ── Display brightness ────────────────────────────────────────────────
|
||||
//
|
||||
// The display statechart dims the OLED a few seconds after the last
|
||||
// activity. Our two driver families (`ssd1306` crate and our in-tree
|
||||
// `sh1106`) expose incompatible brightness APIs, so we wrap them in a
|
||||
// single async trait that the state-machine actions can call uniformly.
|
||||
|
||||
/// Two-level backlight control used by the display dashboard.
|
||||
pub trait DisplayBrightness {
|
||||
fn set_bright(&mut self) -> impl core::future::Future<Output = ()>;
|
||||
fn set_dim(&mut self) -> impl core::future::Future<Output = ()>;
|
||||
}
|
||||
|
||||
// Contrast values for the two brightness levels, picked to be common across
|
||||
// both driver families. `0xFF` is the SSD1306/SH1106 maximum; `0x08` is ~3%
|
||||
// of max — visible in a dim room, barely noticeable in daylight. Tune here.
|
||||
const CONTRAST_BRIGHT: u8 = 0xFF;
|
||||
const CONTRAST_DIM: u8 = 0x08;
|
||||
|
||||
impl<DI, SIZE, MODE> DisplayBrightness for ssd1306::Ssd1306Async<DI, SIZE, MODE>
|
||||
where
|
||||
DI: display_interface::AsyncWriteOnlyDataCommand,
|
||||
SIZE: ssd1306::size::DisplaySizeAsync,
|
||||
{
|
||||
async fn set_bright(&mut self) {
|
||||
// `Brightness::custom(precharge, contrast)`. Matches the shape of
|
||||
// the crate's BRIGHTEST preset (precharge 0x2) at max contrast.
|
||||
let _ = self
|
||||
.set_brightness(ssd1306::prelude::Brightness::custom(0x2, CONTRAST_BRIGHT))
|
||||
.await;
|
||||
}
|
||||
async fn set_dim(&mut self) {
|
||||
// Low precharge pairs with low contrast — same combination the
|
||||
// crate's DIMMEST preset uses.
|
||||
let _ = self
|
||||
.set_brightness(ssd1306::prelude::Brightness::custom(0x1, CONTRAST_DIM))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "wio_tracker_l1", feature = "elecrow_thinknode_m2"))]
|
||||
impl<I> DisplayBrightness for sh1106::Sh1106<I>
|
||||
where
|
||||
I: embedded_hal_async::i2c::I2c,
|
||||
{
|
||||
async fn set_bright(&mut self) {
|
||||
let _ = self.set_brightness(CONTRAST_BRIGHT).await;
|
||||
}
|
||||
async fn set_dim(&mut self) {
|
||||
let _ = self.set_brightness(CONTRAST_DIM).await;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user