Replace the plain status LED with a WS2812B on PIO
The boards this targets carry an addressable LED on GPIO16 rather than a plain LED, so the GPIO25 heartbeat had nothing to drive. Green means running, blue flashes on each measurement, red means the sensor is failing, with the most urgent condition winning. A healthy node reads as dim green with a blue pip once a second, so a stalled or failing one is obvious with no serial port attached. Driven from PIO directly rather than via ws2812-pio, whose current release pins rp2040-hal 0.11 against this project's 0.12. Those are semver incompatible: cargo would build both HALs and the driver would not accept our PIO, state machine or pin types. The alternative was pinning the whole project back a HAL version, for four instructions of PIO. Generating the waveform in the state machine matters beyond convenience. The bus tasks preempt the LED task freely, and a CPU-timed WS2812 would glitch the moment a frame arrived mid-update. The >50 us latch gap needs no explicit delay either, since repaints are 25 ms apart. Byte order is a named constant rather than two transposed shifts, because it is a property of the part fitted and not of the protocol: many parts sold as WS2812B, along with the WS2811 and SK6812 families, want red first. These do. The unused ChannelOrder variant stays so the choice, and the fix if a board differs, is legible. The task derives everything from existing shared state — the reading's own timestamp already says when the last measurement landed — so nothing is published purely to drive an LED. Colours confirmed on hardware after correcting the channel order; bus suite still 15/15 and sensor counters clean, so PIO0 disturbs nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+53
-11
@@ -11,7 +11,7 @@
|
||||
//! | 1 | `sensor_task` | async | Poll the SHT31 once a second |
|
||||
//! | 1 | `usb_irq` | hardware | Service the USB CDC diagnostic port |
|
||||
//! | 1 | `usb_report` | async | Emit a diagnostic line once a second |
|
||||
//! | 1 | `heartbeat` | async | Blink the status LED |
|
||||
//! | 1 | `status_led` | async | Repaint the WS2812B indicator |
|
||||
//!
|
||||
//! The USB port is a diagnostic aid, not part of the protocol: it lets the node
|
||||
//! be verified over a plain USB cable with no SWD probe and no RS485 adapter. It
|
||||
@@ -31,6 +31,7 @@
|
||||
mod board;
|
||||
mod rs485;
|
||||
mod sensor;
|
||||
mod status_led;
|
||||
|
||||
use defmt_rtt as _;
|
||||
use panic_probe as _;
|
||||
@@ -58,7 +59,6 @@ mod app {
|
||||
|
||||
use core::fmt::Write as _;
|
||||
|
||||
use embedded_hal::digital::StatefulOutputPin;
|
||||
use fugit::RateExtU32;
|
||||
use heapless::String;
|
||||
use rp2040_hal as hal;
|
||||
@@ -79,6 +79,7 @@ mod app {
|
||||
use crate::board::{self, Instant};
|
||||
use crate::rs485::{self, Rs485, RxBuffer, FIFO_DEPTH};
|
||||
use crate::sensor::Sht31;
|
||||
use crate::status_led::{Rgb, StatusLed};
|
||||
|
||||
/// A cached sensor reading and when it was taken.
|
||||
///
|
||||
@@ -128,7 +129,7 @@ mod app {
|
||||
#[local]
|
||||
struct Local {
|
||||
sensor: Sht31<board::SensorI2c>,
|
||||
led: board::StatusLedPin,
|
||||
led: StatusLed,
|
||||
}
|
||||
|
||||
// The USB bus allocator must outlive the device and class that borrow from
|
||||
@@ -188,7 +189,13 @@ mod app {
|
||||
);
|
||||
let sensor = Sht31::new(i2c, board::SENSOR_I2C_ADDR);
|
||||
|
||||
let led = pins.gpio25.reconfigure();
|
||||
// ---- WS2812B status indicator on PIO0 --------------------------------
|
||||
let led = StatusLed::new(
|
||||
cx.device.PIO0,
|
||||
pins.gpio16.reconfigure(),
|
||||
&mut cx.device.RESETS,
|
||||
clocks.system_clock.freq(),
|
||||
);
|
||||
|
||||
// ---- USB CDC diagnostics --------------------------------------------
|
||||
// `force_vbus_detect_bit` is true because the Pico ties VBUS to the chip;
|
||||
@@ -223,7 +230,7 @@ mod app {
|
||||
);
|
||||
|
||||
sensor_task::spawn().ok();
|
||||
heartbeat::spawn().ok();
|
||||
status_led::spawn().ok();
|
||||
usb_report::spawn().ok();
|
||||
|
||||
(
|
||||
@@ -623,13 +630,48 @@ mod app {
|
||||
sensor.read_measurement()
|
||||
}
|
||||
|
||||
/// Blink the status LED. Says the executor is alive; says nothing about the
|
||||
/// bus or the sensor, both of which are reported over the protocol instead.
|
||||
#[task(priority = 1, local = [led])]
|
||||
async fn heartbeat(cx: heartbeat::Context) {
|
||||
/// Repaint the WS2812B status indicator.
|
||||
///
|
||||
/// Green for running, blue for a measurement, red for a sensor error, with
|
||||
/// the most urgent condition winning: red over blue, blue over green. Read at
|
||||
/// a glance, a healthy node is dim green with a blue pip once a second, so a
|
||||
/// stalled or failing node is obvious without a serial port.
|
||||
///
|
||||
/// Derived entirely from existing shared state — the reading's own timestamp
|
||||
/// already says when the last measurement landed, so nothing extra is
|
||||
/// published just to drive an LED.
|
||||
#[task(priority = 1, shared = [reading, state], local = [led])]
|
||||
async fn status_led(mut cx: status_led::Context) {
|
||||
let tick = board::Duration::millis(u64::from(board::LED_TICK_MS));
|
||||
let flash = board::Duration::millis(u64::from(board::LED_MEASURING_MS));
|
||||
|
||||
loop {
|
||||
cx.local.led.toggle().ok();
|
||||
Mono::delay(board::Duration::millis(u64::from(board::HEARTBEAT_MS))).await;
|
||||
let now = Mono::now();
|
||||
|
||||
let measured_recently = cx
|
||||
.shared
|
||||
.reading
|
||||
.lock(|slot| slot.as_ref().map(|r| r.taken_at))
|
||||
.and_then(|taken_at| now.checked_duration_since(taken_at))
|
||||
.is_some_and(|age| age < flash);
|
||||
|
||||
let (sensor_ok, failures) = cx
|
||||
.shared
|
||||
.state
|
||||
.lock(|s| (s.sensor_ok, s.consecutive_failures));
|
||||
|
||||
let colour = if failures > 0 || !sensor_ok {
|
||||
// Any failed poll shows red, not just a latched hard fault: a
|
||||
// sensor dropping the occasional reading is worth seeing.
|
||||
Rgb::ERROR
|
||||
} else if measured_recently {
|
||||
Rgb::MEASURING
|
||||
} else {
|
||||
Rgb::RUNNING
|
||||
};
|
||||
|
||||
cx.local.led.set(colour);
|
||||
Mono::delay(tick).await;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user