From 59adb9033ce2bb5fbd29bd2c05fdb4475e882564 Mon Sep 17 00:00:00 2001 From: MSketcher Date: Thu, 21 May 2026 14:33:21 -0400 Subject: [PATCH 1/6] Proof of Concept See if we can modify the behavior so the LEDs work --- examples/simple_repeater/main.cpp | 50 ++++++++++++++++++++++ variants/thinknode_m6/ThinkNodeM6Board.cpp | 22 ++++++++++ variants/thinknode_m6/ThinkNodeM6Board.h | 13 ++++-- 3 files changed, 82 insertions(+), 3 deletions(-) diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 7fad801b98..918c26ca79 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -28,6 +28,19 @@ static unsigned long userBtnDownAt = 0; #define USER_BTN_HOLD_OFF_MILLIS 1500 #endif +#if defined(PIN_USER_BTN) && defined(THINKNODE_M6) +static unsigned long m6BtnDownAt = 0; +static bool m6BtnFeedbackArmed = false; +// Visual thresholds during a long-press: +// < 3000 ms : LEDs off (just sensing the press) +// 3000-4000 : both LEDs blink in unison @ ~5 Hz +// 4000-5000 : both LEDs solid on +// >= 5000 ms : powerOff() +#define M6_HOLD_BLINK_MS 3000 +#define M6_HOLD_SOLID_MS 4000 +#define M6_HOLD_POWEROFF_MS 5000 +#endif + void setup() { Serial.begin(115200); delay(1000); @@ -103,6 +116,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 +164,39 @@ void loop() { } #endif +#if defined(PIN_USER_BTN) && defined(THINKNODE_M6) + // Hold Function Button to power off the ThinkNode M6. + { + int btnState = digitalRead(PIN_USER_BTN); + if (btnState == LOW) { + if (m6BtnDownAt == 0) { + m6BtnDownAt = millis(); + m6BtnFeedbackArmed = true; + } + unsigned long held = millis() - m6BtnDownAt; + + if (held >= M6_HOLD_POWEROFF_MS) { + Serial.println("Powering off..."); + board.powerOff(); // does not return + } else if (held >= M6_HOLD_SOLID_MS) { + digitalWrite(PIN_LED_RED, HIGH); + digitalWrite(PIN_LED_BLUE, HIGH); + } else if (held >= M6_HOLD_BLINK_MS) { + bool on = ((held / 100) % 2) == 0; + digitalWrite(PIN_LED_RED, on ? HIGH : LOW); + digitalWrite(PIN_LED_BLUE, on ? HIGH : LOW); + } + } else { + if (m6BtnFeedbackArmed && m6BtnDownAt != 0) { + digitalWrite(PIN_LED_RED, LOW); + digitalWrite(PIN_LED_BLUE, LOW); + } + m6BtnDownAt = 0; + m6BtnFeedbackArmed = false; + } + } +#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..2b63df64c7 100644 --- a/variants/thinknode_m6/ThinkNodeM6Board.cpp +++ b/variants/thinknode_m6/ThinkNodeM6Board.cpp @@ -8,6 +8,20 @@ void ThinkNodeM6Board::begin() { NRF52Board::begin(); + // Soft-power latch: the Function Button momentarily applies VCC; firmware + // must drive PIN_PWR_EN HIGH within the first few hundred ms to keep the + // rail alive. + pinMode(PIN_PWR_EN, OUTPUT); + digitalWrite(PIN_PWR_EN, HIGH); + + pinMode(PIN_LED_RED, OUTPUT); + pinMode(PIN_LED_BLUE, OUTPUT); + digitalWrite(PIN_LED_RED, HIGH); + digitalWrite(PIN_LED_BLUE, HIGH); + delay(1000); + digitalWrite(PIN_LED_RED, LOW); + digitalWrite(PIN_LED_BLUE, LOW); + Wire.begin(); #ifdef P_LORA_TX_LED @@ -15,6 +29,8 @@ void ThinkNodeM6Board::begin() { digitalWrite(P_LORA_TX_LED, LOW); #endif + pinMode(PIN_USER_BTN, INPUT_PULLUP); + delay(10); // give sx1262 some time to power up } @@ -33,4 +49,10 @@ 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() { + digitalWrite(PIN_LED_RED, HIGH); delay(150); digitalWrite(PIN_LED_RED, LOW); + delay(120); + digitalWrite(PIN_LED_BLUE, HIGH); delay(150); digitalWrite(PIN_LED_BLUE, LOW); +} #endif diff --git a/variants/thinknode_m6/ThinkNodeM6Board.h b/variants/thinknode_m6/ThinkNodeM6Board.h index 32baa2a0a2..9927e82c96 100644 --- a/variants/thinknode_m6/ThinkNodeM6Board.h +++ b/variants/thinknode_m6/ThinkNodeM6Board.h @@ -22,6 +22,7 @@ class ThinkNodeM6Board : public NRF52BoardDCDC { ThinkNodeM6Board() : NRF52Board("THINKNODE_M6_OTA") {} void begin(); uint16_t getBattMilliVolts() override; + void bootComplete(); #if defined(P_LORA_TX_LED) void onBeforeTransmit() override { @@ -37,13 +38,19 @@ class ThinkNodeM6Board : public NRF52BoardDCDC { } void powerOff() override { - - // turn off all leds, sd_power_system_off will not do this for us + // Turn off LEDs so the device visually confirms a clean shutdown. + digitalWrite(PIN_LED_RED, LOW); + digitalWrite(PIN_LED_BLUE, LOW); #ifdef P_LORA_TX_LED digitalWrite(P_LORA_TX_LED, LOW); #endif - // power off board + // Break the soft-power latch — on battery, this physically cuts MCU power. + digitalWrite(PIN_PWR_EN, LOW); + + // Belt-and-braces: if USB is providing power, the latch drop won't kill the chip. sd_power_system_off(); + + while (1) {} // unreachable } }; From 2eb504e05e8acaaa246b6622dc783de2cd083714 Mon Sep 17 00:00:00 2001 From: MSketcher Date: Fri, 22 May 2026 08:02:19 -0400 Subject: [PATCH 2/6] LED behavior --- examples/simple_repeater/main.cpp | 61 +++--- variants/thinknode_m6/ThinkNodeM6Board.cpp | 173 +++++++++++++++++- variants/thinknode_m6/ThinkNodeM6Board.h | 21 +-- variants/thinknode_m6/variant.cpp | 32 ++++ .../variant.cpp.tmp.12200.508f1cad751a | 49 +++++ 5 files changed, 287 insertions(+), 49 deletions(-) create mode 100644 variants/thinknode_m6/variant.cpp.tmp.12200.508f1cad751a diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 918c26ca79..76501b9935 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -30,22 +30,35 @@ static unsigned long userBtnDownAt = 0; #if defined(PIN_USER_BTN) && defined(THINKNODE_M6) static unsigned long m6BtnDownAt = 0; -static bool m6BtnFeedbackArmed = false; -// Visual thresholds during a long-press: -// < 3000 ms : LEDs off (just sensing the press) -// 3000-4000 : both LEDs blink in unison @ ~5 Hz -// 4000-5000 : both LEDs solid on -// >= 5000 ms : powerOff() -#define M6_HOLD_BLINK_MS 3000 -#define M6_HOLD_SOLID_MS 4000 -#define M6_HOLD_POWEROFF_MS 5000 +// Long-press shutdown timeline (red LED only — no blue until the final flash): +// 0-1000 ms : LEDs off +// 1000-1200 ms : red flash @ 50% brightness +// 1200-2000 ms : red off +// 2000-2200 ms : red flash @ 50% brightness +// 2200-3000 ms : red off +// >= 3000 ms : commitment — board.powerOff() shows red full bright for +// 1 s, then both LEDs flash 50 ms, then sleeps at the 4 s +// mark regardless of whether the button is still held. +#define M6_OFF_FLASH1_START_MS 1000 +#define M6_OFF_FLASH1_END_MS 1200 +#define M6_OFF_FLASH2_START_MS 2000 +#define M6_OFF_FLASH2_END_MS 2200 +#define M6_OFF_COMMIT_MS 3000 +#define M6_OFF_FLASH_BRIGHT 128 // ~50% of 255 #endif void setup() { Serial.begin(115200); - delay(1000); +#ifdef THINKNODE_M6 + // The M6's board.begin() does a 0.5-second hold-to-power-on intent check. + // Run it before the pre-setup delay so the user's 0.5 s hold timer starts + // immediately on wake, not after a 1-second pre-delay. + board.begin(); +#else + delay(1000); board.begin(); +#endif #if defined(MESH_DEBUG) && defined(NRF52_PLATFORM) // give some extra time for serial to settle so @@ -171,28 +184,30 @@ void loop() { if (btnState == LOW) { if (m6BtnDownAt == 0) { m6BtnDownAt = millis(); - m6BtnFeedbackArmed = true; } unsigned long held = millis() - m6BtnDownAt; - if (held >= M6_HOLD_POWEROFF_MS) { + if (held >= M6_OFF_COMMIT_MS) { + // Commitment: powerOff() snaps red to full bright for 1 s, then sleeps. Serial.println("Powering off..."); board.powerOff(); // does not return - } else if (held >= M6_HOLD_SOLID_MS) { - digitalWrite(PIN_LED_RED, HIGH); - digitalWrite(PIN_LED_BLUE, HIGH); - } else if (held >= M6_HOLD_BLINK_MS) { - bool on = ((held / 100) % 2) == 0; - digitalWrite(PIN_LED_RED, on ? HIGH : LOW); - digitalWrite(PIN_LED_BLUE, on ? HIGH : LOW); + } 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)) { + // Brief 50% red flash at the 2 s and 3 s marks. + analogWrite(PIN_LED_RED, M6_OFF_FLASH_BRIGHT); + analogWrite(PIN_LED_BLUE, 0); + } else { + // All other pre-commit moments: LEDs off. + analogWrite(PIN_LED_RED, 0); + analogWrite(PIN_LED_BLUE, 0); } } else { - if (m6BtnFeedbackArmed && m6BtnDownAt != 0) { - digitalWrite(PIN_LED_RED, LOW); - digitalWrite(PIN_LED_BLUE, LOW); + // Released before commitment — cancel and clear LEDs. + if (m6BtnDownAt != 0) { + analogWrite(PIN_LED_RED, 0); + analogWrite(PIN_LED_BLUE, 0); } m6BtnDownAt = 0; - m6BtnFeedbackArmed = false; } } #endif diff --git a/variants/thinknode_m6/ThinkNodeM6Board.cpp b/variants/thinknode_m6/ThinkNodeM6Board.cpp index 2b63df64c7..18ce37d06e 100644 --- a/variants/thinknode_m6/ThinkNodeM6Board.cpp +++ b/variants/thinknode_m6/ThinkNodeM6Board.cpp @@ -5,23 +5,141 @@ #include +// --- Boot-phase "disk activity" blue LED flicker --- +// TIMER2 fires at pseudo-random intervals (10-100 ms); ISR toggles the blue +// LED. Runs autonomously throughout setup() so the user gets continuous +// "device is working" feedback during blocking calls like the_mesh.begin(). +// TIMER0 is reserved by SoftDevice; TIMER1 is often used by other libraries; +// TIMER2 is reliably free in the M6 repeater build. +static volatile bool s_flicker_blue_on = false; +static uint32_t s_flicker_rng = 0xC0FFEE42; + +static inline uint32_t flicker_next_rand() { + // xorshift32 — cheap and good enough for "disk activity" jitter + 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); + + // Next toggle in 10-100 ms. + NRF_TIMER2->CC[0] = 10000 + (flicker_next_rand() % 90000); + } +} + +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 — don't block radio/serial + 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. +// SoftDevice may not be enabled in non-BLE builds (repeater), so we fall back +// to a direct register write — mirrors NRF52Board::enterSystemOff(). +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 in normal flow +} + +// Captured by an early constructor in variant.cpp before SystemInit()'s +// errata-136 workaround scrubs the RESETPIN bit. g_m6_was_powered is the +// GPREGRET2-based "did firmware run in this power session?" flag — false +// means power was completely lost (e.g., battery died, solar recharge), +// which we treat as a recovery boot. +extern volatile uint32_t g_m6_reset_reason; +extern volatile bool g_m6_was_powered; + void ThinkNodeM6Board::begin() { NRF52Board::begin(); - // Soft-power latch: the Function Button momentarily applies VCC; firmware - // must drive PIN_PWR_EN HIGH within the first few hundred ms to keep the - // rail alive. - pinMode(PIN_PWR_EN, OUTPUT); - digitalWrite(PIN_PWR_EN, HIGH); + // PIN_PWR_EN (peripheral power rail) is already driven HIGH by + // initVariant() in variant.cpp; no need to re-assert it here. + // NOTE: do not call analogWrite() here. On the Adafruit nRF52 core, + // analogWrite() routes the pin to the PWM peripheral and subsequent + // digitalWrite() calls no longer drive the pin. initVariant() already left + // both LEDs LOW via digitalWrite, so we can use digitalWrite the whole way + // through the boot sequence. powerOff() uses analogWrite later, after the + // boot LEDs are no longer needed. + 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 + + // Decide whether to actually boot based on three signals: + // - RESETPIN bit set → user pressed reset → boot + // - OFF bit set → SENSE-LOW wake (only configured on PIN_USER_BTN) → + // user pressed the Function Button → boot + // - !g_m6_was_powered → GPREGRET2 magic was cleared by full power loss, + // so this is a recovery from a dead battery / solar recharge → boot + // automatically (important for unattended deployments) + // Otherwise (POR/brownout/USB transient while we were already running) → + // re-sleep silently. + 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; + bool from_power_loss = !g_m6_was_powered; + + if (!from_reset_pin && !from_button_wake && !from_power_loss) { + // No deliberate user action and we were already powered — re-sleep. + enterDeepSleep(); + } + + // Intent confirmed — both LEDs full bright for 1 second. 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 (LEDs off), then start the boot-phase indicator: red solid + // on, blue flickering like a disk-drive activity LED. The flicker runs on + // a TIMER2 interrupt so it keeps going through every blocking call in + // setup() (the_mesh.begin() etc.). It's stopped by bootComplete() at the end. + delay(1000); + digitalWrite(PIN_LED_RED, HIGH); + startBootFlicker(); + Wire.begin(); #ifdef P_LORA_TX_LED @@ -29,11 +147,38 @@ void ThinkNodeM6Board::begin() { digitalWrite(P_LORA_TX_LED, LOW); #endif - pinMode(PIN_USER_BTN, INPUT_PULLUP); - 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. + analogWrite(PIN_LED_BLUE, 0); + analogWrite(PIN_LED_RED, 255); + delay(1000); + analogWrite(PIN_LED_RED, 0); + + // Final "goodbye": brief both-LED flash so the user knows we're committed + // and the device is going dark right now. + analogWrite(PIN_LED_RED, 255); + analogWrite(PIN_LED_BLUE, 255); + delay(50); + analogWrite(PIN_LED_RED, 0); + analogWrite(PIN_LED_BLUE, 0); + + // If the button is still held, we can't yet enter SYSTEMOFF (SENSE-LOW + // would wake us immediately). Wait silently for release — no LEDs lit, + // so the wait is invisible. + while (digitalRead(PIN_USER_BTN) == LOW) delay(10); + + Serial.flush(); + delay(50); + enterDeepSleep(); +} + uint16_t ThinkNodeM6Board::getBattMilliVolts() { int adcvalue = 0; @@ -51,8 +196,16 @@ uint16_t ThinkNodeM6Board::getBattMilliVolts() { } void ThinkNodeM6Board::bootComplete() { - digitalWrite(PIN_LED_RED, HIGH); delay(150); digitalWrite(PIN_LED_RED, LOW); - delay(120); - digitalWrite(PIN_LED_BLUE, HIGH); delay(150); digitalWrite(PIN_LED_BLUE, LOW); + // Stop the flicker timer, turn red off, pause 1 second (everything dark) + // to clearly delimit the boot phase, then flash blue for 100 ms as the + // "device is up" signal. + 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); } + #endif diff --git a/variants/thinknode_m6/ThinkNodeM6Board.h b/variants/thinknode_m6/ThinkNodeM6Board.h index 9927e82c96..26b2d89e20 100644 --- a/variants/thinknode_m6/ThinkNodeM6Board.h +++ b/variants/thinknode_m6/ThinkNodeM6Board.h @@ -22,6 +22,10 @@ class ThinkNodeM6Board : public NRF52BoardDCDC { ThinkNodeM6Board() : NRF52Board("THINKNODE_M6_OTA") {} void begin(); uint16_t getBattMilliVolts() override; + + // Called at the very end of setup() to mark "boot complete": stops the + // disk-activity-style blue flicker started in begin(), turns the red LED + // off, and flashes the blue LED for 100 ms. void bootComplete(); #if defined(P_LORA_TX_LED) @@ -37,20 +41,5 @@ class ThinkNodeM6Board : public NRF52BoardDCDC { return "Elecrow ThinkNode M6"; } - void powerOff() override { - // Turn off LEDs so the device visually confirms a clean shutdown. - digitalWrite(PIN_LED_RED, LOW); - digitalWrite(PIN_LED_BLUE, LOW); - #ifdef P_LORA_TX_LED - digitalWrite(P_LORA_TX_LED, LOW); - #endif - - // Break the soft-power latch — on battery, this physically cuts MCU power. - digitalWrite(PIN_PWR_EN, LOW); - - // Belt-and-braces: if USB is providing power, the latch drop won't kill the chip. - sd_power_system_off(); - - while (1) {} // unreachable - } + void powerOff() override; }; diff --git a/variants/thinknode_m6/variant.cpp b/variants/thinknode_m6/variant.cpp index c88f387db6..4c1ac98262 100644 --- a/variants/thinknode_m6/variant.cpp +++ b/variants/thinknode_m6/variant.cpp @@ -1,6 +1,38 @@ #include "variant.h" #include "wiring_constants.h" #include "wiring_digital.h" +#include "nrf.h" + +// Capture NRF_POWER->RESETREAS before SystemInit() runs the Nordic errata-136 +// workaround that clears the RESETPIN bit. Priority 101 places this before +// SystemInit (102) and all C++ static constructors. Exposed via the globals +// below so ThinkNodeM6Board::begin() can decide whether to boot or re-sleep. +// +// We also leverage GPREGRET2 (an 8-bit retained register) to detect full +// power loss: the register persists through SYSTEMOFF but is cleared when +// the chip loses power completely. By stamping a magic value on every boot, +// the next boot can tell "we cleanly went to SYSTEMOFF" (magic still there) +// from "battery died and just came back" (magic cleared). The latter case +// boots automatically — important for unattended solar-powered deployments. +// +// NOTE: GPREGRET2 is also written by NRF52Board::enterSystemOff() when +// NRF52_POWER_MANAGEMENT is defined. That build flag is NOT set for any M6 +// env today, so there is no conflict. If it's ever enabled for the M6, the +// magic value here will collide with the shutdown-reason byte stored there. +#define M6_GPREGRET2_MAGIC 0xA5 +volatile uint32_t g_m6_reset_reason = 0; +volatile bool g_m6_was_powered = false; + +extern "C" __attribute__((constructor(101))) void m6_capture_resetreas(void) { + g_m6_reset_reason = NRF_POWER->RESETREAS; + // Clear sticky bits so the next boot sees a fresh state. + NRF_POWER->RESETREAS = 0xFFFFFFFFul; + + // GPREGRET2 magic value: present = firmware ran in this power session + // (we cleanly went to SYSTEMOFF and came back); absent = power was lost. + g_m6_was_powered = (NRF_POWER->GPREGRET2 == M6_GPREGRET2_MAGIC); + NRF_POWER->GPREGRET2 = M6_GPREGRET2_MAGIC; // stamp it for next boot +} const uint32_t g_ADigitalPinMap[] = { 0xff, 0xff, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, diff --git a/variants/thinknode_m6/variant.cpp.tmp.12200.508f1cad751a b/variants/thinknode_m6/variant.cpp.tmp.12200.508f1cad751a new file mode 100644 index 0000000000..278aae6e43 --- /dev/null +++ b/variants/thinknode_m6/variant.cpp.tmp.12200.508f1cad751a @@ -0,0 +1,49 @@ +#include "variant.h" +#include "wiring_constants.h" +#include "wiring_digital.h" +#include "nrf.h" + +// Capture NRF_POWER->RESETREAS before SystemInit() runs the Nordic errata-136 +// workaround that clears the RESETPIN bit. Priority 101 places this before +// SystemInit (102) and all C++ static constructors. Exposed via the global +// g_m6_reset_reason so ThinkNodeM6Board::begin() can decide whether to honor +// the intent-check (button hold) or bypass it (reset-pin reboot). +volatile uint32_t g_m6_reset_reason = 0; + +extern "C" __attribute__((constructor(101))) void m6_capture_resetreas(void) { + g_m6_reset_reason = NRF_POWER->RESETREAS; + // Clear sticky bits so the next boot sees a fresh state. + NRF_POWER->RESETREAS = 0xFFFFFFFFul; +} + +const uint32_t g_ADigitalPinMap[] = { + 0xff, 0xff, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, + 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, + 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, + 40, 41, 42, 43, 44, 45, 46, 47 +}; + +void initVariant() { + pinMode(PIN_PWR_EN, OUTPUT); + digitalWrite(PIN_PWR_EN, HIGH); + + pinMode(QSPI_FLASH_EN, OUTPUT); + digitalWrite(QSPI_FLASH_EN, HIGH); + + // For now stick adc_ctrl to fixed value + pinMode(PIN_ADC_CTRL, OUTPUT); + digitalWrite(PIN_ADC_CTRL, LOW); + + pinMode(PIN_LED_RED, OUTPUT); + pinMode(PIN_LED_BLUE, OUTPUT); + digitalWrite(PIN_LED_BLUE, LOW); + digitalWrite(PIN_LED_RED, LOW); + + // gps + pinMode(PIN_GPS_STANDBY, OUTPUT); + digitalWrite(PIN_GPS_STANDBY, HIGH); + pinMode(PIN_GPS_EN, OUTPUT); + digitalWrite(PIN_GPS_EN, HIGH); + pinMode(PIN_GPS_RESET, OUTPUT); + digitalWrite(PIN_GPS_RESET, HIGH); +} From 913299e826ef9aa863897a91a2ef048c8da6bca8 Mon Sep 17 00:00:00 2001 From: MSketcher Date: Fri, 22 May 2026 08:49:19 -0400 Subject: [PATCH 3/6] Final cleanup --- examples/simple_repeater/main.cpp | 67 ++++++++++++------- variants/thinknode_m6/ThinkNodeM6Board.cpp | 2 +- .../variant.cpp.tmp.12200.508f1cad751a | 49 -------------- 3 files changed, 45 insertions(+), 73 deletions(-) delete mode 100644 variants/thinknode_m6/variant.cpp.tmp.12200.508f1cad751a diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 76501b9935..44e357c797 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -31,29 +31,36 @@ static unsigned long userBtnDownAt = 0; #if defined(PIN_USER_BTN) && defined(THINKNODE_M6) static unsigned long m6BtnDownAt = 0; // Long-press shutdown timeline (red LED only — no blue until the final flash): -// 0-1000 ms : LEDs off -// 1000-1200 ms : red flash @ 50% brightness +// 0-200 ms : red flash @ 50% — immediate ack of the press +// 200-1000 ms : red off +// 1000-1200 ms : red flash @ 50% // 1200-2000 ms : red off -// 2000-2200 ms : red flash @ 50% brightness -// 2200-3000 ms : red off -// >= 3000 ms : commitment — board.powerOff() shows red full bright for -// 1 s, then both LEDs flash 50 ms, then sleeps at the 4 s -// mark regardless of whether the button is still held. -#define M6_OFF_FLASH1_START_MS 1000 -#define M6_OFF_FLASH1_END_MS 1200 -#define M6_OFF_FLASH2_START_MS 2000 -#define M6_OFF_FLASH2_END_MS 2200 -#define M6_OFF_COMMIT_MS 3000 +// >= 2000 ms : commitment — board.powerOff() shows red full bright for +// 1 s (the third "blink"), then both LEDs flash 50 ms, +// then sleeps at the 3 s mark regardless of button state. +#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 + +// Tap (quick press+release) sends an advertisement and blinks Morse "A" on +// the blue LED to acknowledge. +#define M6_TAP_MIN_MS 30 // debounce floor +#define M6_TAP_MAX_MS 500 // anything longer is a hold attempt +#define M6_MORSE_DOT_MS 200 +#define M6_MORSE_GAP_MS 200 +#define M6_MORSE_DASH_MS 600 #endif void setup() { Serial.begin(115200); #ifdef THINKNODE_M6 - // The M6's board.begin() does a 0.5-second hold-to-power-on intent check. - // Run it before the pre-setup delay so the user's 0.5 s hold timer starts - // immediately on wake, not after a 1-second pre-delay. + // The M6's board.begin() drives the boot LED sequence. Run it before the + // pre-setup delay so the LEDs come on as soon as the chip wakes, rather + // than after a 1-second silent gap. board.begin(); #else delay(1000); @@ -193,19 +200,33 @@ void loop() { board.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)) { - // Brief 50% red flash at the 2 s and 3 s marks. - analogWrite(PIN_LED_RED, M6_OFF_FLASH_BRIGHT); - analogWrite(PIN_LED_BLUE, 0); + // Brief 50% red flash at the 0 s (immediate ack) and 1 s marks. + analogWrite(PIN_LED_RED, M6_OFF_FLASH_BRIGHT); + digitalWrite(PIN_LED_BLUE, LOW); } else { // All other pre-commit moments: LEDs off. - analogWrite(PIN_LED_RED, 0); - analogWrite(PIN_LED_BLUE, 0); + analogWrite(PIN_LED_RED, 0); + digitalWrite(PIN_LED_BLUE, LOW); } } else { - // Released before commitment — cancel and clear LEDs. + // Button released. Could be a cancel (started off-sequence but let go) + // or a tap (quick press+release to send an advert). if (m6BtnDownAt != 0) { - analogWrite(PIN_LED_RED, 0); - analogWrite(PIN_LED_BLUE, 0); + unsigned long held = millis() - m6BtnDownAt; + analogWrite(PIN_LED_RED, 0); + digitalWrite(PIN_LED_BLUE, LOW); + + if (held >= M6_TAP_MIN_MS && held < M6_TAP_MAX_MS) { + // Tap — broadcast an advert and blink Morse "A" (dot-dash) on + // the blue LED. digitalWrite (not analogWrite) so the blue pin + // stays in pure GPIO mode and the LoRa TX LED keeps working. + Serial.println("Tap -> sending advert"); + the_mesh.sendSelfAdvertisement(16000, false); + digitalWrite(PIN_LED_BLUE, HIGH); delay(M6_MORSE_DOT_MS); + digitalWrite(PIN_LED_BLUE, LOW); delay(M6_MORSE_GAP_MS); + digitalWrite(PIN_LED_BLUE, HIGH); delay(M6_MORSE_DASH_MS); + digitalWrite(PIN_LED_BLUE, LOW); + } } m6BtnDownAt = 0; } diff --git a/variants/thinknode_m6/ThinkNodeM6Board.cpp b/variants/thinknode_m6/ThinkNodeM6Board.cpp index 18ce37d06e..3c9a3d7edb 100644 --- a/variants/thinknode_m6/ThinkNodeM6Board.cpp +++ b/variants/thinknode_m6/ThinkNodeM6Board.cpp @@ -125,7 +125,7 @@ void ThinkNodeM6Board::begin() { enterDeepSleep(); } - // Intent confirmed — both LEDs full bright for 1 second. + // Boot indicator: both LEDs full bright for 1 second. digitalWrite(PIN_LED_RED, HIGH); digitalWrite(PIN_LED_BLUE, HIGH); delay(1000); diff --git a/variants/thinknode_m6/variant.cpp.tmp.12200.508f1cad751a b/variants/thinknode_m6/variant.cpp.tmp.12200.508f1cad751a deleted file mode 100644 index 278aae6e43..0000000000 --- a/variants/thinknode_m6/variant.cpp.tmp.12200.508f1cad751a +++ /dev/null @@ -1,49 +0,0 @@ -#include "variant.h" -#include "wiring_constants.h" -#include "wiring_digital.h" -#include "nrf.h" - -// Capture NRF_POWER->RESETREAS before SystemInit() runs the Nordic errata-136 -// workaround that clears the RESETPIN bit. Priority 101 places this before -// SystemInit (102) and all C++ static constructors. Exposed via the global -// g_m6_reset_reason so ThinkNodeM6Board::begin() can decide whether to honor -// the intent-check (button hold) or bypass it (reset-pin reboot). -volatile uint32_t g_m6_reset_reason = 0; - -extern "C" __attribute__((constructor(101))) void m6_capture_resetreas(void) { - g_m6_reset_reason = NRF_POWER->RESETREAS; - // Clear sticky bits so the next boot sees a fresh state. - NRF_POWER->RESETREAS = 0xFFFFFFFFul; -} - -const uint32_t g_ADigitalPinMap[] = { - 0xff, 0xff, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, - 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, - 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, - 40, 41, 42, 43, 44, 45, 46, 47 -}; - -void initVariant() { - pinMode(PIN_PWR_EN, OUTPUT); - digitalWrite(PIN_PWR_EN, HIGH); - - pinMode(QSPI_FLASH_EN, OUTPUT); - digitalWrite(QSPI_FLASH_EN, HIGH); - - // For now stick adc_ctrl to fixed value - pinMode(PIN_ADC_CTRL, OUTPUT); - digitalWrite(PIN_ADC_CTRL, LOW); - - pinMode(PIN_LED_RED, OUTPUT); - pinMode(PIN_LED_BLUE, OUTPUT); - digitalWrite(PIN_LED_BLUE, LOW); - digitalWrite(PIN_LED_RED, LOW); - - // gps - pinMode(PIN_GPS_STANDBY, OUTPUT); - digitalWrite(PIN_GPS_STANDBY, HIGH); - pinMode(PIN_GPS_EN, OUTPUT); - digitalWrite(PIN_GPS_EN, HIGH); - pinMode(PIN_GPS_RESET, OUTPUT); - digitalWrite(PIN_GPS_RESET, HIGH); -} From d5a9845b0aa036211805073ea38e7efe18d00f84 Mon Sep 17 00:00:00 2001 From: AtlavoxDev Date: Fri, 22 May 2026 09:48:14 -0400 Subject: [PATCH 4/6] Comment cleanup --- examples/simple_repeater/main.cpp | 30 +++---- variants/thinknode_m6/ThinkNodeM6Board.cpp | 91 +++++++++------------- variants/thinknode_m6/ThinkNodeM6Board.h | 5 +- variants/thinknode_m6/variant.cpp | 38 ++++----- 4 files changed, 62 insertions(+), 102 deletions(-) diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 44e357c797..00f2624b03 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -30,14 +30,8 @@ static unsigned long userBtnDownAt = 0; #if defined(PIN_USER_BTN) && defined(THINKNODE_M6) static unsigned long m6BtnDownAt = 0; -// Long-press shutdown timeline (red LED only — no blue until the final flash): -// 0-200 ms : red flash @ 50% — immediate ack of the press -// 200-1000 ms : red off -// 1000-1200 ms : red flash @ 50% -// 1200-2000 ms : red off -// >= 2000 ms : commitment — board.powerOff() shows red full bright for -// 1 s (the third "blink"), then both LEDs flash 50 ms, -// then sleeps at the 3 s mark regardless of button state. +// Long-press shutdown: red LED flashes briefly at 0 s and 1 s during the +// hold, then board.powerOff() runs the final shutdown cue at 2 s. #define M6_OFF_FLASH1_START_MS 0 #define M6_OFF_FLASH1_END_MS 200 #define M6_OFF_FLASH2_START_MS 1000 @@ -45,8 +39,7 @@ static unsigned long m6BtnDownAt = 0; #define M6_OFF_COMMIT_MS 2000 #define M6_OFF_FLASH_BRIGHT 128 // ~50% of 255 -// Tap (quick press+release) sends an advertisement and blinks Morse "A" on -// the blue LED to acknowledge. +// Quick tap (press+release) sends an advert; blue LED blinks Morse "A". #define M6_TAP_MIN_MS 30 // debounce floor #define M6_TAP_MAX_MS 500 // anything longer is a hold attempt #define M6_MORSE_DOT_MS 200 @@ -58,9 +51,8 @@ void setup() { Serial.begin(115200); #ifdef THINKNODE_M6 - // The M6's board.begin() drives the boot LED sequence. Run it before the - // pre-setup delay so the LEDs come on as soon as the chip wakes, rather - // than after a 1-second silent gap. + // M6's board.begin() drives the boot LED sequence; run it before any + // pre-setup delays so the LEDs come on immediately at wake. board.begin(); #else delay(1000); @@ -195,31 +187,27 @@ void loop() { unsigned long held = millis() - m6BtnDownAt; if (held >= M6_OFF_COMMIT_MS) { - // Commitment: powerOff() snaps red to full bright for 1 s, then sleeps. Serial.println("Powering off..."); board.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)) { - // Brief 50% red flash at the 0 s (immediate ack) and 1 s marks. analogWrite(PIN_LED_RED, M6_OFF_FLASH_BRIGHT); digitalWrite(PIN_LED_BLUE, LOW); } else { - // All other pre-commit moments: LEDs off. analogWrite(PIN_LED_RED, 0); digitalWrite(PIN_LED_BLUE, LOW); } } else { - // Button released. Could be a cancel (started off-sequence but let go) - // or a tap (quick press+release to send an advert). + // Button released. Quick press+release = tap (send advert). Longer + // holds that don't reach the commit threshold are silent cancels. if (m6BtnDownAt != 0) { unsigned long held = millis() - m6BtnDownAt; analogWrite(PIN_LED_RED, 0); digitalWrite(PIN_LED_BLUE, LOW); if (held >= M6_TAP_MIN_MS && held < M6_TAP_MAX_MS) { - // Tap — broadcast an advert and blink Morse "A" (dot-dash) on - // the blue LED. digitalWrite (not analogWrite) so the blue pin - // stays in pure GPIO mode and the LoRa TX LED keeps working. + // Tap → advert + Morse "A" on blue. digitalWrite keeps blue in + // pure GPIO mode so the LoRa TX LED indicator still works. Serial.println("Tap -> sending advert"); the_mesh.sendSelfAdvertisement(16000, false); digitalWrite(PIN_LED_BLUE, HIGH); delay(M6_MORSE_DOT_MS); diff --git a/variants/thinknode_m6/ThinkNodeM6Board.cpp b/variants/thinknode_m6/ThinkNodeM6Board.cpp index 3c9a3d7edb..d42efdecdb 100644 --- a/variants/thinknode_m6/ThinkNodeM6Board.cpp +++ b/variants/thinknode_m6/ThinkNodeM6Board.cpp @@ -5,17 +5,14 @@ #include -// --- Boot-phase "disk activity" blue LED flicker --- -// TIMER2 fires at pseudo-random intervals (10-100 ms); ISR toggles the blue -// LED. Runs autonomously throughout setup() so the user gets continuous -// "device is working" feedback during blocking calls like the_mesh.begin(). -// TIMER0 is reserved by SoftDevice; TIMER1 is often used by other libraries; -// TIMER2 is reliably free in the M6 repeater build. +// 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() { - // xorshift32 — cheap and good enough for "disk activity" jitter uint32_t x = s_flicker_rng; x ^= x << 13; x ^= x >> 17; @@ -32,8 +29,7 @@ extern "C" void TIMER2_IRQHandler(void) { s_flicker_blue_on = !s_flicker_blue_on; nrf_gpio_pin_write(g_ADigitalPinMap[PIN_LED_BLUE], s_flicker_blue_on ? 1 : 0); - // Next toggle in 10-100 ms. - NRF_TIMER2->CC[0] = 10000 + (flicker_next_rand() % 90000); + NRF_TIMER2->CC[0] = 10000 + (flicker_next_rand() % 90000); // 10-100 ms } } @@ -44,7 +40,7 @@ static void startBootFlicker() { 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 — don't block radio/serial + NVIC_SetPriority(TIMER2_IRQn, 7); // low priority NVIC_ClearPendingIRQ(TIMER2_IRQn); NVIC_EnableIRQ(TIMER2_IRQn); NRF_TIMER2->TASKS_CLEAR = 1; @@ -61,8 +57,8 @@ static void stopBootFlicker() { } // Arm the Function Button as a SENSE-LOW wake source and enter SYSTEMOFF. -// SoftDevice may not be enabled in non-BLE builds (repeater), so we fall back -// to a direct register write — mirrors NRF52Board::enterSystemOff(). +// 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); @@ -77,29 +73,20 @@ static void enterDeepSleep() { if (!sd_enabled) { NRF_POWER->SYSTEMOFF = POWER_SYSTEMOFF_SYSTEMOFF_Enter; } - NVIC_SystemReset(); // unreachable in normal flow + NVIC_SystemReset(); // unreachable } -// Captured by an early constructor in variant.cpp before SystemInit()'s -// errata-136 workaround scrubs the RESETPIN bit. g_m6_was_powered is the -// GPREGRET2-based "did firmware run in this power session?" flag — false -// means power was completely lost (e.g., battery died, solar recharge), -// which we treat as a recovery boot. +// 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_powered; +extern volatile bool g_m6_was_shutdown; void ThinkNodeM6Board::begin() { NRF52Board::begin(); - // PIN_PWR_EN (peripheral power rail) is already driven HIGH by - // initVariant() in variant.cpp; no need to re-assert it here. - - // NOTE: do not call analogWrite() here. On the Adafruit nRF52 core, - // analogWrite() routes the pin to the PWM peripheral and subsequent - // digitalWrite() calls no longer drive the pin. initVariant() already left - // both LEDs LOW via digitalWrite, so we can use digitalWrite the whole way - // through the boot sequence. powerOff() uses analogWrite later, after the - // boot LEDs are no longer needed. + // 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); @@ -107,35 +94,31 @@ void ThinkNodeM6Board::begin() { digitalWrite(PIN_LED_BLUE, LOW); delay(20); // pin settle / debounce - // Decide whether to actually boot based on three signals: - // - RESETPIN bit set → user pressed reset → boot - // - OFF bit set → SENSE-LOW wake (only configured on PIN_USER_BTN) → - // user pressed the Function Button → boot - // - !g_m6_was_powered → GPREGRET2 magic was cleared by full power loss, - // so this is a recovery from a dead battery / solar recharge → boot - // automatically (important for unattended deployments) - // Otherwise (POR/brownout/USB transient while we were already running) → - // re-sleep silently. + // 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; - bool from_power_loss = !g_m6_was_powered; - if (!from_reset_pin && !from_button_wake && !from_power_loss) { - // No deliberate user action and we were already powered — re-sleep. + if (g_m6_was_shutdown && !from_reset_pin && !from_button_wake) { enterDeepSleep(); } - // Boot indicator: both LEDs full bright for 1 second. + // 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 (LEDs off), then start the boot-phase indicator: red solid - // on, blue flickering like a disk-drive activity LED. The flicker runs on - // a TIMER2 interrupt so it keeps going through every blocking call in - // setup() (the_mesh.begin() etc.). It's stopped by bootComplete() at the end. + // 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(); @@ -155,27 +138,27 @@ void ThinkNodeM6Board::powerOff() { digitalWrite(P_LORA_TX_LED, LOW); #endif - // Shutdown cue: red full bright for 1 s. + // 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); - - // Final "goodbye": brief both-LED flash so the user knows we're committed - // and the device is going dark right now. analogWrite(PIN_LED_RED, 255); analogWrite(PIN_LED_BLUE, 255); delay(50); analogWrite(PIN_LED_RED, 0); analogWrite(PIN_LED_BLUE, 0); - // If the button is still held, we can't yet enter SYSTEMOFF (SENSE-LOW - // would wake us immediately). Wait silently for release — no LEDs lit, - // so the wait is invisible. + // 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(); } @@ -196,9 +179,7 @@ uint16_t ThinkNodeM6Board::getBattMilliVolts() { } void ThinkNodeM6Board::bootComplete() { - // Stop the flicker timer, turn red off, pause 1 second (everything dark) - // to clearly delimit the boot phase, then flash blue for 100 ms as the - // "device is up" signal. + // 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); diff --git a/variants/thinknode_m6/ThinkNodeM6Board.h b/variants/thinknode_m6/ThinkNodeM6Board.h index 26b2d89e20..6e4c9e062e 100644 --- a/variants/thinknode_m6/ThinkNodeM6Board.h +++ b/variants/thinknode_m6/ThinkNodeM6Board.h @@ -23,9 +23,8 @@ class ThinkNodeM6Board : public NRF52BoardDCDC { void begin(); uint16_t getBattMilliVolts() override; - // Called at the very end of setup() to mark "boot complete": stops the - // disk-activity-style blue flicker started in begin(), turns the red LED - // off, and flashes the blue LED for 100 ms. + // Called at the end of setup(). Stops the disk-activity blue flicker + // started in begin() and flashes the blue LED briefly. void bootComplete(); #if defined(P_LORA_TX_LED) diff --git a/variants/thinknode_m6/variant.cpp b/variants/thinknode_m6/variant.cpp index 4c1ac98262..1473db4b10 100644 --- a/variants/thinknode_m6/variant.cpp +++ b/variants/thinknode_m6/variant.cpp @@ -3,35 +3,27 @@ #include "wiring_digital.h" #include "nrf.h" -// Capture NRF_POWER->RESETREAS before SystemInit() runs the Nordic errata-136 -// workaround that clears the RESETPIN bit. Priority 101 places this before -// SystemInit (102) and all C++ static constructors. Exposed via the globals -// below so ThinkNodeM6Board::begin() can decide whether to boot or re-sleep. +// 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). // -// We also leverage GPREGRET2 (an 8-bit retained register) to detect full -// power loss: the register persists through SYSTEMOFF but is cleared when -// the chip loses power completely. By stamping a magic value on every boot, -// the next boot can tell "we cleanly went to SYSTEMOFF" (magic still there) -// from "battery died and just came back" (magic cleared). The latter case -// boots automatically — important for unattended solar-powered deployments. +// g_m6_reset_reason: snapshot of NRF_POWER->RESETREAS. +// g_m6_was_shutdown: true if GPREGRET2 holds the "user-intent" magic byte. // -// NOTE: GPREGRET2 is also written by NRF52Board::enterSystemOff() when -// NRF52_POWER_MANAGEMENT is defined. That build flag is NOT set for any M6 -// env today, so there is no conflict. If it's ever enabled for the M6, the -// magic value here will collide with the shutdown-reason byte stored there. -#define M6_GPREGRET2_MAGIC 0xA5 +// 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_powered = false; +volatile bool g_m6_was_shutdown = false; extern "C" __attribute__((constructor(101))) void m6_capture_resetreas(void) { g_m6_reset_reason = NRF_POWER->RESETREAS; - // Clear sticky bits so the next boot sees a fresh state. - NRF_POWER->RESETREAS = 0xFFFFFFFFul; - - // GPREGRET2 magic value: present = firmware ran in this power session - // (we cleanly went to SYSTEMOFF and came back); absent = power was lost. - g_m6_was_powered = (NRF_POWER->GPREGRET2 == M6_GPREGRET2_MAGIC); - NRF_POWER->GPREGRET2 = M6_GPREGRET2_MAGIC; // stamp it for next boot + NRF_POWER->RESETREAS = 0xFFFFFFFFul; // clear sticky bits + g_m6_was_shutdown = (NRF_POWER->GPREGRET2 == 0xA5); } const uint32_t g_ADigitalPinMap[] = { From 6b4dfbbfcc6061dc24a265e11306d4917269f35b Mon Sep 17 00:00:00 2001 From: AtlavoxDev Date: Fri, 22 May 2026 12:30:30 -0400 Subject: [PATCH 5/6] Clean up main.cpp Remove device-specific code from the universal main.cpp file --- examples/simple_repeater/main.cpp | 71 ++-------------------- variants/thinknode_m6/ThinkNodeM6Board.cpp | 62 +++++++++++++++++++ variants/thinknode_m6/ThinkNodeM6Board.h | 9 +++ 3 files changed, 75 insertions(+), 67 deletions(-) diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 00f2624b03..ded847af6d 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -28,36 +28,12 @@ static unsigned long userBtnDownAt = 0; #define USER_BTN_HOLD_OFF_MILLIS 1500 #endif -#if defined(PIN_USER_BTN) && defined(THINKNODE_M6) -static unsigned long m6BtnDownAt = 0; -// Long-press shutdown: red LED flashes briefly at 0 s and 1 s during the -// hold, then board.powerOff() runs the final shutdown cue at 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 - -// Quick tap (press+release) sends an advert; blue LED blinks Morse "A". -#define M6_TAP_MIN_MS 30 // debounce floor -#define M6_TAP_MAX_MS 500 // anything longer is a hold attempt -#define M6_MORSE_DOT_MS 200 -#define M6_MORSE_GAP_MS 200 -#define M6_MORSE_DASH_MS 600 -#endif void setup() { Serial.begin(115200); - -#ifdef THINKNODE_M6 - // M6's board.begin() drives the boot LED sequence; run it before any - // pre-setup delays so the LEDs come on immediately at wake. - board.begin(); -#else delay(1000); + board.begin(); -#endif #if defined(MESH_DEBUG) && defined(NRF52_PLATFORM) // give some extra time for serial to settle so @@ -176,48 +152,9 @@ void loop() { } #endif -#if defined(PIN_USER_BTN) && defined(THINKNODE_M6) - // Hold Function Button to power off the ThinkNode M6. - { - int btnState = digitalRead(PIN_USER_BTN); - if (btnState == LOW) { - if (m6BtnDownAt == 0) { - m6BtnDownAt = millis(); - } - unsigned long held = millis() - m6BtnDownAt; - - if (held >= M6_OFF_COMMIT_MS) { - Serial.println("Powering off..."); - board.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. Quick press+release = tap (send advert). Longer - // holds that don't reach the commit threshold are silent cancels. - if (m6BtnDownAt != 0) { - unsigned long held = millis() - m6BtnDownAt; - analogWrite(PIN_LED_RED, 0); - digitalWrite(PIN_LED_BLUE, LOW); - - if (held >= M6_TAP_MIN_MS && held < M6_TAP_MAX_MS) { - // Tap → advert + Morse "A" on blue. digitalWrite keeps blue in - // pure GPIO mode so the LoRa TX LED indicator still works. - Serial.println("Tap -> sending advert"); - the_mesh.sendSelfAdvertisement(16000, false); - digitalWrite(PIN_LED_BLUE, HIGH); delay(M6_MORSE_DOT_MS); - digitalWrite(PIN_LED_BLUE, LOW); delay(M6_MORSE_GAP_MS); - digitalWrite(PIN_LED_BLUE, HIGH); delay(M6_MORSE_DASH_MS); - digitalWrite(PIN_LED_BLUE, LOW); - } - } - m6BtnDownAt = 0; - } +#ifdef THINKNODE_M6 + if (board.checkButton()) { + the_mesh.sendSelfAdvertisement(16000, false); } #endif diff --git a/variants/thinknode_m6/ThinkNodeM6Board.cpp b/variants/thinknode_m6/ThinkNodeM6Board.cpp index d42efdecdb..b378a3174d 100644 --- a/variants/thinknode_m6/ThinkNodeM6Board.cpp +++ b/variants/thinknode_m6/ThinkNodeM6Board.cpp @@ -5,6 +5,23 @@ #include +// Function-button timing constants (used by checkButton()). +// Long-press shutdown: 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 + +// Quick tap (press+release) sends an advert; blue blinks Morse "A". +#define M6_TAP_MIN_MS 30 // debounce floor +#define M6_TAP_MAX_MS 500 // anything longer is a hold attempt +#define M6_MORSE_DOT_MS 200 +#define M6_MORSE_GAP_MS 200 +#define M6_MORSE_DASH_MS 600 + // 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.). @@ -189,4 +206,49 @@ void ThinkNodeM6Board::bootComplete() { digitalWrite(PIN_LED_BLUE, LOW); } +bool ThinkNodeM6Board::checkButton() { + 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); + } + return false; + } + + // Button released. Quick press+release = tap (send advert). Longer + // holds that don't reach the commit threshold are silent cancels. + bool was_tap = false; + if (_btn_down_at != 0) { + unsigned long held = millis() - _btn_down_at; + analogWrite(PIN_LED_RED, 0); + digitalWrite(PIN_LED_BLUE, LOW); + + if (held >= M6_TAP_MIN_MS && held < M6_TAP_MAX_MS) { + // Tap → run Morse "A" ack on blue. digitalWrite keeps blue in pure + // GPIO mode so the LoRa TX LED indicator still works afterwards. + Serial.println("Tap -> sending advert"); + digitalWrite(PIN_LED_BLUE, HIGH); delay(M6_MORSE_DOT_MS); + digitalWrite(PIN_LED_BLUE, LOW); delay(M6_MORSE_GAP_MS); + digitalWrite(PIN_LED_BLUE, HIGH); delay(M6_MORSE_DASH_MS); + digitalWrite(PIN_LED_BLUE, LOW); + was_tap = true; + } + } + _btn_down_at = 0; + return was_tap; +} + #endif diff --git a/variants/thinknode_m6/ThinkNodeM6Board.h b/variants/thinknode_m6/ThinkNodeM6Board.h index 6e4c9e062e..cdad4a1eb3 100644 --- a/variants/thinknode_m6/ThinkNodeM6Board.h +++ b/variants/thinknode_m6/ThinkNodeM6Board.h @@ -18,6 +18,9 @@ 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(); @@ -27,6 +30,12 @@ class ThinkNodeM6Board : public NRF52BoardDCDC { // started in begin() and flashes the blue LED briefly. void bootComplete(); + // Polls the function button. Drives all LED feedback during a hold, + // calls powerOff() internally on a long press, and runs the Morse "A" + // ack blink on a quick tap. + // Returns true on a tap — caller should broadcast a self-advertisement. + bool checkButton(); + #if defined(P_LORA_TX_LED) void onBeforeTransmit() override { digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on From 53d663f084b0683ca58876bb1f4783ee1f258ee9 Mon Sep 17 00:00:00 2001 From: AtlavoxDev Date: Fri, 22 May 2026 13:26:10 -0400 Subject: [PATCH 6/6] Tread lightly in main.cpp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The M6 follows a cleaner version of the existing SenseCAP Solar pattern: device-specific button logic lives in the variant (ThinkNodeM6Board::pollButton()), with a minimal dispatch call in main.cpp. The buttonStateChanged() methods on ThinkNodeM3Board, T1000eBoard, and PromicroBoard were defined but never connected to any caller — those boards could adopt the same pollButton() pattern in follow-up PRs if button UX is wanted. --- examples/simple_repeater/main.cpp | 5 +-- variants/thinknode_m6/ThinkNodeM6Board.cpp | 43 +++++----------------- variants/thinknode_m6/ThinkNodeM6Board.h | 8 ++-- 3 files changed, 14 insertions(+), 42 deletions(-) diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index ded847af6d..bcfce951a7 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -28,7 +28,6 @@ static unsigned long userBtnDownAt = 0; #define USER_BTN_HOLD_OFF_MILLIS 1500 #endif - void setup() { Serial.begin(115200); delay(1000); @@ -153,9 +152,7 @@ void loop() { #endif #ifdef THINKNODE_M6 - if (board.checkButton()) { - the_mesh.sendSelfAdvertisement(16000, false); - } + board.pollButton(); #endif the_mesh.loop(); diff --git a/variants/thinknode_m6/ThinkNodeM6Board.cpp b/variants/thinknode_m6/ThinkNodeM6Board.cpp index b378a3174d..e1cd86acd9 100644 --- a/variants/thinknode_m6/ThinkNodeM6Board.cpp +++ b/variants/thinknode_m6/ThinkNodeM6Board.cpp @@ -5,9 +5,9 @@ #include -// Function-button timing constants (used by checkButton()). -// Long-press shutdown: brief red blink at 0 s and 1 s during the hold, -// then board.powerOff() runs the final cue when held past 2 s. +// 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 @@ -15,13 +15,6 @@ #define M6_OFF_COMMIT_MS 2000 #define M6_OFF_FLASH_BRIGHT 128 // ~50% of 255 -// Quick tap (press+release) sends an advert; blue blinks Morse "A". -#define M6_TAP_MIN_MS 30 // debounce floor -#define M6_TAP_MAX_MS 500 // anything longer is a hold attempt -#define M6_MORSE_DOT_MS 200 -#define M6_MORSE_GAP_MS 200 -#define M6_MORSE_DASH_MS 600 - // 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.). @@ -206,7 +199,7 @@ void ThinkNodeM6Board::bootComplete() { digitalWrite(PIN_LED_BLUE, LOW); } -bool ThinkNodeM6Board::checkButton() { +void ThinkNodeM6Board::pollButton() { int btnState = digitalRead(PIN_USER_BTN); if (btnState == LOW) { if (_btn_down_at == 0) { @@ -225,30 +218,14 @@ bool ThinkNodeM6Board::checkButton() { analogWrite(PIN_LED_RED, 0); digitalWrite(PIN_LED_BLUE, LOW); } - return false; - } - - // Button released. Quick press+release = tap (send advert). Longer - // holds that don't reach the commit threshold are silent cancels. - bool was_tap = false; - if (_btn_down_at != 0) { - unsigned long held = millis() - _btn_down_at; - analogWrite(PIN_LED_RED, 0); - digitalWrite(PIN_LED_BLUE, LOW); - - if (held >= M6_TAP_MIN_MS && held < M6_TAP_MAX_MS) { - // Tap → run Morse "A" ack on blue. digitalWrite keeps blue in pure - // GPIO mode so the LoRa TX LED indicator still works afterwards. - Serial.println("Tap -> sending advert"); - digitalWrite(PIN_LED_BLUE, HIGH); delay(M6_MORSE_DOT_MS); - digitalWrite(PIN_LED_BLUE, LOW); delay(M6_MORSE_GAP_MS); - digitalWrite(PIN_LED_BLUE, HIGH); delay(M6_MORSE_DASH_MS); - digitalWrite(PIN_LED_BLUE, LOW); - was_tap = true; + } 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; } - _btn_down_at = 0; - return was_tap; } #endif diff --git a/variants/thinknode_m6/ThinkNodeM6Board.h b/variants/thinknode_m6/ThinkNodeM6Board.h index cdad4a1eb3..a209ac9101 100644 --- a/variants/thinknode_m6/ThinkNodeM6Board.h +++ b/variants/thinknode_m6/ThinkNodeM6Board.h @@ -30,11 +30,9 @@ class ThinkNodeM6Board : public NRF52BoardDCDC { // started in begin() and flashes the blue LED briefly. void bootComplete(); - // Polls the function button. Drives all LED feedback during a hold, - // calls powerOff() internally on a long press, and runs the Morse "A" - // ack blink on a quick tap. - // Returns true on a tap — caller should broadcast a self-advertisement. - bool checkButton(); + // 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 {