From 7d3b99ce06c43ee1e99430cc1217f41b65f6654c Mon Sep 17 00:00:00 2001 From: Cameron Brooks <95648474+CameronBrooks11@users.noreply.github.com> Date: Sat, 31 Oct 2026 22:49:31 -0400 Subject: [PATCH] feat(firmware): show amber on a watchdog trip or heater fault The status LED stayed green while a command-watchdog trip or a latched heater fault held the heaters off, so a held Slice looked like a running one. updateStatusLed() now takes its state from bsc::StatusLed::stateFor(), with the same hold term the control loop uses (commandWatchdog.tripped() || heaterFaults != 0): amber on a hold, red while the e-stop is held, green once the hold is cleared. I bumped bread-slice-core to ^0.2.0 in platformio.ini and slice.yaml for the Hold state. The e-stop input keeps the internal pull-up. test_status_led_hold covers a trip, a fault, an e-stop over each, the release while each stays latched, and each clear. The amber is worked from the WS2812B datasheet and has not been seen on a board. Flash/RAM against main (8de10b3): gen2 Nano 23998 -> 24038 B / 921 -> 922 B, gen2 Nano Every 25467 -> 25507 B / 999 -> 1000 B, gen1 Nano 22868 -> 22870 B / 907 -> 908 B, gen1 Nano Every 24355 -> 24357 B / 987 -> 988 B. --- CHANGELOG.md | 11 ++ firmware/platformio.ini | 2 +- firmware/src/main.cpp | 12 ++- firmware/test/host/Makefile | 2 +- firmware/test/host/README.md | 1 + firmware/test/host/test_status_led_hold.cpp | 108 ++++++++++++++++++++ slice.yaml | 2 +- 7 files changed, 130 insertions(+), 8 deletions(-) create mode 100644 firmware/test/host/test_status_led_hold.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bf9134..964ee07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,17 @@ This project did not use formal release tags through most of its history, so thi ### Changed +- **The status LED shows amber on a firmware hold** (gen2). While a + command-watchdog trip or a latched heater fault holds the heaters off, the + LED is amber (red 50, green 16) instead of green; red still wins while the + e-stop is held, and the LED returns to green once the trip and faults are + cleared. It takes the colour from `bsc::StatusLed::stateFor()`. Requires + bread-slice-core `^0.2.0` (`firmware/platformio.ini` and `slice.yaml`). + The e-stop input keeps the internal pull-up. + Host-tested only: the amber value is worked from the WS2812B datasheet and + has not been seen on a board. Gen2 flash grows by 40 B and RAM by 1 B; + gen1, which has no LED, by 2 B of flash and 1 B of RAM from the core's + e-stop input. - **The firmware README says which env fits a board** (#32). It links the generation-to-env map in `docs/hardware-revisions.md` and says to name the env when flashing, since a bare `pio run` builds `gen2_nano`. diff --git a/firmware/platformio.ini b/firmware/platformio.ini index 8b2db6e..20a485f 100644 --- a/firmware/platformio.ini +++ b/firmware/platformio.ini @@ -15,7 +15,7 @@ lib_deps = cameronbrooks11/bread-crumbs-contracts @ ^0.11.1 br3ttb/PID @ ^1.2.1 https://github.com/adafruit/MAX6675-library.git - cameronbrooks11/bread-slice-core @ ^0.1.0 + cameronbrooks11/bread-slice-core @ ^0.2.0 build_flags = -DCRUMBS_MAX_HANDLERS=12 ; override for flashed board address: diff --git a/firmware/src/main.cpp b/firmware/src/main.cpp index b3d0a5e..e6ac1ce 100644 --- a/firmware/src/main.cpp +++ b/firmware/src/main.cpp @@ -471,14 +471,16 @@ void setupRLHT() timing.relay2Start = millis(); } -// Red while the e-stop is held, green otherwise; StatusLed writes only on a -// change. Called from loop() only. The boot wait in setup() polls the e-stop -// too, and when that wrote the LED, its first pass replaced the boot blue -// within microseconds. +// Red while the e-stop is held, amber while a command-watchdog trip or a +// latched heater fault holds the heaters off (the hold relayControlLogic and +// checkHeaterFaults apply), green otherwise; red wins over amber. StatusLed +// writes only on a change. Called from loop() only. The boot wait in setup() +// polls the e-stop too, and when that wrote the LED, its first pass replaced +// the boot blue within microseconds. void updateStatusLed() { #if RLHT_HAS_STATUS_LED - statusLed.set(estopInput.asserted() ? bsc::StatusLed::EStop : bsc::StatusLed::Running); + statusLed.set(bsc::StatusLed::stateFor(estopInput.asserted(), commandWatchdog.tripped() || heaterFaults != 0)); #endif } diff --git a/firmware/test/host/Makefile b/firmware/test/host/Makefile index a8fe489..c01e896 100644 --- a/firmware/test/host/Makefile +++ b/firmware/test/host/Makefile @@ -36,7 +36,7 @@ FW_SRC := $(wildcard $(FW)/src/*.cpp) $(PIDLIB)/PID_v1.cpp $(wildcard $(CORE)/ CRUMBS_OBJ := $(OUT)/crumbs_core.o $(OUT)/crumbs_crc.o $(OUT)/crc8_nibble.o # test_status_led runs once per e-stop case (the test target); the others # run once. -SINGLE := test_dispatch test_estop_hold test_watchdog_trip test_serial_burst test_serial_numbers test_set_pid_length test_setpoint_limits test_heater_faults test_watchdog_lease test_pid_sample test_tc_select +SINGLE := test_dispatch test_estop_hold test_watchdog_trip test_serial_burst test_serial_numbers test_set_pid_length test_setpoint_limits test_heater_faults test_watchdog_lease test_pid_sample test_tc_select test_status_led_hold TESTS := $(SINGLE) test_status_led .PHONY: all test clean check-libdeps FORCE diff --git a/firmware/test/host/README.md b/firmware/test/host/README.md index ebe2f40..9a20c7e 100644 --- a/firmware/test/host/README.md +++ b/firmware/test/host/README.md @@ -47,6 +47,7 @@ the board. `make` exits non-zero if any check fails. | `test_tc_select.cpp` | A heater's thermocouple select can't change while it heats in closed loop: SET_TC_SELECT is refused `WRONG_MODE` for that channel and applies the other, resending the select in force is applied, serial `R1TC=` is ignored; with the setpoint at 0 it changes, and the heater restarts on the new sensor with no derivative kick and no P term carried over (#54). The serial status line shows the selected reading in open loop (#28) | | `test_set_pid_length.cpp` | SET_PID takes 6 bytes (u8 x10) and 12 bytes (u16 x1000, up to 65.535, `RLHT_CAP_PID_X1000`); 5, 7, 11 and 13 bytes are refused `MALFORMED` and change no gain (#50, bread-crumbs-contracts#64) | | `test_status_led.cpp` | Blue through the boot wait, then green or red on the first `loop()` pass, with the e-stop released or held from power-up; no LED driven on gen1 (#46) | +| `test_status_led_hold.cpp` | Amber while a command-watchdog trip or a latched heater fault holds the heaters off, red over amber while the e-stop is held, amber again on the release while the hold stays latched, and green once it is cleared; no LED driven on gen1 | ## Writing a test diff --git a/firmware/test/host/test_status_led_hold.cpp b/firmware/test/host/test_status_led_hold.cpp new file mode 100644 index 0000000..4615c52 --- /dev/null +++ b/firmware/test/host/test_status_led_hold.cpp @@ -0,0 +1,108 @@ +// The status LED through a firmware hold: amber while a command-watchdog trip +// or a latched heater fault holds the heaters off, red over amber while the +// e-stop is held, amber again on the release while the hold stays latched, +// and green once it is cleared. Gen1 boards have no LED, and their firmware +// must never drive one. +#include "host.h" +#include "globals.h" +#include +#include + +void setup(); +void loop(); + +// The last frame the core's StatusLed sent, in GRB order (BSC_HOST_TEST +// records it instead of sending it). +static bool shown(uint8_t g, uint8_t r, uint8_t b) +{ + return bsc_host_led_writes > 0 && bsc_host_led_frame[0] == g && bsc_host_led_frame[1] == r && + bsc_host_led_frame[2] == b; +} + +static bool green() { return shown(25, 0, 0); } +static bool red() { return shown(0, 50, 0); } +static bool amber() { return shown(16, 50, 0); } + +static void arm(uint16_t ms) +{ + const uint8_t d[2] = {(uint8_t)(ms & 0xFF), (uint8_t)(ms >> 8)}; + host_send(RLHT_TYPE_ID, BREAD_OP_SET_WATCHDOG, d, 2); + (void)host_read_ack(); +} + +static void press() +{ + host_estop_level = LOW; + loop(); +} + +// Past the core's 25 ms release debounce. +static void release() +{ + host_estop_level = HIGH; + loop(); + host_now += 30; + loop(); + loop(); +} + +static void check(const char *what, bool led_ok) +{ +#if RLHT_HAS_STATUS_LED + host_check(what, led_ok); +#else + (void)what; + (void)led_ok; +#endif +} + +int main() +{ + host_estop_level = HIGH; + host_tc1_celsius = 25.0f; + host_tc2_celsius = 25.0f; + setup(); + loop(); + check("running shows green", green()); + + // ---- A command-watchdog trip. + arm(100); + host_now += 100; + loop(); // trips + loop(); // shows it + check("a watchdog trip shows amber", commandWatchdog.tripped() && amber()); + press(); + check("an e-stop over a watchdog trip shows red", red()); + release(); + check("the release with the trip latched shows amber", commandWatchdog.tripped() && amber()); + arm(0); + host_send(RLHT_TYPE_ID, BREAD_OP_CLEAR_WATCHDOG_TRIP, nullptr, 0); + (void)host_read_ack(); + loop(); + check("clearing the trip shows green", !commandWatchdog.tripped() && green()); + + // ---- A latched heater fault (an over-temperature, which latches in + // either mode). + host_tc1_celsius = RLHT_OVERTEMP_C + 5.0f; + for (int i = 0; i < RLHT_OVERTEMP_READS; i++) + { + host_now += THERMO_UPDATE_TIME_MS; + loop(); + } + loop(); + check("a latched heater fault shows amber", heaterFaults != 0 && amber()); + press(); + check("an e-stop over a latched fault shows red", red()); + release(); + check("the release with the fault latched shows amber", heaterFaults != 0 && amber()); + host_tc1_celsius = 25.0f; + host_send(RLHT_TYPE_ID, BREAD_OP_CLEAR_FAULTS, nullptr, 0); + (void)host_read_ack(); + loop(); + check("clearing the fault shows green", heaterFaults == 0 && green()); + +#if !RLHT_HAS_STATUS_LED + host_check("no LED is driven on a board without one", bsc_host_led_writes == 0); +#endif + return host_finish(); +} diff --git a/slice.yaml b/slice.yaml index f96ea4c..877d1bd 100644 --- a/slice.yaml +++ b/slice.yaml @@ -107,7 +107,7 @@ firmware: - "gen1_nanoevery" - "gen2_nanoevery" lib_deps: - - "cameronbrooks11/bread-slice-core @ ^0.1.0" + - "cameronbrooks11/bread-slice-core @ ^0.2.0" - "cameronbrooks11/CRUMBS @ ^0.17.0" - "cameronbrooks11/bread-crumbs-contracts @ ^0.11.1" - "br3ttb/PID @ ^1.2.1"