diff --git a/variants/thinknode_m6/ThinkNodeM6Board.cpp b/variants/thinknode_m6/ThinkNodeM6Board.cpp index 8ebae64c64..d7038190d2 100644 --- a/variants/thinknode_m6/ThinkNodeM6Board.cpp +++ b/variants/thinknode_m6/ThinkNodeM6Board.cpp @@ -1,4 +1,5 @@ #include "ThinkNodeM6Board.h" + #include #ifdef THINKNODE_M6 @@ -15,9 +16,62 @@ void ThinkNodeM6Board::begin() { digitalWrite(P_LORA_TX_LED, LOW); #endif + // Red LED solid while booting; onBootComplete() hands it over to the heartbeat. + pinMode(PIN_LED_RED, OUTPUT); + digitalWrite(PIN_LED_RED, LED_STATE_ON); + _booting = true; + delay(10); // give sx1262 some time to power up } +void ThinkNodeM6Board::onBootComplete() { + // Flash both LEDs together a few times. This is the one moment worth being + // loud about: it is how you confirm a freshly flashed node actually came up. + for (uint8_t i = 0; i < BOOT_FLASH_COUNT; i++) { + digitalWrite(PIN_LED_RED, LED_STATE_ON); +#ifdef P_LORA_TX_LED + digitalWrite(P_LORA_TX_LED, HIGH); +#endif + delay(BOOT_FLASH_ON_MS); + digitalWrite(PIN_LED_RED, !LED_STATE_ON); +#ifdef P_LORA_TX_LED + digitalWrite(P_LORA_TX_LED, LOW); +#endif + delay(BOOT_FLASH_OFF_MS); + } + + _booting = false; + _status_cycle_start = millis(); + digitalWrite(PIN_LED_RED, !LED_STATE_ON); +} + +void ThinkNodeM6Board::updateStatusLed(bool gps_fix) { + if (_booting) return; // still solid-on, boot hasn't finished + + unsigned long phase = millis() - _status_cycle_start; // wrap-safe + if (phase >= STATUS_LED_PERIOD_MS) { + _status_cycle_start += STATUS_LED_PERIOD_MS; + phase -= STATUS_LED_PERIOD_MS; + // Latch the pattern once per cycle so a fix flapping mid-blink can't + // produce a half-formed pulse. + _status_blinks = gps_fix ? 2 : 1; + if (phase >= STATUS_LED_PERIOD_MS) { // fell far behind (long sleep); resync + _status_cycle_start = millis(); + phase = 0; + } + } + + bool on = false; + for (uint8_t i = 0; i < _status_blinks; i++) { + unsigned long start = i * (unsigned long)(STATUS_LED_ON_MS + STATUS_LED_GAP_MS); + if (phase >= start && phase < start + STATUS_LED_ON_MS) { + on = true; + break; + } + } + digitalWrite(PIN_LED_RED, on ? LED_STATE_ON : !LED_STATE_ON); +} + uint16_t ThinkNodeM6Board::getBattMilliVolts() { int adcvalue = 0; diff --git a/variants/thinknode_m6/ThinkNodeM6Board.h b/variants/thinknode_m6/ThinkNodeM6Board.h index 78815e2c9c..d149c35129 100644 --- a/variants/thinknode_m6/ThinkNodeM6Board.h +++ b/variants/thinknode_m6/ThinkNodeM6Board.h @@ -12,7 +12,28 @@ #define PIN_VBAT_READ BATTERY_PIN #define REAL_VBAT_MV_PER_LSB (VBAT_DIVIDER_COMP * VBAT_MV_PER_LSB) +// Status LED (PIN_LED_RED, the enclosure's power LED) heartbeat timings. +// The M6 is a sealed outdoor box: a slow blink is the only way to tell a live +// node from a dead one, and to see whether the GNSS has a fix, without opening +// it up or attaching a laptop. Duty cycle is ~1% so it costs nothing on solar. +// The LEDs sit on the bottom face of the enclosure next to the USB-C port, so +// they are read at arm's length in daylight -- a blink has to be long enough to +// actually catch the eye. 150ms is comfortably visible and still only ~3% duty. +#define STATUS_LED_PERIOD_MS 5000 // one heartbeat every 5s +#define STATUS_LED_ON_MS 150 // length of each blink +#define STATUS_LED_GAP_MS 200 // dark gap between blinks of a double-blink + +// Unmistakable "firmware is alive" signature at the end of setup(), so a fresh +// flash can be confirmed without a serial console. +#define BOOT_FLASH_COUNT 3 +#define BOOT_FLASH_ON_MS 120 +#define BOOT_FLASH_OFF_MS 120 + class ThinkNodeM6Board : public NRF52BoardDCDC { + bool _booting = true; + unsigned long _status_cycle_start = 0; + uint8_t _status_blinks = 1; + protected: #if NRF52_POWER_MANAGEMENT void initiateShutdown(uint8_t reason) override; @@ -23,6 +44,16 @@ class ThinkNodeM6Board : public NRF52BoardDCDC { void begin(); uint16_t getBattMilliVolts() override; + // Boot indicator: red LED stays solid from begin() until the sketch reports + // that setup() finished, so a boot loop is visible as a flickering LED. + void onBootComplete() override; + + // Drives the red LED heartbeat. Called every iteration from the variant's + // sensor manager, which is the one place that knows the live GNSS state: + // 1 blink / 5s -> running, no GPS fix (yet) + // 2 blinks / 5s -> running, GPS fix acquired + void updateStatusLed(bool gps_fix); + #if defined(P_LORA_TX_LED) void onBeforeTransmit() override { digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on @@ -42,6 +73,7 @@ class ThinkNodeM6Board : public NRF52BoardDCDC { #ifdef P_LORA_TX_LED digitalWrite(P_LORA_TX_LED, LOW); #endif + digitalWrite(PIN_LED_RED, LOW); // power off board NRF52Board::powerOff(); diff --git a/variants/thinknode_m6/target.cpp b/variants/thinknode_m6/target.cpp index de167194e4..e10873294b 100644 --- a/variants/thinknode_m6/target.cpp +++ b/variants/thinknode_m6/target.cpp @@ -13,9 +13,19 @@ VolatileRTCClock fallback_clock; AutoDiscoverRTCClock rtc_clock(fallback_clock); #ifdef ENV_INCLUDE_GPS MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock); -EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea); +ThinkNodeM6SensorManager sensors = ThinkNodeM6SensorManager(nmea); + +void ThinkNodeM6SensorManager::loop() { + EnvironmentSensorManager::loop(); + board.updateStatusLed(gps_active && _location != NULL && _location->isValid()); +} #else -EnvironmentSensorManager sensors = EnvironmentSensorManager(); +ThinkNodeM6SensorManager sensors = ThinkNodeM6SensorManager(); + +void ThinkNodeM6SensorManager::loop() { + EnvironmentSensorManager::loop(); + board.updateStatusLed(false); +} #endif #ifdef DISPLAY_CLASS diff --git a/variants/thinknode_m6/target.h b/variants/thinknode_m6/target.h index 76188e584e..e25c6be5be 100644 --- a/variants/thinknode_m6/target.h +++ b/variants/thinknode_m6/target.h @@ -14,10 +14,28 @@ #include #endif +// Wraps the stock environment sensor manager purely to drive the status LED. +// Its loop() is already called every iteration by every example sketch, and it +// is the only place holding the live GNSS state, so it is where the board gets +// told whether to blink once (no fix) or twice (fix). +#ifdef ENV_INCLUDE_GPS +class ThinkNodeM6SensorManager : public EnvironmentSensorManager { +public: + ThinkNodeM6SensorManager(LocationProvider& location) : EnvironmentSensorManager(location) { } + void loop() override; +}; +#else +class ThinkNodeM6SensorManager : public EnvironmentSensorManager { +public: + ThinkNodeM6SensorManager() : EnvironmentSensorManager() { } + void loop() override; +}; +#endif + extern ThinkNodeM6Board board; extern WRAPPER_CLASS radio_driver; extern AutoDiscoverRTCClock rtc_clock; -extern EnvironmentSensorManager sensors; +extern ThinkNodeM6SensorManager sensors; #ifdef DISPLAY_CLASS extern DISPLAY_CLASS display;