diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 7fad801b98..bcfce951a7 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -103,6 +103,10 @@ void setup() { #if ENABLE_ADVERT_ON_BOOT == 1 the_mesh.sendSelfAdvertisement(16000, false); #endif + +#ifdef THINKNODE_M6 + board.bootComplete(); +#endif } void loop() { @@ -147,6 +151,10 @@ void loop() { } #endif +#ifdef THINKNODE_M6 + board.pollButton(); +#endif + the_mesh.loop(); sensors.loop(); #ifdef DISPLAY_CLASS diff --git a/variants/thinknode_m6/ThinkNodeM6Board.cpp b/variants/thinknode_m6/ThinkNodeM6Board.cpp index 8ebae64c64..e1cd86acd9 100644 --- a/variants/thinknode_m6/ThinkNodeM6Board.cpp +++ b/variants/thinknode_m6/ThinkNodeM6Board.cpp @@ -5,9 +5,134 @@ #include +// Function-button timing for hold-to-power-off. +// Brief red blink at 0 s and 1 s during the hold, then board.powerOff() +// runs the final cue when held past 2 s. +#define M6_OFF_FLASH1_START_MS 0 +#define M6_OFF_FLASH1_END_MS 200 +#define M6_OFF_FLASH2_START_MS 1000 +#define M6_OFF_FLASH2_END_MS 1200 +#define M6_OFF_COMMIT_MS 2000 +#define M6_OFF_FLASH_BRIGHT 128 // ~50% of 255 + +// Boot-phase blue LED flicker. TIMER2 fires at pseudo-random 10-100 ms +// intervals; the ISR toggles the blue LED. Runs in the background through +// every blocking call in setup() (mesh init, etc.). +static volatile bool s_flicker_blue_on = false; +static uint32_t s_flicker_rng = 0xC0FFEE42; + +// xorshift32 PRNG for flicker jitter. +static inline uint32_t flicker_next_rand() { + uint32_t x = s_flicker_rng; + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + s_flicker_rng = x; + return x; +} + +extern "C" void TIMER2_IRQHandler(void) { + if (NRF_TIMER2->EVENTS_COMPARE[0]) { + NRF_TIMER2->EVENTS_COMPARE[0] = 0; + NRF_TIMER2->TASKS_CLEAR = 1; + + s_flicker_blue_on = !s_flicker_blue_on; + nrf_gpio_pin_write(g_ADigitalPinMap[PIN_LED_BLUE], s_flicker_blue_on ? 1 : 0); + + NRF_TIMER2->CC[0] = 10000 + (flicker_next_rand() % 90000); // 10-100 ms + } +} + +static void startBootFlicker() { + NRF_TIMER2->TASKS_STOP = 1; + NRF_TIMER2->MODE = TIMER_MODE_MODE_Timer; + NRF_TIMER2->BITMODE = TIMER_BITMODE_BITMODE_32Bit << TIMER_BITMODE_BITMODE_Pos; + NRF_TIMER2->PRESCALER = 4; // 16 MHz / 2^4 = 1 MHz tick (1 µs) + NRF_TIMER2->CC[0] = 30000; // first toggle in ~30 ms + NRF_TIMER2->INTENSET = TIMER_INTENSET_COMPARE0_Msk; + NVIC_SetPriority(TIMER2_IRQn, 7); // low priority + NVIC_ClearPendingIRQ(TIMER2_IRQn); + NVIC_EnableIRQ(TIMER2_IRQn); + NRF_TIMER2->TASKS_CLEAR = 1; + NRF_TIMER2->TASKS_START = 1; +} + +static void stopBootFlicker() { + NRF_TIMER2->TASKS_STOP = 1; + NRF_TIMER2->INTENCLR = TIMER_INTENCLR_COMPARE0_Msk; + NVIC_DisableIRQ(TIMER2_IRQn); + NVIC_ClearPendingIRQ(TIMER2_IRQn); + digitalWrite(PIN_LED_BLUE, LOW); + s_flicker_blue_on = false; +} + +// Arm the Function Button as a SENSE-LOW wake source and enter SYSTEMOFF. +// Falls back to a direct register write if SoftDevice isn't enabled +// (non-BLE builds). +static void enterDeepSleep() { + nrf_gpio_cfg_sense_input(digitalPinToInterrupt(g_ADigitalPinMap[PIN_USER_BTN]), + NRF_GPIO_PIN_PULLUP, NRF_GPIO_PIN_SENSE_LOW); + + uint8_t sd_enabled = 0; + sd_softdevice_is_enabled(&sd_enabled); + if (sd_enabled) { + if (sd_power_system_off() == NRF_ERROR_SOFTDEVICE_NOT_ENABLED) { + sd_enabled = 0; + } + } + if (!sd_enabled) { + NRF_POWER->SYSTEMOFF = POWER_SYSTEMOFF_SYSTEMOFF_Enter; + } + NVIC_SystemReset(); // unreachable +} + +// Captured by variant.cpp's early constructor. See that file for details. +extern volatile uint32_t g_m6_reset_reason; +extern volatile bool g_m6_was_shutdown; + void ThinkNodeM6Board::begin() { NRF52Board::begin(); + // The boot sequence drives the LEDs via digitalWrite throughout. + // analogWrite() must not be called on these pins before powerOff(), + // because on the Adafruit nRF52 core it routes the pin to the PWM + // peripheral and subsequent digitalWrite() calls no longer drive it. + pinMode(PIN_USER_BTN, INPUT_PULLUP); + pinMode(PIN_LED_RED, OUTPUT); + pinMode(PIN_LED_BLUE, OUTPUT); + digitalWrite(PIN_LED_RED, LOW); + digitalWrite(PIN_LED_BLUE, LOW); + delay(20); // pin settle / debounce + + // Boot decision: + // g_m6_was_shutdown && no button wake => the user deliberately powered + // off and isn't asking to come back. Stay asleep. + // otherwise => boot (fresh power-up, dead-battery recovery, transient + // reset of a running device, reset pin, or Function-Button wake). + bool from_reset_pin = (g_m6_reset_reason & POWER_RESETREAS_RESETPIN_Msk) != 0; + bool from_button_wake = (g_m6_reset_reason & POWER_RESETREAS_OFF_Msk) != 0; + + if (g_m6_was_shutdown && !from_reset_pin && !from_button_wake) { + enterDeepSleep(); + } + + // Clear the user-intent flag now that we've committed to booting, so the + // next reset starts from a clean "I'm running" state. + NRF_POWER->GPREGRET2 = 0; + + // Boot indicator: both LEDs full bright for 1 s. + digitalWrite(PIN_LED_RED, HIGH); + digitalWrite(PIN_LED_BLUE, HIGH); + delay(1000); + digitalWrite(PIN_LED_RED, LOW); + digitalWrite(PIN_LED_BLUE, LOW); + + // 1-second gap, then red solid + blue disk-activity flicker. The flicker + // runs in the background via TIMER2 until bootComplete() stops it. + delay(1000); + digitalWrite(PIN_LED_RED, HIGH); + startBootFlicker(); + Wire.begin(); #ifdef P_LORA_TX_LED @@ -18,6 +143,35 @@ void ThinkNodeM6Board::begin() { delay(10); // give sx1262 some time to power up } +void ThinkNodeM6Board::powerOff() { +#ifdef P_LORA_TX_LED + digitalWrite(P_LORA_TX_LED, LOW); +#endif + + // Shutdown cue: red full bright for 1 s, then a brief both-LED flash. + analogWrite(PIN_LED_BLUE, 0); + analogWrite(PIN_LED_RED, 255); + delay(1000); + analogWrite(PIN_LED_RED, 0); + analogWrite(PIN_LED_RED, 255); + analogWrite(PIN_LED_BLUE, 255); + delay(50); + analogWrite(PIN_LED_RED, 0); + analogWrite(PIN_LED_BLUE, 0); + + // SENSE-LOW would fire immediately if we enter SYSTEMOFF with the button + // still held — wait for release first. + while (digitalRead(PIN_USER_BTN) == LOW) delay(10); + + Serial.flush(); + delay(50); + + // User-intent magic byte; read by variant.cpp's early constructor. + NRF_POWER->GPREGRET2 = 0xA5; + + enterDeepSleep(); +} + uint16_t ThinkNodeM6Board::getBattMilliVolts() { int adcvalue = 0; @@ -33,4 +187,45 @@ uint16_t ThinkNodeM6Board::getBattMilliVolts() { // divider into account (providing the actual LIPO voltage) return (uint16_t)((float)adcvalue * REAL_VBAT_MV_PER_LSB); } + +void ThinkNodeM6Board::bootComplete() { + // Stop the disk-activity flicker, dark 1 s gap, then 100 ms blue flash. + stopBootFlicker(); + digitalWrite(PIN_LED_RED, LOW); + digitalWrite(PIN_LED_BLUE, LOW); + delay(1000); + digitalWrite(PIN_LED_BLUE, HIGH); + delay(100); + digitalWrite(PIN_LED_BLUE, LOW); +} + +void ThinkNodeM6Board::pollButton() { + int btnState = digitalRead(PIN_USER_BTN); + if (btnState == LOW) { + if (_btn_down_at == 0) { + _btn_down_at = millis(); + } + unsigned long held = millis() - _btn_down_at; + + if (held >= M6_OFF_COMMIT_MS) { + Serial.println("Powering off..."); + powerOff(); // does not return + } else if ((held >= M6_OFF_FLASH1_START_MS && held < M6_OFF_FLASH1_END_MS) || + (held >= M6_OFF_FLASH2_START_MS && held < M6_OFF_FLASH2_END_MS)) { + analogWrite(PIN_LED_RED, M6_OFF_FLASH_BRIGHT); + digitalWrite(PIN_LED_BLUE, LOW); + } else { + analogWrite(PIN_LED_RED, 0); + digitalWrite(PIN_LED_BLUE, LOW); + } + } else { + // Button released before commit — clear LEDs and reset state. + if (_btn_down_at != 0) { + analogWrite(PIN_LED_RED, 0); + digitalWrite(PIN_LED_BLUE, LOW); + } + _btn_down_at = 0; + } +} + #endif diff --git a/variants/thinknode_m6/ThinkNodeM6Board.h b/variants/thinknode_m6/ThinkNodeM6Board.h index 32baa2a0a2..a209ac9101 100644 --- a/variants/thinknode_m6/ThinkNodeM6Board.h +++ b/variants/thinknode_m6/ThinkNodeM6Board.h @@ -18,11 +18,22 @@ class ThinkNodeM6Board : public NRF52BoardDCDC { void initiateShutdown(uint8_t reason) override; #endif +private: + unsigned long _btn_down_at = 0; // function-button press timestamp (0 = not pressed) + public: ThinkNodeM6Board() : NRF52Board("THINKNODE_M6_OTA") {} void begin(); uint16_t getBattMilliVolts() override; + // Called at the end of setup(). Stops the disk-activity blue flicker + // started in begin() and flashes the blue LED briefly. + void bootComplete(); + + // Polls the function button. Drives LED feedback during a hold and + // calls powerOff() internally on a long press (>= 2 s). + void pollButton(); + #if defined(P_LORA_TX_LED) void onBeforeTransmit() override { digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on @@ -36,14 +47,5 @@ class ThinkNodeM6Board : public NRF52BoardDCDC { return "Elecrow ThinkNode M6"; } - void powerOff() override { - - // turn off all leds, sd_power_system_off will not do this for us - #ifdef P_LORA_TX_LED - digitalWrite(P_LORA_TX_LED, LOW); - #endif - - // power off board - sd_power_system_off(); - } + void powerOff() override; }; diff --git a/variants/thinknode_m6/variant.cpp b/variants/thinknode_m6/variant.cpp index c88f387db6..1473db4b10 100644 --- a/variants/thinknode_m6/variant.cpp +++ b/variants/thinknode_m6/variant.cpp @@ -1,6 +1,30 @@ #include "variant.h" #include "wiring_constants.h" #include "wiring_digital.h" +#include "nrf.h" + +// Globals captured early — before SystemInit()'s errata-136 workaround +// clears the RESETPIN bit, and before any C++ static constructors. Priority +// 101 runs before SystemInit (102). +// +// g_m6_reset_reason: snapshot of NRF_POWER->RESETREAS. +// g_m6_was_shutdown: true if GPREGRET2 holds the "user-intent" magic byte. +// +// GPREGRET2 is used as a user-intent flag: powerOff() writes 0xA5 right +// before SYSTEMOFF, board.begin() clears it on a successful boot. It +// persists across SYSTEMOFF but is wiped by a true power-on reset. +// +// NOTE: GPREGRET2 is also written by NRF52Board::enterSystemOff() under the +// NRF52_POWER_MANAGEMENT build flag. That flag is not set for any M6 env; +// if it ever is, the byte stored here will collide. +volatile uint32_t g_m6_reset_reason = 0; +volatile bool g_m6_was_shutdown = false; + +extern "C" __attribute__((constructor(101))) void m6_capture_resetreas(void) { + g_m6_reset_reason = NRF_POWER->RESETREAS; + NRF_POWER->RESETREAS = 0xFFFFFFFFul; // clear sticky bits + g_m6_was_shutdown = (NRF_POWER->GPREGRET2 == 0xA5); +} const uint32_t g_ADigitalPinMap[] = { 0xff, 0xff, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,