Skip to content
Open
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
54 changes: 54 additions & 0 deletions variants/thinknode_m6/ThinkNodeM6Board.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "ThinkNodeM6Board.h"

#include <Arduino.h>

#ifdef THINKNODE_M6
Expand All @@ -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;

Expand Down
32 changes: 32 additions & 0 deletions variants/thinknode_m6/ThinkNodeM6Board.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -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();
Expand Down
14 changes: 12 additions & 2 deletions variants/thinknode_m6/target.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 19 additions & 1 deletion variants/thinknode_m6/target.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,28 @@
#include <helpers/ui/MomentaryButton.h>
#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;
Expand Down