From b6e3c7e0a5037646542c0617961880e05101cead Mon Sep 17 00:00:00 2001 From: pelgraine <140762863+pelgraine@users.noreply.github.com> Date: Thu, 4 Jun 2026 23:25:03 +1000 Subject: [PATCH] implemented $PCAS04,7 - Multi-constellation mode (GPS, Galileo, BeiDou, and QZSS) The default LilyGo/MeshOS firmware uses GPS-only ($PCAS04,1) & $PMTK869,1,1 - EASY predicted ephemeris --- examples/companion_radio/main.cpp | 31 ++++ examples/companion_radio/ui-new/UITask.cpp | 25 ++- variants/lilygo_tdeck_max/GPSStreamCounter.h | 161 +++++++++++++++++++ 3 files changed, 212 insertions(+), 5 deletions(-) create mode 100644 variants/lilygo_tdeck_max/GPSStreamCounter.h diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 100df87a..759ed8fc 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -2582,6 +2582,15 @@ void setup() { #ifdef PIN_GPS_EN digitalWrite(PIN_GPS_EN, GPS_EN_ACTIVE); #endif +#if defined(LilyGo_TDeck_Pro_Max) + // Speed up / improve the MAX GPS fix: enable multi-constellation + // (GPS + GLONASS + BeiDou) and EASY predicted ephemeris (warm/hot starts + // on subsequent boots). A module that doesn't recognise a given sentence + // simply ignores it. Sent once here, after the module is powered. + delay(300); // allow the module to finish booting before accepting config + Serial2.print("$PCAS04,7*1E\r\n"); // GPS + GLONASS + BeiDou + Serial2.print("$PMTK869,1,1*35\r\n"); // EASY predicted ephemeris ON +#endif sensors.setSettingValue("gps", "1"); } else { #ifdef PIN_GPS_EN @@ -2723,6 +2732,28 @@ void loop() { sensors.loop(); + // Satellite-count diagnostic (MAX): every 10s while GPS is on, print the + // reported satellite count, fix state and NMEA rate so the multi-constellation + // change can be watched climbing. Remove once confirmed if you don't want it. +#if defined(LilyGo_TDeck_Pro_Max) && HAS_GPS + { + static unsigned long lastSatDiag = 0; + if (the_mesh.getNodePrefs()->gps_enabled && millis() - lastSatDiag >= 10000) { + lastSatDiag = millis(); + auto* lp = sensors.getLocationProvider(); + if (lp) { + char sv[64]; + gpsStream.getInViewBreakdown(sv, sizeof(sv)); + Serial.printf("[GPS] inview=%u [%s] used=%d fix=%s nmea=%u/s total=%lu\n", + gpsStream.getSatellitesInView(), sv, + lp->satellitesCount(), lp->isValid() ? "Y" : "N", + gpsStream.getSentencesPerSec(), + (unsigned long)gpsStream.getSentenceCount()); + } + } + } +#endif + // GPS diagnostic — disabled to reduce serial noise (uncomment for debugging) // #if HAS_GPS // { diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 34a9cafb..18815104 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -84,6 +84,21 @@ // from the same mesh callback on the same thread. static bool s_lastMsgSuppressed = false; +#ifdef KB_BL_PIN +// Drive the keyboard-backlight pin for the message/ring flash. On the T-Deck +// Pro MAX the manual (both-shifts) keyboard-backlight toggle drives this pin +// via LEDC (analogWrite); once LEDC is bound to a pin a later digitalWrite is +// swallowed, so the flash must use analogWrite too or it silently does nothing +// after the manual toggle has been used. Other boards keep the plain GPIO write. +static inline void kbBacklightFlashWrite(bool on) { + #if defined(LilyGo_TDeck_Pro_Max) + analogWrite(KB_BL_PIN, on ? 255 : 0); + #else + digitalWrite(KB_BL_PIN, on ? HIGH : LOW); + #endif +} +#endif + class SplashScreen : public UIScreen { UITask* _task; unsigned long dismiss_after; @@ -1747,7 +1762,7 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i // Keyboard flash notification (suppress for room sync and muted channels) #ifdef KB_BL_PIN if (_node_prefs->kb_flash_notify && !isRoomMsg && !suppressNotif) { - digitalWrite(KB_BL_PIN, HIGH); + kbBacklightFlashWrite(true); _kb_flash_off_at = millis() + 200; // 200ms flash } #endif @@ -1994,10 +2009,10 @@ void UITask::loop() { #ifdef HAS_4G_MODEM // Don't turn off LED if incoming call flash is active if (!_incomingCallRinging) { - digitalWrite(KB_BL_PIN, LOW); + kbBacklightFlashWrite(false); } #else - digitalWrite(KB_BL_PIN, LOW); + kbBacklightFlashWrite(false); #endif _kb_flash_off_at = 0; } @@ -2025,7 +2040,7 @@ void UITask::loop() { _incomingCallRinging = false; // Only turn off LED if message flash isn't also active if (!_kb_flash_off_at) { - digitalWrite(KB_BL_PIN, LOW); + kbBacklightFlashWrite(false); } _callFlashState = false; } @@ -2035,7 +2050,7 @@ void UITask::loop() { unsigned long now = millis(); if (now >= _nextCallFlash) { _callFlashState = !_callFlashState; - digitalWrite(KB_BL_PIN, _callFlashState ? HIGH : LOW); + kbBacklightFlashWrite(_callFlashState); // 250ms on, 250ms off — fast pulse to distinguish from single msg flash _nextCallFlash = now + 250; } diff --git a/variants/lilygo_tdeck_max/GPSStreamCounter.h b/variants/lilygo_tdeck_max/GPSStreamCounter.h new file mode 100644 index 00000000..5b905c5f --- /dev/null +++ b/variants/lilygo_tdeck_max/GPSStreamCounter.h @@ -0,0 +1,161 @@ +#pragma once + +#include + +// Transparent Stream wrapper that counts NMEA sentences (newline-delimited) +// flowing from the GPS serial port to the MicroNMEA parser. +// +// Usage: Instead of MicroNMEALocationProvider gps(Serial2, &rtc_clock); +// Use: GPSStreamCounter gpsStream(Serial2); +// MicroNMEALocationProvider gps(gpsStream, &rtc_clock); +// +// Every read() call passes through to the underlying stream; when a '\n' +// is seen the sentence counter increments. This lets the UI display a +// live "nmea" count so users can confirm the baud rate is correct and +// the GPS module is actually sending data. +// +// MAX addition: as bytes pass through, each completed $xxGSV sentence is +// tapped for its "satellites in view" field. The per-constellation talkers +// (GP/GL/GA/BD/GB/...) are tracked separately and summed, so the total +// reflects multi-constellation reception (used to confirm $PCAS04,7 took +// effect). This costs only a small per-byte accumulation in read(). + +class GPSStreamCounter : public Stream { +public: + GPSStreamCounter(Stream& inner) + : _inner(inner), _sentences(0), _sentences_snapshot(0), + _last_snapshot(0), _sentences_per_sec(0), _line_len(0), _gsv_n(0) {} + + // --- Stream read interface (passes through) --- + int available() override { return _inner.available(); } + int peek() override { return _inner.peek(); } + + int read() override { + int c = _inner.read(); + if (c < 0) return c; + + // Accumulate the current sentence so completed $xxGSV lines can be tapped + // for their satellites-in-view count. + if (c == '$') { + _line_len = 0; + _line[_line_len++] = (char)c; + } else if (c == '\n') { + if (_line_len > 0) { _line[_line_len] = '\0'; parseGSV(_line); } + _sentences++; + _line_len = 0; + } else if (c != '\r' && _line_len < (int)sizeof(_line) - 1) { + _line[_line_len++] = (char)c; + } + return c; + } + + // --- Stream write interface (pass through for NMEA commands if needed) --- + size_t write(uint8_t b) override { return _inner.write(b); } + + // --- Sentence counting API --- + + // Total sentences received since boot (or last reset) + uint32_t getSentenceCount() const { return _sentences; } + + // Sentences received per second (updated each time you call it, + // with a 1-second rolling window) + uint16_t getSentencesPerSec() { + unsigned long now = millis(); + unsigned long elapsed = now - _last_snapshot; + if (elapsed >= 1000) { + uint32_t delta = _sentences - _sentences_snapshot; + // Scale to per-second if interval wasn't exactly 1000ms + _sentences_per_sec = (uint16_t)((delta * 1000UL) / elapsed); + _sentences_snapshot = _sentences; + _last_snapshot = now; + } + return _sentences_per_sec; + } + + // Total satellites in view across all constellations, summed from the latest + // $xxGSV per talker. Talkers not heard in the last few seconds are dropped so + // the figure tracks the live sky rather than accumulating stale entries. + uint16_t getSatellitesInView() { + uint16_t total = 0; + unsigned long now = millis(); + for (int i = 0; i < _gsv_n; i++) { + if (now - _gsv[i].seen <= 8000) total += _gsv[i].inview; + } + return total; + } + + // Per-talker in-view summary, e.g. "GP=11 GL=0 GA=5 BD=8", for diagnostics. + // Only talkers heard in the last few seconds are listed. + void getInViewBreakdown(char* buf, size_t len) { + if (len == 0) return; + buf[0] = '\0'; + size_t pos = 0; + unsigned long now = millis(); + for (int i = 0; i < _gsv_n; i++) { + if (now - _gsv[i].seen > 8000) continue; + int n = snprintf(buf + pos, len - pos, "%s%c%c=%u", + pos ? " " : "", _gsv[i].t0, _gsv[i].t1, _gsv[i].inview); + if (n <= 0 || (size_t)n >= len - pos) break; + pos += (size_t)n; + } + } + + // Reset all counters (e.g. when GPS hardware power cycles) + void resetCounters() { + _sentences = 0; + _sentences_snapshot = 0; + _sentences_per_sec = 0; + _last_snapshot = millis(); + _gsv_n = 0; + _line_len = 0; + } + +private: + Stream& _inner; + volatile uint32_t _sentences; + uint32_t _sentences_snapshot; + unsigned long _last_snapshot; + uint16_t _sentences_per_sec; + + // Current sentence accumulation (for the GSV tap) + char _line[100]; + int _line_len; + + // Per-talker satellites-in-view, e.g. GP (GPS), GL (GLONASS), BD/GB (BeiDou), + // GA (Galileo). Small fixed table; combined "GN" talkers are skipped so they + // don't double-count the per-constellation sentences. + struct GsvEntry { char t0; char t1; uint8_t inview; unsigned long seen; }; + GsvEntry _gsv[6]; + int _gsv_n; + + void parseGSV(const char* s) { + // Expect "$ttGSV,total,num,inview,..." where tt is the 2-char talker id. + if (s[0] != '$' || s[1] == '\0' || s[2] == '\0') return; + if (!(s[3] == 'G' && s[4] == 'S' && s[5] == 'V')) return; + char t0 = s[1], t1 = s[2]; + if (t0 == 'G' && t1 == 'N') return; // combined talker - skip to avoid double count + + // The satellites-in-view value is the field after the 3rd comma. + const char* p = s; + int commas = 0; + while (*p && commas < 3) { if (*p == ',') commas++; p++; } + if (commas < 3) return; + int inview = atoi(p); + + unsigned long now = millis(); + for (int i = 0; i < _gsv_n; i++) { + if (_gsv[i].t0 == t0 && _gsv[i].t1 == t1) { + _gsv[i].inview = (uint8_t)inview; + _gsv[i].seen = now; + return; + } + } + if (_gsv_n < (int)(sizeof(_gsv) / sizeof(_gsv[0]))) { + _gsv[_gsv_n].t0 = t0; + _gsv[_gsv_n].t1 = t1; + _gsv[_gsv_n].inview = (uint8_t)inview; + _gsv[_gsv_n].seen = now; + _gsv_n++; + } + } +}; \ No newline at end of file