diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index a711ec0a51..ef86df5952 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -29,6 +29,16 @@ #define ADVERT_LON 0.0 #endif +// GPS defaults for a NEW install. Boards with a permanently-fitted GNSS (and no +// UI to switch it on) override these in their platformio.ini; every other board +// keeps the historical "GPS off, advertise the configured coordinates" defaults. +#ifndef GPS_ENABLED_DEFAULT + #define GPS_ENABLED_DEFAULT 0 +#endif +#ifndef ADVERT_LOC_POLICY_DEFAULT + #define ADVERT_LOC_POLICY_DEFAULT ADVERT_LOC_PREFS +#endif + #ifndef ADMIN_PASSWORD #define ADMIN_PASSWORD "password" #endif @@ -919,9 +929,9 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc StrHelper::strncpy(_prefs.bridge_secret, "LVSITANOS", sizeof(_prefs.bridge_secret)); // GPS defaults - _prefs.gps_enabled = 0; + _prefs.gps_enabled = GPS_ENABLED_DEFAULT; _prefs.gps_interval = 0; - _prefs.advert_loc_policy = ADVERT_LOC_PREFS; + _prefs.advert_loc_policy = ADVERT_LOC_POLICY_DEFAULT; _prefs.adc_multiplier = 0.0f; // 0.0f means use default board multiplier diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index 546d094fc8..fb66008f08 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -670,9 +670,9 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc #endif // GPS defaults - _prefs.gps_enabled = 0; + _prefs.gps_enabled = GPS_ENABLED_DEFAULT; _prefs.gps_interval = 0; - _prefs.advert_loc_policy = ADVERT_LOC_PREFS; + _prefs.advert_loc_policy = ADVERT_LOC_POLICY_DEFAULT; #if defined(USE_SX1262) || defined(USE_SX1268) #ifdef SX126X_RX_BOOSTED_GAIN diff --git a/examples/simple_room_server/MyMesh.h b/examples/simple_room_server/MyMesh.h index 5cf949c6bd..d376ee958e 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -61,6 +61,16 @@ #define ADVERT_LON 0.0 #endif +// GPS defaults for a NEW install. Boards with a permanently-fitted GNSS (and no +// UI to switch it on) override these in their platformio.ini; every other board +// keeps the historical "GPS off, advertise the configured coordinates" defaults. +#ifndef GPS_ENABLED_DEFAULT + #define GPS_ENABLED_DEFAULT 0 +#endif +#ifndef ADVERT_LOC_POLICY_DEFAULT + #define ADVERT_LOC_POLICY_DEFAULT ADVERT_LOC_PREFS +#endif + #ifndef ADMIN_PASSWORD #define ADMIN_PASSWORD "password" #endif diff --git a/variants/thinknode_m6/ThinkNodeM6Board.cpp b/variants/thinknode_m6/ThinkNodeM6Board.cpp index 8ebae64c64..88481eac2a 100644 --- a/variants/thinknode_m6/ThinkNodeM6Board.cpp +++ b/variants/thinknode_m6/ThinkNodeM6Board.cpp @@ -15,9 +15,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/platformio.ini b/variants/thinknode_m6/platformio.ini index ad7e6902cf..cfac5e2cd5 100644 --- a/variants/thinknode_m6/platformio.ini +++ b/variants/thinknode_m6/platformio.ini @@ -47,6 +47,16 @@ build_flags = -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' -D MAX_NEIGHBOURS=50 +; The L76K GNSS is soldered to the M6 and the enclosure is sealed, so there is +; no button or screen to switch GPS on with. Skip the 1s "is a GPS attached?" +; probe (it gates the 'gps' setting, and losing that race leaves GPS +; unreachable until reboot), enable GPS for new installs, and advertise the +; position the GNSS actually reports instead of the fixed 0,0 above. +; All of it stays changeable at runtime over the serial CLI: 'gps on'/'gps off', +; 'gps advert prefs|share|none', and bare 'gps' to print fix + satellite count. + -D ENV_SKIP_GPS_DETECT=1 + -D GPS_ENABLED_DEFAULT=1 + -D ADVERT_LOC_POLICY_DEFAULT=ADVERT_LOC_SHARE ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 ; -D GPS_NMEA_DEBUG=1 @@ -64,6 +74,13 @@ build_flags = -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' -D ROOM_PASSWORD='"hello"' +; Same reasoning as the repeater env above: the L76K is soldered on and the +; enclosure is sealed, so GPS has to come up enabled and advertise the position +; the GNSS actually reports. Runtime CLI still wins ('gps on'/'gps off', +; 'gps advert prefs|share|none'). + -D ENV_SKIP_GPS_DETECT=1 + -D GPS_ENABLED_DEFAULT=1 + -D ADVERT_LOC_POLICY_DEFAULT=ADVERT_LOC_SHARE ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${ThinkNode_M6.build_src_filter} 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;