Merge upstream/dev into observer-firmware-dev

Preserve observer discovery when forwarding is disabled while honoring hidden-node settings. Keep GPS power ownership paired and incorporate upstream GPS diagnostics.
This commit is contained in:
agessaman committed 2026-10-13 19:10:10 -07:00
commit 7bab7e0e73
42 files changed
+582 -125

No files matched your search

+6 -6
View File
@@ -87,13 +87,14 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
}
if (self_id.isHashMatch(pkt->path, pkt->getPathHashSize()) && allowPacketForward(pkt)) {
uint32_t d = getDirectRetransmitDelay(pkt);
if (pkt->getPayloadType() == PAYLOAD_TYPE_MULTIPART) {
return forwardMultipartDirect(pkt);
return forwardMultipartDirect(pkt, d);
} else if (pkt->getPayloadType() == PAYLOAD_TYPE_ACK) {
if (!_tables->wasSeen(pkt)) { // don't retransmit!
_tables->markSeen(pkt);
removeSelfFromPath(pkt);
routeDirectRecvAcks(pkt, 0);
routeDirectRecvAcks(pkt, d);
}
return ACTION_RELEASE;
}
@@ -102,7 +103,6 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) {
_tables->markSeen(pkt);
removeSelfFromPath(pkt);
uint32_t d = getDirectRetransmitDelay(pkt);
return ACTION_RETRANSMIT_DELAYED(0, d); // Routed traffic is HIGHEST priority
}
}
@@ -356,7 +356,7 @@ DispatcherAction Mesh::routeRecvPacket(Packet* packet) {
return ACTION_RELEASE;
}
DispatcherAction Mesh::forwardMultipartDirect(Packet* pkt) {
DispatcherAction Mesh::forwardMultipartDirect(Packet* pkt, uint32_t delay_millis) {
uint8_t remaining = pkt->payload[0] >> 4; // num of packets in this multipart sequence still to be sent
uint8_t type = pkt->payload[0] & 0x0F;
@@ -370,7 +370,7 @@ DispatcherAction Mesh::forwardMultipartDirect(Packet* pkt) {
if (!_tables->wasSeen(&tmp)) { // don't retransmit!
_tables->markSeen(&tmp);
removeSelfFromPath(&tmp);
routeDirectRecvAcks(&tmp, ((uint32_t)remaining + 1) * 300); // expect multipart ACKs 300ms apart (x2)
routeDirectRecvAcks(&tmp, delay_millis + ((uint32_t)remaining + 1) * 300); // expect multipart ACKs 300ms apart (x2)
}
}
return ACTION_RELEASE;
@@ -380,13 +380,13 @@ void Mesh::routeDirectRecvAcks(Packet* packet, uint32_t delay_millis) {
if (!packet->isMarkedDoNotRetransmit()) {
uint8_t extra = getExtraAckTransmitCount();
while (extra > 0) {
delay_millis += getDirectRetransmitDelay(packet) + 300;
auto a1 = createMultiAck(packet->payload, packet->payload_len, extra);
if (a1) {
a1->path_len = Packet::copyPath(a1->path, packet->path, packet->path_len);
a1->header &= ~PH_ROUTE_MASK;
a1->header |= ROUTE_TYPE_DIRECT;
sendPacket(a1, 0, delay_millis);
delay_millis += 300;
}
extra--;
}
+1 -1
View File
@@ -32,7 +32,7 @@ class Mesh : public Dispatcher {
void removeSelfFromPath(Packet* packet);
void routeDirectRecvAcks(Packet* packet, uint32_t delay_millis);
//void routeRecvAcks(Packet* packet, uint32_t delay_millis);
DispatcherAction forwardMultipartDirect(Packet* pkt);
DispatcherAction forwardMultipartDirect(Packet* pkt, uint32_t delay_millis);
protected:
DispatcherAction onRecvPacket(Packet* pkt) override;
+10
View File
@@ -80,6 +80,16 @@ public:
// Power management interface (boards with power management override these)
virtual bool isPwrMgtInitialised() const { return false; }
virtual bool isExternalPowered() { return false; }
// True while a board can identify an active battery-charging source.
virtual bool isChargerActive() { return false; }
// Optional, source-specific power detection. Boards that can distinguish a USB
// supply from a solar charger override these; defaults keep every other board
// unaffected (the generic CLI prints "n/a" when a capability is absent).
virtual bool hasUsbPowerDetect() const { return false; }
virtual bool isUsbPowered() { return false; }
virtual bool hasSolarChargerDetect() const { return false; }
virtual bool isSolarChargerActive() { return false; }
virtual uint16_t getBootVoltage() { return 0; }
virtual bool getWakeLpcompSupported() const { return false; }
virtual uint32_t getResetReason() const { return 0; }
+5 -2
View File
@@ -147,6 +147,9 @@ int Utils::encryptThenMAC(const uint8_t* shared_secret, uint8_t* dest, const uin
int Utils::MACThenDecrypt(const uint8_t* shared_secret, uint8_t* dest, const uint8_t* src, int src_len) {
if (src_len <= CIPHER_MAC_SIZE) return 0; // invalid src bytes
int enc_len = src_len - CIPHER_MAC_SIZE;
if (enc_len % CIPHER_BLOCK_SIZE != 0) return 0; // reject non-block-aligned ciphertext
uint8_t hmac[CIPHER_MAC_SIZE];
#ifdef USE_CC310_HW_CRYPTO
{
@@ -161,12 +164,12 @@ int Utils::MACThenDecrypt(const uint8_t* shared_secret, uint8_t* dest, const uin
{
SHA256 sha;
sha.resetHMAC(shared_secret, PUB_KEY_SIZE);
sha.update(src + CIPHER_MAC_SIZE, src_len - CIPHER_MAC_SIZE);
sha.update(src + CIPHER_MAC_SIZE, enc_len);
sha.finalizeHMAC(shared_secret, PUB_KEY_SIZE, hmac, CIPHER_MAC_SIZE);
}
#endif
if (memcmp(hmac, src, CIPHER_MAC_SIZE) == 0) {
return decrypt(shared_secret, dest, src + CIPHER_MAC_SIZE, src_len - CIPHER_MAC_SIZE);
return decrypt(shared_secret, dest, src + CIPHER_MAC_SIZE, enc_len);
}
return 0; // invalid HMAC
}
+1 -1
View File
@@ -50,7 +50,7 @@ public:
/**
* \brief checks the MAC (in leading bytes of 'src'), then if valid, decrypts remaining bytes in src.
* \returns zero if MAC is invalid, otherwise the length of decrypted bytes in 'dest'
* \returns zero if MAC is invalid or ciphertext is not block-aligned, otherwise the length of decrypted bytes in 'dest'
*/
static int MACThenDecrypt(const uint8_t* shared_secret, uint8_t* dest, const uint8_t* src, int src_len);
+1 -1
View File
@@ -9,7 +9,7 @@
#include "ContactInfo.h"
#define MAX_SEARCH_RESULTS 8
#define MAX_SEARCH_RESULTS 24
#define MSG_SEND_FAILED 0
#define MSG_SEND_SENT_FLOOD 1
+27
View File
@@ -1360,6 +1360,19 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
sprintf(reply, "%s (Build: %s)", _callbacks->getFirmwareVer(), _callbacks->getBuildDate());
} else if (memcmp(command, "board", 5) == 0) {
sprintf(reply, "%s", _board->getManufacturerName());
} else if (strcmp(command, "power") == 0) {
const char* usb_state = _board->hasUsbPowerDetect()
? (_board->isUsbPowered() ? "yes" : "no")
: "n/a";
const char* solar_state = _board->hasSolarChargerDetect()
? (_board->isSolarChargerActive() ? "yes" : "no")
: "n/a";
snprintf(reply, 160, "batt:%u mV usb:%s solar_chg:%s ext:%s charger:%s",
(unsigned)_board->getBattMilliVolts(),
usb_state,
solar_state,
_board->isExternalPowered() ? "yes" : "no",
_board->isChargerActive() ? "yes" : "no");
} else if (memcmp(command, "sensor get ", 11) == 0) {
const char* key = command + 11;
const char* val = _sensors->getSettingByKey(key);
@@ -1431,6 +1444,20 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
} else {
strcpy(reply, "gps provider not found");
}
} else if (strcmp(command, "gps diag") == 0) {
LocationProvider * l = _sensors->getLocationProvider();
if (l != NULL) {
const char* setting = _sensors->getSettingByKey("gps");
bool requested = setting != NULL && strcmp(setting, "1") == 0;
int prefix_len = snprintf(reply, 160, "req:%u ", requested ? 1U : 0U);
if (prefix_len < 0 || prefix_len >= 160) {
reply[159] = 0;
} else {
l->formatDiagnostics(reply + prefix_len, 160 - prefix_len);
}
} else {
strcpy(reply, "gps provider not found");
}
} else if (memcmp(command, "gps setloc", 10) == 0) {
_prefs->node_lat = _sensors->node_lat;
_prefs->node_lon = _sensors->node_lon;
+26 -5
View File
@@ -208,19 +208,30 @@ public:
}
};
static RTC_NOINIT_ATTR uint32_t _rtc_backup_time;
static RTC_NOINIT_ATTR uint32_t _rtc_backup_magic;
#define RTC_BACKUP_MAGIC 0xAA55CC33
#define RTC_TIME_MIN 1772323200 // 1 Mar 2026
class ESP32RTCClock : public mesh::RTCClock {
public:
ESP32RTCClock() { }
void begin() {
esp_reset_reason_t reason = esp_reset_reason();
if (reason == ESP_RST_POWERON) {
// start with some date/time in the recent past
struct timeval tv;
tv.tv_sec = 1715770351; // 15 May 2024, 8:50pm
if (reason == ESP_RST_DEEPSLEEP) {
return; // ESP-IDF preserves system time across deep sleep
}
// All other resets (power-on, crash, WDT, brownout) lose system time.
// Restore from RTC backup if valid, otherwise use hardcoded seed.
struct timeval tv;
if (_rtc_backup_magic == RTC_BACKUP_MAGIC && _rtc_backup_time > RTC_TIME_MIN) {
tv.tv_sec = _rtc_backup_time;
} else {
tv.tv_sec = RTC_TIME_MIN;
}
tv.tv_usec = 0;
settimeofday(&tv, NULL);
}
}
uint32_t getCurrentTime() override {
time_t _now;
time(&_now);
@@ -231,6 +242,16 @@ public:
tv.tv_sec = time;
tv.tv_usec = 0;
settimeofday(&tv, NULL);
_rtc_backup_time = time;
_rtc_backup_magic = RTC_BACKUP_MAGIC;
}
void tick() override {
time_t now;
time(&now);
if (now > RTC_TIME_MIN && (uint32_t)now != _rtc_backup_time) {
_rtc_backup_time = (uint32_t)now;
_rtc_backup_magic = RTC_BACKUP_MAGIC;
}
}
};
+1
View File
@@ -188,6 +188,7 @@ int RegionMap::getTransportKeysFor(const RegionEntry& src, TransportKey dest[],
}
RegionEntry* RegionMap::findMatch(mesh::Packet* packet, uint8_t mask) {
if (packet->transport_codes[0] == 0) return &wildcard;
for (int i = 0; i < num_regions; i++) {
auto region = &regions[i];
if ((region->flags & mask) == 0) { // does region allow this? (per 'mask' param)
+6 -2
View File
@@ -129,8 +129,12 @@ void SerialBLEInterface::begin(const char* prefix, char* name, uint32_t pin_code
char charpin[20];
snprintf(charpin, sizeof(charpin), "%lu", (unsigned long)pin_code);
// If we want to control BLE LED ourselves, uncomment this:
// Bluefruit.autoConnLed(false);
#ifdef DISABLE_BLE_LED // use variant platformio.ini build_flags macro: -D DISABLE_BLE_LED
Bluefruit.autoConnLed(false); // false removes the automatic control of the LED by the Bluefruit library
#else
Bluefruit.autoConnLed(true); // true leaves the default automatic control of the LED by the Bluefruit library
#endif
Bluefruit.configPrphBandwidth(BANDWIDTH_MAX);
Bluefruit.begin();
+29
View File
@@ -9,6 +9,8 @@ class CustomLR1121 : public LR1121 {
uint32_t _activityAt = 0;
bool _headerSeen = false;
bool _rx_boosted = false;
int8_t _txPower = 0;
bool _txPowerSet = false;
public:
CustomLR1121(Module *mod) : LR1121(mod) { }
@@ -36,6 +38,33 @@ class CustomLR1121 : public LR1121 {
return len;
}
using LR1121::setOutputPower;
using LR1121::setFrequency;
int16_t setOutputPower(int8_t power) override {
#ifdef LR1121_FORCE_HP_PA
const bool force_hp = true; // boards with only RFO_HP_LF wired: never select the LP PA
#else
const bool force_hp = false;
#endif
// clip to the active PA's range (HF PA tops out at 13 dBm) rather than fail and keep the other band's PA
int8_t clipped = power;
checkOutputPower(power, &clipped, force_hp);
int16_t state = LR1121::setOutputPower(clipped, force_hp);
if (state == RADIOLIB_ERR_NONE) {
_txPower = power;
_txPowerSet = true;
}
return state;
}
int16_t setFrequency(float freq) override {
int16_t state = LR1121::setFrequency(freq);
// PA selection depends on the band, and set radio / tempradio don't call setTxPower
if (state == RADIOLIB_ERR_NONE && _txPowerSet) state = setOutputPower(_txPower);
return state;
}
float getFreqMHz() const { return freqMHz; }
int16_t setRxBoostedGainMode(bool en) {
+19
View File
@@ -0,0 +1,19 @@
#pragma once
#include <helpers/sensors/LocationProvider.h>
// Board must keep the GPS RTC backup pin powered and can cut GPS power once we return.
// Returns true if the command was acked within 3 attempts, otherwise returns false.
static inline bool airohaEnterSleep(LocationProvider* nmea) {
for (uint8_t attempt = 0; attempt < 3; attempt++) {
nmea->drain();
nmea->sendSentence("$PAIR650,0");
if (nmea->waitFor("$PAIR001,650,0", 50)) { // wait for the command received signal
#ifdef GPS_NMEA_DEBUG
Serial.printf("Airoha RTC Backup sleep command accepted by GPS after %u attempts\r\n", attempt + 1);
#endif
nmea->waitFor("$PAIR650,0", 50); // give the GPS 50ms grace to signal it is ready for sleep
return true;
}
}
return false;
}
@@ -765,14 +765,18 @@ void EnvironmentSensorManager::initBasicGPS() {
MESH_DEBUG_PRINTLN("No GPS wake/reset pin found for this board. Continuing on...");
#endif
// Give GPS a moment to power up and send data
delay(1000);
// We'll consider GPS detected if we see any data on Serial1
// Give the GPS time to power up and emit its first NMEA sentence. Cold-start
// modules can take several seconds, so poll for up to ~5s instead of checking
// once after a fixed 1s delay -- returning as soon as any data appears, so
// fast modules add no boot delay (issue #2258).
#ifdef ENV_SKIP_GPS_DETECT
gps_detected = true;
#else
gps_detected = (Serial1.available() > 0);
gps_detected = false;
for (int i = 0; i < 20 && !gps_detected; i++) {
delay(250);
gps_detected = (Serial1.available() > 0);
}
#endif
if (gps_detected) {
+14
View File
@@ -1,6 +1,8 @@
#pragma once
#include "Mesh.h"
#include <stddef.h>
#include <stdio.h>
class LocationProvider {
@@ -17,9 +19,21 @@ public:
virtual bool isValid() = 0;
virtual long getTimestamp() = 0;
virtual void sendSentence(const char * sentence);
virtual bool waitFor(const char* prefix, uint32_t timeout_ms) { return false; }
virtual void drain() { }
virtual void reset() = 0;
virtual void begin() = 0;
virtual void stop() = 0;
virtual void loop() = 0;
virtual bool isEnabled() = 0;
// Format compact diagnostics in the caller-provided buffer. Providers that
// have more information (for example UART counters) can override this.
virtual void formatDiagnostics(char* out, size_t out_size) {
if (out_size == 0) return;
snprintf(out, out_size, "en:%u sat:%ld fix:%u",
isEnabled() ? 1U : 0U,
satellitesCount(),
isValid() ? 1U : 0U);
}
};
+119 -1
View File
@@ -4,6 +4,7 @@
#include <MicroNMEA.h>
#include <RTClib.h>
#include <helpers/RefCountedDigitalPin.h>
#include <string.h>
#ifndef GPS_EN
#ifdef PIN_GPS_EN
@@ -49,6 +50,42 @@ class MicroNMEALocationProvider : public LocationProvider {
unsigned long next_check = 0;
long time_valid = 0;
#ifdef GPS_L76K
void configureL76K() {
// Reapply the constellation mode at each provider start instead of
// assuming it survives a receiver power cycle.
// Mode 7 enables GPS + BeiDou + GLONASS.
delay(250);
MicroNMEA::sendSentence(*_gps_serial, "$PCAS04,7");
delay(250);
}
#endif
// Observation-only diagnostics. These fields do not change GPS power,
// timing, reset behaviour, or parser input.
uint32_t _uart_bytes = 0;
uint32_t _nmea_checksum_ok = 0;
uint32_t _nmea_checksum_bad = 0;
uint32_t _begin_calls = 0;
uint32_t _stop_calls = 0;
unsigned long _last_uart_ms = 0;
unsigned long _last_nmea_ms = 0;
unsigned long _last_fix_ms = 0;
bool _uart_seen = false;
bool _nmea_seen = false;
bool _fix_seen = false;
static uint32_t ageMs(unsigned long timestamp) {
return (uint32_t)(millis() - timestamp);
}
static void formatAge(char* out, size_t out_size, bool seen, unsigned long timestamp) {
if (!seen) {
snprintf(out, out_size, "never");
} else {
snprintf(out, out_size, "%lu", (unsigned long)ageMs(timestamp));
}
}
public :
MicroNMEALocationProvider(Stream& ser, mesh::RTCClock* clock = NULL, int pin_reset = GPS_RESET, int pin_en = GPS_EN,RefCountedDigitalPin* peripher_power=NULL) :
nmea(_nmeaBuffer, sizeof(_nmeaBuffer)), _clock(clock), _gps_serial(&ser), _peripher_power(peripher_power), _pin_reset(pin_reset), _pin_en(pin_en) {
@@ -74,6 +111,7 @@ public :
}
void begin() override {
_begin_calls++;
if (_peripher_power) _peripher_power->claim();
if (_pin_en != -1) {
digitalWrite(_pin_en, GPS_EN_ACTIVE);
@@ -89,9 +127,15 @@ public :
delay(10);
digitalWrite(_pin_reset, !GPS_RESET_ACTIVE);
}
#ifdef GPS_L76K
// EnvironmentSensorManager calls reset() immediately after begin().
// Configure only after that reset so the setting is not discarded.
configureL76K();
#endif
}
void stop() override {
_stop_calls++;
if (_pin_en != -1) {
digitalWrite(_pin_en, !GPS_EN_ACTIVE);
}
@@ -127,18 +171,65 @@ public :
return dt.unixtime();
}
void drain() { while (_gps_serial->available()) _gps_serial->read(); }
const char* getSentence() const { return nmea.getSentence(); }
void sendSentence(const char *sentence) override {
nmea.sendSentence(*_gps_serial, sentence);
}
bool waitFor(const char* prefix, uint32_t timeout_ms) {
size_t plen = strlen(prefix);
uint32_t timeout = millis() + timeout_ms;
while ((int32_t)(millis() - timeout) < 0) {
if (_gps_serial->available()) {
char c = _gps_serial->read();
#ifdef GPS_NMEA_DEBUG
Serial.print(c);
#endif
if (nmea.process(c) && strncmp(nmea.getSentence(), prefix, plen) == 0) return true;
} else {
yield();
}
}
return false;
}
void loop() override {
while (_gps_serial->available()) {
char c = _gps_serial->read();
_uart_bytes++;
_last_uart_ms = millis();
_uart_seen = true;
#ifdef GPS_NMEA_DEBUG
Serial.print(c);
#endif
nmea.process(c);
bool parsed = nmea.process(c);
// MicroNMEA leaves the completed sentence in its buffer after the
// first line terminator. CRLF therefore counts once: CR observes the
// sentence, then LF clears the now-empty buffer.
if ((c == '\0' || c == '\r' || c == '\n') && nmea.getSentence()[0] != '\0') {
if (MicroNMEA::testChecksum(nmea.getSentence())) {
_nmea_checksum_ok++;
_last_nmea_ms = millis();
_nmea_seen = true;
// Refresh fix age only when a newly parsed GGA/RMC sentence
// carries a valid fix. Other valid NMEA sentences must not
// make an old position look fresh.
const char* message_id = nmea.getMessageID();
if (parsed && isValid() &&
(strcmp(message_id, "GGA") == 0 || strcmp(message_id, "RMC") == 0)) {
_last_fix_ms = millis();
_fix_seen = true;
}
} else {
_nmea_checksum_bad++;
}
}
}
if (!isValid()) time_valid = 0;
@@ -156,4 +247,31 @@ public :
}
}
}
void formatDiagnostics(char* out, size_t out_size) override {
if (out_size == 0) return;
char uart_age[11];
char nmea_age[11];
char fix_age[11];
formatAge(uart_age, sizeof(uart_age), _uart_seen, _last_uart_ms);
formatAge(nmea_age, sizeof(nmea_age), _nmea_seen, _last_nmea_ms);
formatAge(fix_age, sizeof(fix_age), _fix_seen, _last_fix_ms);
// Compact enough for CommonCLI's 160-byte reply buffer, including the
// "req:" prefix added by CommonCLI.
snprintf(out, out_size,
"en:%u ub:%lu ua:%s ok:%lu bad:%lu na:%s sat:%ld fix:%u fa:%s bc:%lu sc:%lu",
isEnabled() ? 1U : 0U,
(unsigned long)_uart_bytes,
uart_age,
(unsigned long)_nmea_checksum_ok,
(unsigned long)_nmea_checksum_bad,
nmea_age,
satellitesCount(),
isValid() ? 1U : 0U,
fix_age,
(unsigned long)_begin_calls,
(unsigned long)_stop_calls);
}
};