Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions examples/simple_repeater/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -147,6 +151,10 @@ void loop() {
}
#endif

#ifdef THINKNODE_M6
board.pollButton();
#endif

the_mesh.loop();
sensors.loop();
#ifdef DISPLAY_CLASS
Expand Down
195 changes: 195 additions & 0 deletions variants/thinknode_m6/ThinkNodeM6Board.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,134 @@

#include <Wire.h>

// 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
Expand All @@ -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;

Expand All @@ -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
22 changes: 12 additions & 10 deletions variants/thinknode_m6/ThinkNodeM6Board.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
};
24 changes: 24 additions & 0 deletions variants/thinknode_m6/variant.cpp
Original file line number Diff line number Diff line change
@@ -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,
Expand Down