diff --git a/docs/number_allocations.md b/docs/number_allocations.md index b8cc22a21..79cc90b84 100644 --- a/docs/number_allocations.md +++ b/docs/number_allocations.md @@ -19,6 +19,7 @@ Once you have a working app/project, you need to be able to demonstrate it exist | 0110 - 011F | Ripple | ripple_biz@protonmail.com — https://buymeacoffee.com/ripplebiz | | 0120 | MCO Advanced | most.original.address@gmail.com — https://hdden.ru/MCOa/ | | 0130 - 013F | StreamSensor | william@housedillon.com - https://housedillon.com/blog/lora-e5-with-seeed-fusion | +| 0140 | Nelos | support@nelos.app — https://nelos.app | | FF00 - FFFF | -reserved for testing/dev- | | (add rows, inside the range 0100 - FEFF for custom apps) diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 61cff7609..77d9a7b2a 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -944,7 +944,8 @@ bool MyMesh::onPeerPathRecv(mesh::Packet *packet, int sender_idx, const uint8_t void MyMesh::onControlDataRecv(mesh::Packet* packet) { uint8_t type = packet->payload[0] & 0xF0; // just test upper 4 bits - if (type == CTL_TYPE_NODE_DISCOVER_REQ && packet->payload_len >= 6 && discover_limiter.allow(rtc_clock.getCurrentTime())) { + if (type == CTL_TYPE_NODE_DISCOVER_REQ && packet->payload_len >= 6 + && !isHiddenNode() && discover_limiter.allow(rtc_clock.getCurrentTime())) { int i = 1; uint8_t filter = packet->payload[i++]; uint32_t tag; diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 8f2be8bf6..2ff77ad88 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -300,6 +300,9 @@ public: NodePrefs* getNodePrefs() { return &_prefs; } + bool isHiddenNode() const { + return _prefs.advert_interval == 0 && _prefs.flood_advert_interval == 0; + } #ifdef DISPLAY_ACTIVITY_DASHBOARD RadioActivityWindow* getActivityWindow() { return &_activity; } diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index 5c590e2e9..9d8931392 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -1554,6 +1554,8 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply addSystemPost(msg); snprintf(reply, MAX_POST_TEXT_LEN, "OK"); } + } else if (memcmp(command, "set repeat on", 13) == 0) { + strcpy(reply, "ERR - repeat now deprecated"); } else{ _cli.handleCommand(sender_timestamp, command, reply); // common CLI commands } diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index 6b7a32fef..8b421081b 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -43,7 +43,7 @@ #define FIRMWARE_ROLE "sensor" -#define MAX_SEARCH_RESULTS 8 +#define MAX_SEARCH_RESULTS 24 #define MAX_CONCURRENT_ALERTS 4 class SensorMesh : public mesh::Mesh, public CommonCLICallbacks { diff --git a/src/Mesh.cpp b/src/Mesh.cpp index c11f37cac..9dc42f0b0 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -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--; } diff --git a/src/Mesh.h b/src/Mesh.h index 49a299a6a..22bb5c6d5 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -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; diff --git a/src/MeshCore.h b/src/MeshCore.h index 7da86a254..2d78f1bb1 100644 --- a/src/MeshCore.h +++ b/src/MeshCore.h @@ -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; } diff --git a/src/Utils.cpp b/src/Utils.cpp index 7a3fb78b3..dc547bd2f 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -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 } diff --git a/src/Utils.h b/src/Utils.h index 7a0f7b6ee..0f1af39d8 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -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); diff --git a/src/helpers/BaseChatMesh.h b/src/helpers/BaseChatMesh.h index 0a8fdef4f..835384b74 100644 --- a/src/helpers/BaseChatMesh.h +++ b/src/helpers/BaseChatMesh.h @@ -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 diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 657157d1d..73b4a658a 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -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; diff --git a/src/helpers/ESP32Board.h b/src/helpers/ESP32Board.h index 5bb2b02d6..a0ec9e426 100644 --- a/src/helpers/ESP32Board.h +++ b/src/helpers/ESP32Board.h @@ -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; + } } }; diff --git a/src/helpers/RegionMap.cpp b/src/helpers/RegionMap.cpp index 4667e0038..d8d773803 100644 --- a/src/helpers/RegionMap.cpp +++ b/src/helpers/RegionMap.cpp @@ -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 = ®ions[i]; if ((region->flags & mask) == 0) { // does region allow this? (per 'mask' param) diff --git a/src/helpers/nrf52/SerialBLEInterface.cpp b/src/helpers/nrf52/SerialBLEInterface.cpp index 75a4e3b06..f7e5bbd99 100644 --- a/src/helpers/nrf52/SerialBLEInterface.cpp +++ b/src/helpers/nrf52/SerialBLEInterface.cpp @@ -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(); diff --git a/src/helpers/radiolib/CustomLR1121.h b/src/helpers/radiolib/CustomLR1121.h index 67e3b52a9..7182d1d5c 100644 --- a/src/helpers/radiolib/CustomLR1121.h +++ b/src/helpers/radiolib/CustomLR1121.h @@ -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) { diff --git a/src/helpers/sensors/AirohaSleep.h b/src/helpers/sensors/AirohaSleep.h new file mode 100644 index 000000000..6637d0fbf --- /dev/null +++ b/src/helpers/sensors/AirohaSleep.h @@ -0,0 +1,19 @@ +#pragma once +#include + +// 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; +} \ No newline at end of file diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index 6f4607751..0dc892c3a 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -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) { diff --git a/src/helpers/sensors/LocationProvider.h b/src/helpers/sensors/LocationProvider.h index 81d08652e..bc4f1fe76 100644 --- a/src/helpers/sensors/LocationProvider.h +++ b/src/helpers/sensors/LocationProvider.h @@ -1,6 +1,8 @@ #pragma once #include "Mesh.h" +#include +#include 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); + } }; diff --git a/src/helpers/sensors/MicroNMEALocationProvider.h b/src/helpers/sensors/MicroNMEALocationProvider.h index c9aee3b61..43899c989 100644 --- a/src/helpers/sensors/MicroNMEALocationProvider.h +++ b/src/helpers/sensors/MicroNMEALocationProvider.h @@ -4,6 +4,7 @@ #include #include #include +#include #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); + } }; diff --git a/variants/heltec_t1/variant.h b/variants/heltec_t1/variant.h index b52aeba0a..de40ea642 100644 --- a/variants/heltec_t1/variant.h +++ b/variants/heltec_t1/variant.h @@ -9,6 +9,12 @@ #define USE_LFXO #define VARIANT_MCK (64000000ul) +// This board has no external RTC chip -- the ICM-42607-P IMU sits at 0x68, +// the same I2C address DS3231 probing checks, so without this the IMU gets +// misidentified as a DS3231 and its unrelated registers get read/written +// as if they were time/status registers, producing a bogus, unfixable clock. +#define DISABLE_DS3231_PROBE + //////////////////////////////////////////////////////////////////////////////// // Number of pins diff --git a/variants/lilygo_t_impulse_plus/TImpulsePlusBoard.h b/variants/lilygo_t_impulse_plus/TImpulsePlusBoard.h index 30bbc3a5c..f7ef536ba 100644 --- a/variants/lilygo_t_impulse_plus/TImpulsePlusBoard.h +++ b/variants/lilygo_t_impulse_plus/TImpulsePlusBoard.h @@ -9,7 +9,7 @@ protected: uint8_t btn_prev_state; public: - TImpulsePlusBoard() : NRF52Board("T-Impulse-Plus OTA") {} + TImpulsePlusBoard() : NRF52Board("T-Impulse-Plus_OTA") {} void begin(); #if defined(P_LORA_TX_LED) diff --git a/variants/lilygo_techo_lite/platformio.ini b/variants/lilygo_techo_lite/platformio.ini index 1a8ad6772..2a92eb9be 100644 --- a/variants/lilygo_techo_lite/platformio.ini +++ b/variants/lilygo_techo_lite/platformio.ini @@ -98,6 +98,32 @@ lib_deps = ${LilyGo_T-Echo-Lite.lib_deps} densaugeo/base64 @ ~1.4.0 +[env:LilyGo_T-Echo-Lite_kiss_modem] +extends = LilyGo_T-Echo-Lite +build_src_filter = ${LilyGo_T-Echo-Lite.build_src_filter} + +<../examples/kiss_modem/> +[env:LilyGo_T-Echo-Lite_companion_radio_usb] +extends = LilyGo_T-Echo-Lite +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 712704 +build_flags = + ${LilyGo_T-Echo-Lite.build_flags} + -I src/helpers/ui + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D OFFLINE_QUEUE_SIZE=256 + -D UI_RECENT_LIST_SIZE=9 + -D UI_SENSORS_PAGE=1 + -D AUTO_SHUTDOWN_MILLIVOLTS=3300 +; -D QSPIFLASH=1 +build_src_filter = ${LilyGo_T-Echo-Lite.build_src_filter} + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${LilyGo_T-Echo-Lite.lib_deps} + densaugeo/base64 @ ~1.4.0 + [env:LilyGo_T-Echo-Lite_non_shell_companion_radio_ble] extends = LilyGo_T-Echo-Lite upload_protocol = nrfutil @@ -187,8 +213,3 @@ build_src_filter = ${nrf52_base.build_src_filter} lib_deps = ${LilyGo_T-Echo-Lite.lib_deps} densaugeo/base64 @ ~1.4.0 - -[env:LilyGo_T-Echo-Lite_kiss_modem] -extends = LilyGo_T-Echo-Lite -build_src_filter = ${LilyGo_T-Echo-Lite.build_src_filter} - +<../examples/kiss_modem/> diff --git a/variants/lilygo_techo_lite/variant.h b/variants/lilygo_techo_lite/variant.h index 83bb6f07c..b58b9470e 100644 --- a/variants/lilygo_techo_lite/variant.h +++ b/variants/lilygo_techo_lite/variant.h @@ -70,9 +70,6 @@ #define PIN_QSPI_IO2 _PINNUM(1, 9) #define PIN_QSPI_IO3 _PINNUM(0, 26) -#define EXTERNAL_FLASH_DEVICES ZD25WQ32CEIGR -#define EXTERNAL_FLASH_USE_QSPI - //////////////////////////////////////////////////////////////////////////////// // Builtin LEDs @@ -95,9 +92,6 @@ #define PIN_BUTTON2 _PINNUM(0, 18) #define BUTTON_PIN2 PIN_BUTTON2 -#define EXTERNAL_FLASH_DEVICES MX25R1635F -#define EXTERNAL_FLASH_USE_QSPI - //////////////////////////////////////////////////////////////////////////////// // Lora @@ -154,8 +148,8 @@ extern const int SCK; // GPS — per LilyGo t_echo_lite_config.h // PIN_GPS_TX/RX named from GPS module's perspective -#define PIN_GPS_TX _PINNUM(0, 29) // GPS UART TX → MCU RX -#define PIN_GPS_RX _PINNUM(1, 10) // GPS UART RX ← MCU TX +#define PIN_GPS_RX _PINNUM(0, 29) // GPS UART TX → MCU RX +#define PIN_GPS_TX _PINNUM(1, 10) // GPS UART RX ← MCU TX #define GPS_EN _PINNUM(1, 11) // GPS RT9080 power enable #define PIN_GPS_STANDBY _PINNUM(1, 13) // GPS wake-up -#define PIN_GPS_PPS _PINNUM(1, 15) // GPS 1PPS \ No newline at end of file +#define PIN_GPS_PPS _PINNUM(1, 15) // GPS 1PPS diff --git a/variants/meshtiny/MeshtinyBoard.h b/variants/meshtiny/MeshtinyBoard.h index 67a521fe9..21673d0aa 100644 --- a/variants/meshtiny/MeshtinyBoard.h +++ b/variants/meshtiny/MeshtinyBoard.h @@ -9,7 +9,7 @@ protected: uint8_t btn_prev_state; public: - MeshtinyBoard() : NRF52Board("Meshtiny OTA") {} + MeshtinyBoard() : NRF52Board("Meshtiny_OTA") {} void begin(); #if defined(P_LORA_TX_LED) diff --git a/variants/meshtracker_x1/target.cpp b/variants/meshtracker_x1/target.cpp index d6d548b51..5b90d6609 100644 --- a/variants/meshtracker_x1/target.cpp +++ b/variants/meshtracker_x1/target.cpp @@ -1,6 +1,7 @@ #include #include "target.h" #include +#include MeshTrackerX1Board board; @@ -27,46 +28,27 @@ mesh::LocalIdentity radio_new_identity() { void MeshTrackerX1SensorManager::start_gps() { gps_active = true; - // this init sequence comes from seeed examples and deals with all gps pins - pinMode(GPS_EN, OUTPUT); digitalWrite(GPS_EN, HIGH); delay(10); - pinMode(GPS_VRTC_EN, OUTPUT); - digitalWrite(GPS_VRTC_EN, HIGH); - delay(10); - - pinMode(GPS_RESET, OUTPUT); - digitalWrite(GPS_RESET, HIGH); - delay(10); - digitalWrite(GPS_RESET, LOW); - - pinMode(GPS_SLEEP_INT, OUTPUT); - digitalWrite(GPS_SLEEP_INT, HIGH); - pinMode(GPS_RTC_INT, OUTPUT); - digitalWrite(GPS_RTC_INT, LOW); -} - -void MeshTrackerX1SensorManager::sleep_gps() { - gps_active = false; - digitalWrite(GPS_VRTC_EN, HIGH); // keep RTC alive for faster fix on wake - digitalWrite(GPS_EN, LOW); - digitalWrite(GPS_RESET, LOW); - digitalWrite(GPS_SLEEP_INT, HIGH); + digitalWrite(GPS_RTC_INT, HIGH); + delay(5); digitalWrite(GPS_RTC_INT, LOW); } void MeshTrackerX1SensorManager::stop_gps() { gps_active = false; - digitalWrite(GPS_VRTC_EN, LOW); + digitalWrite(GPS_VRTC_EN, HIGH); // keep GPS RTC alive for faster fix on wake + digitalWrite(GPS_RTC_INT, LOW); // make sure this is LOW so we can pulse it to wake + airohaEnterSleep(_nmea); digitalWrite(GPS_EN, LOW); - digitalWrite(GPS_RESET, LOW); - digitalWrite(GPS_SLEEP_INT, HIGH); - digitalWrite(GPS_RTC_INT, LOW); } bool MeshTrackerX1SensorManager::begin() { // init GPS Serial1.begin(GPS_BAUD_RATE); + digitalWrite(GPS_RESET, HIGH); + delay(10); + digitalWrite(GPS_RESET, LOW); // init SPA06-003 barometer baro_ok = spa06.begin(SPA06_003_DEFAULT_ADDR, &Wire) || spa06.begin(0x76, &Wire); @@ -122,7 +104,7 @@ const char* MeshTrackerX1SensorManager::getSettingValue(int i) const { bool MeshTrackerX1SensorManager::setSettingValue(const char* name, const char* value) { if (strcmp(name, "gps") == 0) { if (strcmp(value, "0") == 0) { - sleep_gps(); // sleep for faster fix ! + stop_gps(); } else { start_gps(); } diff --git a/variants/promicro/platformio.ini b/variants/promicro/platformio.ini index 90cb475f5..da27c2fab 100644 --- a/variants/promicro/platformio.ini +++ b/variants/promicro/platformio.ini @@ -23,6 +23,7 @@ build_flags = ${nrf52_base.build_flags} -D ENV_INCLUDE_BMP280=1 -D ENV_INCLUDE_INA3221=1 -D ENV_INCLUDE_INA219=1 + -D ENV_INCLUDE_INA226=1 build_src_filter = ${nrf52_base.build_src_filter} + +<../variants/promicro> @@ -34,6 +35,7 @@ lib_deps= ${nrf52_base.lib_deps} adafruit/Adafruit BME280 Library @ ^2.3.0 adafruit/Adafruit BMP280 Library@^2.6.8 stevemarple/MicroNMEA @ ^2.0.6 + robtillaart/INA226@^0.6.6 [env:ProMicro_repeater] extends = Promicro diff --git a/variants/rak11310/RAK11310Board.h b/variants/rak11310/RAK11310Board.h index ea0f15e26..5b75ac361 100644 --- a/variants/rak11310/RAK11310Board.h +++ b/variants/rak11310/RAK11310Board.h @@ -2,6 +2,7 @@ #include #include +#include // from https://github.com/RAKWireless/RAK11300-AT-Command-Firmware/blob/9c48409a43620a828d653501d536473200aa33af/RAK11300-AT-Arduino/batt.cpp#L17-L19 #define VBAT_MV_PER_LSB (0.806F) // 3.0V ADC range and 12 - bit ADC resolution = 3300mV / 4096 @@ -46,4 +47,6 @@ public: void reboot() override { rp2040.reboot(); } bool startOTAUpdate(const char *id, char reply[]) override; + + void attachDynamicPrefs(KeyValueStore* prefs) { } // no-op }; diff --git a/variants/t1000-e/T1000eBoard.h b/variants/t1000-e/T1000eBoard.h index 1111d3c40..ba8bf0c1d 100644 --- a/variants/t1000-e/T1000eBoard.h +++ b/variants/t1000-e/T1000eBoard.h @@ -69,10 +69,6 @@ public: #ifdef PIN_3V3_ACC_EN digitalWrite(PIN_3V3_ACC_EN, LOW); #endif - #ifdef SENSOR_EN - digitalWrite(SENSOR_EN, LOW); - #endif - // set led on and wait for button release before poweroff #ifdef LED_PIN digitalWrite(LED_PIN, HIGH); diff --git a/variants/t1000-e/t1000e_sensors.cpp b/variants/t1000-e/t1000e_sensors.cpp index f02541382..7641a5435 100644 --- a/variants/t1000-e/t1000e_sensors.cpp +++ b/variants/t1000-e/t1000e_sensors.cpp @@ -72,7 +72,6 @@ float t1000e_get_temperature(void) { unsigned int ntc_v, vcc_v; digitalWrite(PIN_3V3_EN, HIGH); - digitalWrite(SENSOR_EN, HIGH); analogReference(AR_INTERNAL_3_0); analogReadResolution(12); delay(10); @@ -80,7 +79,6 @@ float t1000e_get_temperature(void) { vcc_v = (rail_v > NTC_REF_VCC) ? NTC_REF_VCC : rail_v; ntc_v = (1000.0 * AREF_VOLTAGE * analogRead(TEMP_SENSOR)) / 4096; digitalWrite(PIN_3V3_EN, LOW); - digitalWrite(SENSOR_EN, LOW); return get_heater_temperature(vcc_v, ntc_v); } @@ -90,13 +88,11 @@ uint32_t t1000e_get_light(void) { unsigned int lux_v = 0; digitalWrite(PIN_3V3_EN, HIGH); - digitalWrite(SENSOR_EN, HIGH); analogReference(AR_INTERNAL_3_0); analogReadResolution(12); delay(10); lux_v = 1000 * analogRead(LUX_SENSOR) * AREF_VOLTAGE / 4096; lux = get_light_lv(lux_v); - digitalWrite(SENSOR_EN, LOW); digitalWrite(PIN_3V3_EN, LOW); return lux; diff --git a/variants/t1000-e/target.cpp b/variants/t1000-e/target.cpp index 425328270..70a64359f 100644 --- a/variants/t1000-e/target.cpp +++ b/variants/t1000-e/target.cpp @@ -2,6 +2,7 @@ #include "t1000e_sensors.h" #include "target.h" #include +#include T1000eBoard board; @@ -81,56 +82,28 @@ mesh::LocalIdentity radio_new_identity() { void T1000SensorManager::start_gps() { gps_active = true; - //_nmea->begin(); - // this init sequence should be better - // comes from seeed examples and deals with all gps pins - pinMode(GPS_EN, OUTPUT); digitalWrite(GPS_EN, HIGH); delay(10); - pinMode(GPS_VRTC_EN, OUTPUT); - digitalWrite(GPS_VRTC_EN, HIGH); - delay(10); - - pinMode(GPS_RESET, OUTPUT); - digitalWrite(GPS_RESET, HIGH); - delay(10); - digitalWrite(GPS_RESET, LOW); - - pinMode(GPS_SLEEP_INT, OUTPUT); - digitalWrite(GPS_SLEEP_INT, HIGH); - pinMode(GPS_RTC_INT, OUTPUT); + digitalWrite(GPS_RTC_INT, HIGH); + delay(5); digitalWrite(GPS_RTC_INT, LOW); - pinMode(GPS_RESETB, INPUT_PULLUP); -} - -void T1000SensorManager::sleep_gps() { - gps_active = false; - digitalWrite(GPS_VRTC_EN, HIGH); - digitalWrite(GPS_EN, LOW); - digitalWrite(GPS_RESET, HIGH); - digitalWrite(GPS_SLEEP_INT, HIGH); - digitalWrite(GPS_RTC_INT, LOW); - pinMode(GPS_RESETB, OUTPUT); - digitalWrite(GPS_RESETB, LOW); - //_nmea->stop(); } void T1000SensorManager::stop_gps() { gps_active = false; - digitalWrite(GPS_VRTC_EN, LOW); + digitalWrite(GPS_VRTC_EN, HIGH); // keep GPS RTC alive for faster fix on wake + digitalWrite(GPS_RTC_INT, LOW); // make sure this is LOW so we can pulse it to wake + airohaEnterSleep(_nmea); // send command to put the GPS into RTC backup sleep digitalWrite(GPS_EN, LOW); - digitalWrite(GPS_RESET, HIGH); - digitalWrite(GPS_SLEEP_INT, HIGH); - digitalWrite(GPS_RTC_INT, LOW); - pinMode(GPS_RESETB, OUTPUT); - digitalWrite(GPS_RESETB, LOW); - //_nmea->stop(); } bool T1000SensorManager::begin() { // init GPS Serial1.begin(115200); + digitalWrite(GPS_RESET, HIGH); + delay(10); + digitalWrite(GPS_RESET, LOW); return true; } @@ -156,7 +129,6 @@ void T1000SensorManager::loop() { node_lat = ((double)_nmea->getLatitude())/1000000.; node_lon = ((double)_nmea->getLongitude())/1000000.; node_altitude = ((double)_nmea->getAltitude()) / 1000.0; - //Serial.printf("lat %f lon %f\r\n", _lat, _lon); } next_gps_update = millis() + 1000; } @@ -176,7 +148,7 @@ const char* T1000SensorManager::getSettingValue(int i) const { bool T1000SensorManager::setSettingValue(const char* name, const char* value) { if (strcmp(name, "gps") == 0) { if (strcmp(value, "0") == 0) { - sleep_gps(); // sleep for faster fix ! + stop_gps(); } else { start_gps(); } diff --git a/variants/t1000-e/variant.cpp b/variants/t1000-e/variant.cpp index ed21fd68c..6a9f1638c 100644 --- a/variants/t1000-e/variant.cpp +++ b/variants/t1000-e/variant.cpp @@ -14,7 +14,7 @@ const uint32_t g_ADigitalPinMap[PINS_COUNT + 1] = 1, // P0.01 2, // P0.02, AIN0 BATTERY_PIN 3, // P0.03 - 4, // P0.04, SENSOR_EN + 4, // P0.04 5, // P0.05, EXT_PWR_DETEC 6, // P0.06, PIN_BUTTON1 7, // P0.07, LORA_BUSY @@ -71,13 +71,12 @@ void initVariant() pinMode(LUX_SENSOR, INPUT); pinMode(EXT_CHRG_DETECT, INPUT); pinMode(EXT_PWR_DETECT, INPUT); - pinMode(GPS_RESETB, INPUT); + pinMode(GPS_RESETB, INPUT_PULLUP); pinMode(PIN_BUTTON1, INPUT); pinMode(PIN_3V3_EN, OUTPUT); pinMode(PIN_3V3_ACC_EN, OUTPUT); pinMode(BUZZER_EN, OUTPUT); - pinMode(SENSOR_EN, OUTPUT); pinMode(GPS_EN, OUTPUT); pinMode(GPS_RESET, OUTPUT); pinMode(GPS_VRTC_EN, OUTPUT); @@ -88,11 +87,10 @@ void initVariant() digitalWrite(PIN_3V3_EN, LOW); digitalWrite(PIN_3V3_ACC_EN, LOW); digitalWrite(BUZZER_EN, LOW); - digitalWrite(SENSOR_EN, LOW); digitalWrite(GPS_EN, LOW); digitalWrite(GPS_RESET, LOW); - digitalWrite(GPS_VRTC_EN, LOW); - digitalWrite(GPS_SLEEP_INT, HIGH); + digitalWrite(GPS_VRTC_EN, HIGH); + digitalWrite(GPS_SLEEP_INT, LOW); digitalWrite(GPS_RTC_INT, LOW); digitalWrite(LED_PIN, LOW); } diff --git a/variants/t1000-e/variant.h b/variants/t1000-e/variant.h index b95e1e4d8..bd5090733 100644 --- a/variants/t1000-e/variant.h +++ b/variants/t1000-e/variant.h @@ -120,7 +120,6 @@ //////////////////////////////////////////////////////////////////////////////// // Temp+Lux Sensor -#define SENSOR_EN (4) // P0.4 #define TEMP_SENSOR (31) // P0.31/AIN7 #define LUX_SENSOR (29) // P0.29/AIN5 diff --git a/variants/thinknode_m6/ThinkNodeM6Board.cpp b/variants/thinknode_m6/ThinkNodeM6Board.cpp index 8ebae64c6..f83f274f9 100644 --- a/variants/thinknode_m6/ThinkNodeM6Board.cpp +++ b/variants/thinknode_m6/ThinkNodeM6Board.cpp @@ -15,6 +15,9 @@ void ThinkNodeM6Board::begin() { digitalWrite(P_LORA_TX_LED, LOW); #endif + pinMode(EXT_PWR_DETECT, INPUT); + pinMode(EXT_CHRG_DETECT, INPUT); + delay(10); // give sx1262 some time to power up } diff --git a/variants/thinknode_m6/ThinkNodeM6Board.h b/variants/thinknode_m6/ThinkNodeM6Board.h index 78815e2c9..934794c9a 100644 --- a/variants/thinknode_m6/ThinkNodeM6Board.h +++ b/variants/thinknode_m6/ThinkNodeM6Board.h @@ -36,6 +36,35 @@ public: return "Elecrow ThinkNode M6"; } + bool hasUsbPowerDetect() const override { return true; } + + bool isUsbPowered() override { + // P0.13 is the dedicated USB / external power-detect line on the M6 PCB + // (same pin the Meshtastic M6 variant uses for USB detection). + return digitalRead(EXT_PWR_DETECT) == EXT_PWR_DETECT_VALUE; + } + + bool hasSolarChargerDetect() const override { return true; } + + bool isSolarChargerActive() override { + // P0.15 is the active-low solar-charger status line. It reports the + // charger's state, not photovoltaic voltage or current. + return digitalRead(EXT_CHRG_DETECT) == EXT_CHRG_DETECT_VALUE; + } + + bool isExternalPowered() override { + if (isUsbPowered() || isSolarChargerActive()) { + return true; + } + return NRF52Board::isExternalPowered(); + } + + bool isChargerActive() override { + // Preserve a generic charging indication while also exposing both + // source-specific states (USB / solar) separately through the CLI. + return isSolarChargerActive() || isUsbPowered(); + } + void powerOff() override { // turn off all leds, sd_power_system_off will not do this for us diff --git a/variants/thinknode_m6/variant.h b/variants/thinknode_m6/variant.h index 0a4025f3b..3428fcd64 100644 --- a/variants/thinknode_m6/variant.h +++ b/variants/thinknode_m6/variant.h @@ -29,6 +29,12 @@ #define AREF_VOLTAGE (3.0) +// External power and charger status lines. +#define EXT_PWR_DETECT (13) +#define EXT_PWR_DETECT_VALUE HIGH +#define EXT_CHRG_DETECT (15) +#define EXT_CHRG_DETECT_VALUE LOW + //////////////////////////////////////////////////////////////////////////////// // Number of pins diff --git a/variants/waveshare_esp32s3_lr1121/WaveshareS3LR1121Board.h b/variants/waveshare_esp32s3_lr1121/WaveshareS3LR1121Board.h new file mode 100644 index 000000000..8307ba505 --- /dev/null +++ b/variants/waveshare_esp32s3_lr1121/WaveshareS3LR1121Board.h @@ -0,0 +1,11 @@ +#pragma once + +#include +#include + +class WaveshareS3LR1121Board : public ESP32Board { +public: + const char* getManufacturerName() const override { + return "Waveshare ESP32-S3-LR1121"; + } +}; diff --git a/variants/waveshare_esp32s3_lr1121/platformio.ini b/variants/waveshare_esp32s3_lr1121/platformio.ini new file mode 100644 index 000000000..8e7b52d4f --- /dev/null +++ b/variants/waveshare_esp32s3_lr1121/platformio.ini @@ -0,0 +1,102 @@ +; Waveshare ESP32-S3-LR1121 (HF: 850-930 MHz + 2.4 GHz) +; ESP32-S3FH4R2 (4MB flash, 2MB PSRAM), native USB-CDC via the Tiny-Adapter FPC. +; No user LED, battery sense or user button (BOOT is GPIO0). +[Waveshare_S3_LR1121] +extends = esp32_base +board = esp32-s3-zero +; min_spiffs: 1.9MB OTA app slots; one layout for every role so switching roles keeps the filesystem +board_build.partitions = min_spiffs.csv +build_flags = ${esp32_base.build_flags} + -I variants/waveshare_esp32s3_lr1121 + -D BOARD_HAS_PSRAM + -D P_LORA_DIO_1=38 + -D P_LORA_NSS=42 + -D P_LORA_RESET=39 + -D P_LORA_BUSY=41 + -D P_LORA_SCLK=40 + -D P_LORA_MISO=46 + -D P_LORA_MOSI=45 + -D RADIO_CLASS=CustomLR1121 + -D WRAPPER_CLASS=CustomLR1121Wrapper + -D LR11X0_DIO3_TCXO_VOLTAGE=1.8 + -D LR1121_FORCE_HP_PA ; RFO_LP_LF is not fitted (R4 NC) + -D RX_BOOSTED_GAIN=true + -D LORA_TX_POWER=22 +build_src_filter = ${esp32_base.build_src_filter} + +<../variants/waveshare_esp32s3_lr1121> +lib_deps = + ${esp32_base.lib_deps} + +[env:Waveshare_S3_LR1121_repeater] +extends = Waveshare_S3_LR1121 +build_flags = + ${Waveshare_S3_LR1121.build_flags} + -D ADVERT_NAME='"Waveshare LR1121 Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Waveshare_S3_LR1121.build_src_filter} + +<../examples/simple_repeater/*.cpp> +lib_deps = + ${Waveshare_S3_LR1121.lib_deps} + ${esp32_ota.lib_deps} + +[env:Waveshare_S3_LR1121_room_server] +extends = Waveshare_S3_LR1121 +build_flags = + ${Waveshare_S3_LR1121.build_flags} + -D ADVERT_NAME='"Waveshare LR1121 Room"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Waveshare_S3_LR1121.build_src_filter} + +<../examples/simple_room_server> +lib_deps = + ${Waveshare_S3_LR1121.lib_deps} + ${esp32_ota.lib_deps} + +[env:Waveshare_S3_LR1121_companion_radio_usb] +extends = Waveshare_S3_LR1121 +build_flags = + ${Waveshare_S3_LR1121.build_flags} + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D OFFLINE_QUEUE_SIZE=256 + -D ENABLE_USB_INTERFACE +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Waveshare_S3_LR1121.build_src_filter} + + + +<../examples/companion_radio/*.cpp> +lib_deps = + ${Waveshare_S3_LR1121.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:Waveshare_S3_LR1121_companion_radio_ble] +extends = Waveshare_S3_LR1121 +build_flags = + ${Waveshare_S3_LR1121.build_flags} + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D OFFLINE_QUEUE_SIZE=256 + -D BLE_PIN_CODE=123456 +; -D BLE_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Waveshare_S3_LR1121.build_src_filter} + + + +<../examples/companion_radio/*.cpp> +lib_deps = + ${Waveshare_S3_LR1121.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:Waveshare_S3_LR1121_kiss_modem] +extends = Waveshare_S3_LR1121 +build_src_filter = ${Waveshare_S3_LR1121.build_src_filter} + +<../examples/kiss_modem/> diff --git a/variants/waveshare_esp32s3_lr1121/target.cpp b/variants/waveshare_esp32s3_lr1121/target.cpp new file mode 100644 index 000000000..56775c4ce --- /dev/null +++ b/variants/waveshare_esp32s3_lr1121/target.cpp @@ -0,0 +1,63 @@ +#include +#include "target.h" + +WaveshareS3LR1121Board board; + +static SPIClass spi; +RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi); +WRAPPER_CLASS radio_driver(radio, board); + +ESP32RTCClock fallback_clock; +AutoDiscoverRTCClock rtc_clock(fallback_clock); +SensorManager sensors; + +#ifndef LORA_CR + #define LORA_CR 5 +#endif + +// DIO5/DIO6 drive the RTC6603SP sub-GHz T/R switch; the 2.4 GHz port (RFIO_HF) is unswitched +static const uint32_t rfswitch_dios[Module::RFSWITCH_MAX_PINS] = { + RADIOLIB_LR11X0_DIO5, + RADIOLIB_LR11X0_DIO6, + RADIOLIB_NC +}; + +static const Module::RfSwitchMode_t rfswitch_table[] = { + // mode DIO5 DIO6 + { LR11x0::MODE_STBY, {LOW, LOW}}, + { LR11x0::MODE_RX, {HIGH, LOW}}, + { LR11x0::MODE_TX, {LOW, HIGH}}, + { LR11x0::MODE_TX_HP, {LOW, HIGH}}, + { LR11x0::MODE_TX_HF, {LOW, LOW}}, + { LR11x0::MODE_GNSS, {LOW, LOW}}, + { LR11x0::MODE_WIFI, {LOW, LOW}}, + END_OF_MODE_TABLE, +}; + +bool radio_init() { + fallback_clock.begin(); + rtc_clock.begin(Wire); + + spi.begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI); + int status = radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, RADIOLIB_LR11X0_LORA_SYNC_WORD_PRIVATE, + LORA_TX_POWER, 16, LR11X0_DIO3_TCXO_VOLTAGE); + if (status != RADIOLIB_ERR_NONE) { + Serial.print("ERROR: radio init failed: "); + Serial.println(status); + return false; + } + + radio.setCRC(2); + radio.explicitHeader(); + radio.setRfSwitchTable(rfswitch_dios, rfswitch_table); +#ifdef RX_BOOSTED_GAIN + radio.setRxBoostedGainMode(RX_BOOSTED_GAIN); +#endif + + return true; +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); +} diff --git a/variants/waveshare_esp32s3_lr1121/target.h b/variants/waveshare_esp32s3_lr1121/target.h new file mode 100644 index 000000000..461509e51 --- /dev/null +++ b/variants/waveshare_esp32s3_lr1121/target.h @@ -0,0 +1,17 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#include "WaveshareS3LR1121Board.h" + +extern WaveshareS3LR1121Board board; +extern WRAPPER_CLASS radio_driver; +extern AutoDiscoverRTCClock rtc_clock; +extern SensorManager sensors; + +bool radio_init(); +mesh::LocalIdentity radio_new_identity(); diff --git a/variants/wio-tracker-l1-1w/WioTrackerL1Board.h b/variants/wio-tracker-l1-1w/WioTrackerL1Board.h index 798145db0..f6164e47a 100644 --- a/variants/wio-tracker-l1-1w/WioTrackerL1Board.h +++ b/variants/wio-tracker-l1-1w/WioTrackerL1Board.h @@ -14,7 +14,7 @@ protected: bool isLoRaFemPaGainEnabled() const { return _is_pa_enabled; } public: - WioTrackerL1Board() : NRF52Board("WioTrackerL1 OTA") {} + WioTrackerL1Board() : NRF52Board("WioTrackerL1_OTA") {} void begin(); void attachDynamicPrefs(KeyValueStore* prefs); bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) override; diff --git a/variants/wio-tracker-l1/WioTrackerL1Board.h b/variants/wio-tracker-l1/WioTrackerL1Board.h index e376e0666..99e852bb7 100644 --- a/variants/wio-tracker-l1/WioTrackerL1Board.h +++ b/variants/wio-tracker-l1/WioTrackerL1Board.h @@ -9,7 +9,7 @@ protected: uint8_t btn_prev_state; public: - WioTrackerL1Board() : NRF52Board("WioTrackerL1 OTA") {} + WioTrackerL1Board() : NRF52Board("WioTrackerL1_OTA") {} void begin(); #if defined(P_LORA_TX_LED)