Exercise 10 works now, #9 needs to be revised accordingly

This commit is contained in:
John Poole 2026-02-17 11:01:09 -08:00
commit 0077381546
12 changed files with 1775 additions and 0 deletions

View file

@ -0,0 +1,42 @@
## Exercise 09: GPS Time (L76K)
This exercise boots the T-Beam Supreme and verifies GPS behavior at startup.
Implemented behavior:
1. Initializes PMU, OLED, and SD startup watcher (same startup SD path used in Exercise 08).
2. Probes GPS at startup for NMEA traffic, module identity, satellite count, and UTC time availability.
3. If L76K is detected, normal GPS-time flow continues.
4. If L76K is not detected and Quectel-style module text is detected, OLED shows a hard TODO error:
- Quectel detected
- L76K required
- Quectel support is TODO
5. Every minute:
- If GPS UTC is valid: shows GPS UTC time and satellites on OLED.
- If satellites are seen but UTC is not valid yet: shows that condition and RTC time.
- If no satellites: shows:
- "Unable to acquire"
- "satellites"
- "Take me outside so I"
- "can see satellites"
- plus current RTC time.
Notes:
- GPS time displayed is UTC from NMEA RMC with valid status.
- Satellite count uses best available from GGA/GSV.
- RTC fallback reads PCF8563 via Wire1.
## Build
```bash
source /home/jlpoole/rnsenv/bin/activate
pio run -e node_a
```
## Upload
```bash
source /home/jlpoole/rnsenv/bin/activate
pio run -e node_a -t upload --upload-port /dev/ttyACM0
```

View file

@ -0,0 +1,360 @@
#include "StartupSdManager.h"
#include <stdarg.h>
#include "driver/gpio.h"
StartupSdManager::StartupSdManager(Print& serial) : serial_(serial) {}
bool StartupSdManager::begin(const SdWatcherConfig& cfg, SdStatusCallback callback) {
cfg_ = cfg;
callback_ = callback;
forceSpiDeselected();
dumpSdPins("very-early");
if (!initPmuForSdPower()) {
return false;
}
cycleSdRail();
delay(cfg_.startupWarmupMs);
bool warmMounted = false;
for (uint8_t i = 0; i < 3; ++i) {
if (mountPreferred(false)) {
warmMounted = true;
break;
}
delay(200);
}
// Some cards need a longer power/settle window after cold boot.
// Before declaring ABSENT, retry with extended settle and a full scan.
if (!warmMounted) {
logf("Watcher: startup preferred mount failed, retrying with extended settle");
cycleSdRail(400, 1200);
delay(cfg_.startupWarmupMs + 1500);
warmMounted = mountCardFullScan();
}
if (warmMounted) {
setStateMounted();
} else {
setStateAbsent();
}
return true;
}
void StartupSdManager::update() {
const uint32_t now = millis();
const uint32_t pollInterval =
(watchState_ == SdWatchState::MOUNTED) ? cfg_.pollIntervalMountedMs : cfg_.pollIntervalAbsentMs;
if ((uint32_t)(now - lastPollMs_) < pollInterval) {
return;
}
lastPollMs_ = now;
if (watchState_ == SdWatchState::MOUNTED) {
if (verifyMountedCard()) {
presentVotes_ = 0;
absentVotes_ = 0;
return;
}
if (mountPreferred(false) && verifyMountedCard()) {
presentVotes_ = 0;
absentVotes_ = 0;
return;
}
absentVotes_++;
presentVotes_ = 0;
if (absentVotes_ >= cfg_.votesToAbsent) {
setStateAbsent();
absentVotes_ = 0;
}
return;
}
bool mounted = mountPreferred(false);
if (!mounted && (uint32_t)(now - lastFullScanMs_) >= cfg_.fullScanIntervalMs) {
lastFullScanMs_ = now;
if (cfg_.recoveryRailCycleOnFullScan) {
logf("Watcher: recovery rail cycle before full scan");
cycleSdRail(cfg_.recoveryRailOffMs, cfg_.recoveryRailOnSettleMs);
delay(150);
}
logf("Watcher: preferred probe failed, running full scan");
mounted = mountCardFullScan();
}
if (mounted) {
presentVotes_++;
absentVotes_ = 0;
if (presentVotes_ >= cfg_.votesToPresent) {
setStateMounted();
presentVotes_ = 0;
}
} else {
absentVotes_++;
presentVotes_ = 0;
if (absentVotes_ >= cfg_.votesToAbsent) {
setStateAbsent();
absentVotes_ = 0;
}
}
}
bool StartupSdManager::consumeMountedEvent() {
bool out = mountedEventPending_;
mountedEventPending_ = false;
return out;
}
bool StartupSdManager::consumeRemovedEvent() {
bool out = removedEventPending_;
removedEventPending_ = false;
return out;
}
void StartupSdManager::logf(const char* fmt, ...) {
char msg[196];
va_list args;
va_start(args, fmt);
vsnprintf(msg, sizeof(msg), fmt, args);
va_end(args);
serial_.printf("[%10lu][%06lu] %s\r\n",
(unsigned long)millis(),
(unsigned long)logSeq_++,
msg);
}
void StartupSdManager::notify(SdEvent event, const char* message) {
if (callback_ != nullptr) {
callback_(event, message);
}
}
void StartupSdManager::forceSpiDeselected() {
pinMode(tbeam_supreme::sdCs(), OUTPUT);
digitalWrite(tbeam_supreme::sdCs(), HIGH);
pinMode(tbeam_supreme::imuCs(), OUTPUT);
digitalWrite(tbeam_supreme::imuCs(), HIGH);
}
void StartupSdManager::dumpSdPins(const char* tag) {
if (!cfg_.enablePinDumps) {
(void)tag;
return;
}
const gpio_num_t cs = (gpio_num_t)tbeam_supreme::sdCs();
const gpio_num_t sck = (gpio_num_t)tbeam_supreme::sdSck();
const gpio_num_t miso = (gpio_num_t)tbeam_supreme::sdMiso();
const gpio_num_t mosi = (gpio_num_t)tbeam_supreme::sdMosi();
logf("PINS(%s): CS=%d SCK=%d MISO=%d MOSI=%d",
tag, gpio_get_level(cs), gpio_get_level(sck), gpio_get_level(miso), gpio_get_level(mosi));
}
bool StartupSdManager::initPmuForSdPower() {
if (!tbeam_supreme::initPmuForPeripherals(pmu_, &serial_)) {
logf("ERROR: PMU init failed");
return false;
}
return true;
}
void StartupSdManager::cycleSdRail(uint32_t offMs, uint32_t onSettleMs) {
if (!cfg_.enableSdRailCycle) {
return;
}
if (!pmu_) {
logf("SD rail cycle skipped: pmu=null");
return;
}
forceSpiDeselected();
pmu_->disablePowerOutput(XPOWERS_BLDO1);
delay(offMs);
pmu_->setPowerChannelVoltage(XPOWERS_BLDO1, 3300);
pmu_->enablePowerOutput(XPOWERS_BLDO1);
delay(onSettleMs);
}
bool StartupSdManager::tryMountWithBus(SPIClass& bus, const char* busName, uint32_t hz, bool verbose) {
SD.end();
bus.end();
delay(10);
forceSpiDeselected();
bus.begin(tbeam_supreme::sdSck(), tbeam_supreme::sdMiso(), tbeam_supreme::sdMosi(), tbeam_supreme::sdCs());
digitalWrite(tbeam_supreme::sdCs(), HIGH);
delay(2);
for (int i = 0; i < 10; i++) {
bus.transfer(0xFF);
}
delay(2);
if (verbose) {
logf("SD: trying bus=%s freq=%lu Hz", busName, (unsigned long)hz);
}
if (!SD.begin(tbeam_supreme::sdCs(), bus, hz)) {
if (verbose) {
logf("SD: mount failed (possible non-FAT format, power, or bus issue)");
}
return false;
}
if (SD.cardType() == CARD_NONE) {
SD.end();
return false;
}
sdSpi_ = &bus;
sdBusName_ = busName;
sdFreq_ = hz;
return true;
}
bool StartupSdManager::mountPreferred(bool verbose) {
return tryMountWithBus(sdSpiH_, "HSPI", 400000, verbose);
}
bool StartupSdManager::mountCardFullScan() {
const uint32_t freqs[] = {400000, 1000000, 4000000, 10000000};
for (uint8_t i = 0; i < (sizeof(freqs) / sizeof(freqs[0])); ++i) {
if (tryMountWithBus(sdSpiH_, "HSPI", freqs[i], true)) {
logf("SD: card detected and mounted");
return true;
}
}
for (uint8_t i = 0; i < (sizeof(freqs) / sizeof(freqs[0])); ++i) {
if (tryMountWithBus(sdSpiF_, "FSPI", freqs[i], true)) {
logf("SD: card detected and mounted");
return true;
}
}
logf("SD: begin() failed on all bus/frequency attempts");
return false;
}
bool StartupSdManager::verifyMountedCard() {
File root = SD.open("/", FILE_READ);
if (!root) {
return false;
}
root.close();
return true;
}
const char* StartupSdManager::cardTypeToString(uint8_t type) {
switch (type) {
case CARD_MMC:
return "MMC";
case CARD_SD:
return "SDSC";
case CARD_SDHC:
return "SDHC/SDXC";
default:
return "UNKNOWN";
}
}
void StartupSdManager::printCardInfo() {
uint8_t cardType = SD.cardType();
uint64_t cardSizeMB = SD.cardSize() / (1024ULL * 1024ULL);
uint64_t totalMB = SD.totalBytes() / (1024ULL * 1024ULL);
uint64_t usedMB = SD.usedBytes() / (1024ULL * 1024ULL);
logf("SD type: %s", cardTypeToString(cardType));
logf("SD size: %llu MB", cardSizeMB);
logf("FS total: %llu MB", totalMB);
logf("FS used : %llu MB", usedMB);
logf("SPI bus: %s @ %lu Hz", sdBusName_, (unsigned long)sdFreq_);
}
bool StartupSdManager::ensureDirRecursive(const char* path) {
String full(path);
if (!full.startsWith("/")) {
full = "/" + full;
}
int start = 1;
while (start > 0 && start < (int)full.length()) {
int slash = full.indexOf('/', start);
String partial = (slash < 0) ? full : full.substring(0, slash);
if (!SD.exists(partial.c_str()) && !SD.mkdir(partial.c_str())) {
logf("ERROR: mkdir failed for %s", partial.c_str());
return false;
}
if (slash < 0) {
break;
}
start = slash + 1;
}
return true;
}
bool StartupSdManager::rewriteFile(const char* path, const char* payload) {
if (SD.exists(path) && !SD.remove(path)) {
logf("ERROR: failed to erase %s", path);
return false;
}
File f = SD.open(path, FILE_WRITE);
if (!f) {
logf("ERROR: failed to create %s", path);
return false;
}
size_t wrote = f.println(payload);
f.close();
if (wrote == 0) {
logf("ERROR: write failed for %s", path);
return false;
}
return true;
}
void StartupSdManager::permissionsDemo(const char* path) {
logf("Permissions demo: FAT has no Unix chmod/chown, use open mode only.");
File r = SD.open(path, FILE_READ);
if (!r) {
logf("Could not open %s as FILE_READ", path);
return;
}
size_t writeInReadMode = r.print("attempt write while opened read-only");
if (writeInReadMode == 0) {
logf("As expected, FILE_READ write was blocked.");
} else {
logf("NOTE: FILE_READ write returned %u (unexpected)", (unsigned)writeInReadMode);
}
r.close();
}
void StartupSdManager::setStateMounted() {
if (watchState_ != SdWatchState::MOUNTED) {
logf("EVENT: card inserted/mounted");
mountedEventPending_ = true;
notify(SdEvent::CARD_MOUNTED, "SD card mounted");
}
watchState_ = SdWatchState::MOUNTED;
}
void StartupSdManager::setStateAbsent() {
if (watchState_ == SdWatchState::MOUNTED) {
logf("EVENT: card removed/unavailable");
removedEventPending_ = true;
notify(SdEvent::CARD_REMOVED, "SD card removed");
} else if (watchState_ != SdWatchState::ABSENT) {
logf("EVENT: no card detected");
notify(SdEvent::NO_CARD, "Missing SD card or invalid FAT16/FAT32 format");
}
SD.end();
watchState_ = SdWatchState::ABSENT;
}

View file

@ -0,0 +1,90 @@
#pragma once
#include <Arduino.h>
#include <SD.h>
#include <SPI.h>
#include <Wire.h>
#include "tbeam_supreme_adapter.h"
enum class SdWatchState : uint8_t {
UNKNOWN = 0,
ABSENT,
MOUNTED
};
enum class SdEvent : uint8_t {
NO_CARD,
CARD_MOUNTED,
CARD_REMOVED
};
using SdStatusCallback = void (*)(SdEvent event, const char* message);
struct SdWatcherConfig {
bool enableSdRailCycle = true;
bool enablePinDumps = true;
bool recoveryRailCycleOnFullScan = true;
uint32_t recoveryRailOffMs = 250;
uint32_t recoveryRailOnSettleMs = 700;
uint32_t startupWarmupMs = 1500;
uint32_t pollIntervalAbsentMs = 1000;
uint32_t pollIntervalMountedMs = 2000;
uint32_t fullScanIntervalMs = 10000;
uint8_t votesToPresent = 2;
uint8_t votesToAbsent = 5;
};
class StartupSdManager {
public:
explicit StartupSdManager(Print& serial = Serial);
bool begin(const SdWatcherConfig& cfg, SdStatusCallback callback = nullptr);
void update();
bool isMounted() const { return watchState_ == SdWatchState::MOUNTED; }
SdWatchState state() const { return watchState_; }
bool consumeMountedEvent();
bool consumeRemovedEvent();
void printCardInfo();
bool ensureDirRecursive(const char* path);
bool rewriteFile(const char* path, const char* payload);
void permissionsDemo(const char* path);
private:
void logf(const char* fmt, ...);
void notify(SdEvent event, const char* message);
void forceSpiDeselected();
void dumpSdPins(const char* tag);
bool initPmuForSdPower();
void cycleSdRail(uint32_t offMs = 250, uint32_t onSettleMs = 600);
bool tryMountWithBus(SPIClass& bus, const char* busName, uint32_t hz, bool verbose);
bool mountPreferred(bool verbose);
bool mountCardFullScan();
bool verifyMountedCard();
const char* cardTypeToString(uint8_t type);
void setStateMounted();
void setStateAbsent();
Print& serial_;
SdWatcherConfig cfg_{};
SdStatusCallback callback_ = nullptr;
SPIClass sdSpiH_{HSPI};
SPIClass sdSpiF_{FSPI};
SPIClass* sdSpi_ = nullptr;
const char* sdBusName_ = "none";
uint32_t sdFreq_ = 0;
XPowersLibInterface* pmu_ = nullptr;
SdWatchState watchState_ = SdWatchState::UNKNOWN;
uint8_t presentVotes_ = 0;
uint8_t absentVotes_ = 0;
uint32_t lastPollMs_ = 0;
uint32_t lastFullScanMs_ = 0;
uint32_t logSeq_ = 0;
bool mountedEventPending_ = false;
bool removedEventPending_ = false;
};

View file

@ -0,0 +1,12 @@
{
"name": "startup_sd",
"version": "0.1.0",
"dependencies": [
{
"name": "XPowersLib"
},
{
"name": "Wire"
}
]
}

View file

@ -0,0 +1,96 @@
#include "SystemStartup.h"
#include <Wire.h>
#include <U8g2lib.h>
#ifndef OLED_SDA
#define OLED_SDA 17
#endif
#ifndef OLED_SCL
#define OLED_SCL 18
#endif
#ifndef OLED_ADDR
#define OLED_ADDR 0x3C
#endif
static const bool kEnableOled = true;
static U8G2_SH1106_128X64_NONAME_F_HW_I2C g_oled(U8G2_R0, /* reset=*/U8X8_PIN_NONE);
static SystemStartup* g_activeSystemStartup = nullptr;
static void forceSpiDeselectedEarly() {
pinMode(tbeam_supreme::sdCs(), OUTPUT);
digitalWrite(tbeam_supreme::sdCs(), HIGH);
pinMode(tbeam_supreme::imuCs(), OUTPUT);
digitalWrite(tbeam_supreme::imuCs(), HIGH);
}
SystemStartup::SystemStartup(Print& serial) : serial_(serial), sd_(serial) {}
bool SystemStartup::begin(const SystemStartupConfig& cfg, SystemEventCallback callback) {
cfg_ = cfg;
callback_ = callback;
g_activeSystemStartup = this;
// Match Exercise 05 behavior: deselect SPI devices immediately at startup.
forceSpiDeselectedEarly();
if (kEnableOled) {
Wire.begin(OLED_SDA, OLED_SCL);
g_oled.setI2CAddress(OLED_ADDR << 1);
g_oled.begin();
}
emit(SystemEvent::BOOTING, "System startup booting");
oledShow3("System Startup", "Booting...");
serial_.printf("Sleeping for %lu ms to allow Serial Monitor connection...\r\n",
(unsigned long)cfg_.serialDelayMs);
delay(cfg_.serialDelayMs);
return sd_.begin(cfg_.sd, &SystemStartup::onSdEventThunk);
}
void SystemStartup::update() {
sd_.update();
}
void SystemStartup::onSdEventThunk(SdEvent event, const char* message) {
if (g_activeSystemStartup != nullptr) {
g_activeSystemStartup->onSdEvent(event, message);
}
}
void SystemStartup::onSdEvent(SdEvent event, const char* message) {
if (event == SdEvent::NO_CARD) {
oledShow3("SD missing or", "invalid FAT16/32", "Insert/format card");
emit(SystemEvent::SD_MISSING, message);
} else if (event == SdEvent::CARD_MOUNTED) {
oledShow3("SD card ready", "Mounted OK");
emit(SystemEvent::SD_READY, message);
} else if (event == SdEvent::CARD_REMOVED) {
oledShow3("SD card removed", "Please re-insert");
emit(SystemEvent::SD_REMOVED, message);
}
}
void SystemStartup::emit(SystemEvent event, const char* message) {
serial_.printf("[SYSTEM] %s\r\n", message);
if (callback_ != nullptr) {
callback_(event, message);
}
}
void SystemStartup::oledShow3(const char* l1, const char* l2, const char* l3) {
if (!kEnableOled) {
return;
}
g_oled.clearBuffer();
g_oled.setFont(u8g2_font_6x10_tf);
if (l1) g_oled.drawUTF8(0, 16, l1);
if (l2) g_oled.drawUTF8(0, 32, l2);
if (l3) g_oled.drawUTF8(0, 48, l3);
g_oled.sendBuffer();
}

View file

@ -0,0 +1,46 @@
#pragma once
#include <Arduino.h>
#include "StartupSdManager.h"
// Convenience alias so sketches can use System.println(...) style logging.
// Arduino exposes Serial, not System, so map System -> Serial.
#ifndef System
#define System Serial
#endif
enum class SystemEvent : uint8_t {
BOOTING = 0,
SD_MISSING,
SD_READY,
SD_REMOVED
};
using SystemEventCallback = void (*)(SystemEvent event, const char* message);
struct SystemStartupConfig {
uint32_t serialDelayMs = 5000;
SdWatcherConfig sd{};
};
class SystemStartup {
public:
explicit SystemStartup(Print& serial = Serial);
bool begin(const SystemStartupConfig& cfg = SystemStartupConfig{}, SystemEventCallback callback = nullptr);
void update();
bool isSdMounted() const { return sd_.isMounted(); }
StartupSdManager& sdManager() { return sd_; }
private:
static void onSdEventThunk(SdEvent event, const char* message);
void onSdEvent(SdEvent event, const char* message);
void emit(SystemEvent event, const char* message);
void oledShow3(const char* l1, const char* l2 = nullptr, const char* l3 = nullptr);
Print& serial_;
SystemStartupConfig cfg_{};
SystemEventCallback callback_ = nullptr;
StartupSdManager sd_;
};

View file

@ -0,0 +1,15 @@
{
"name": "system_startup",
"version": "0.1.0",
"dependencies": [
{
"name": "startup_sd"
},
{
"name": "U8g2"
},
{
"name": "Wire"
}
]
}

View file

@ -0,0 +1,37 @@
; 20260213 ChatGPT
; $Id$
; $HeadURL$
[platformio]
default_envs = node_a
[env]
platform = espressif32
framework = arduino
board = esp32-s3-devkitc-1
monitor_speed = 115200
lib_deps =
lewisxhe/XPowersLib@0.3.3
Wire
olikraus/U8g2@^2.36.4
; SD pins based on T-Beam S3 core pin mapping
build_flags =
-I ../../shared/boards
-I ../../external/microReticulum_Firmware
-D BOARD_MODEL=BOARD_TBEAM_S_V1
-D OLED_SDA=17
-D OLED_SCL=18
-D OLED_ADDR=0x3C
-D ARDUINO_USB_MODE=1
-D ARDUINO_USB_CDC_ON_BOOT=1
[env:node_a]
build_flags =
${env.build_flags}
-D NODE_LABEL=\"A\"
[env:node_b]
build_flags =
${env.build_flags}
-D NODE_LABEL=\"B\"

View file

@ -0,0 +1,508 @@
// 20260217 ChatGPT
// $Id$
// $HeadURL$
#include <Arduino.h>
#include <Wire.h>
#include <U8g2lib.h>
#include "StartupSdManager.h"
#include "tbeam_supreme_adapter.h"
#ifndef OLED_SDA
#define OLED_SDA 17
#endif
#ifndef OLED_SCL
#define OLED_SCL 18
#endif
#ifndef OLED_ADDR
#define OLED_ADDR 0x3C
#endif
#ifndef RTC_I2C_ADDR
#define RTC_I2C_ADDR 0x51
#endif
#ifndef GPS_BAUD
#define GPS_BAUD 9600
#endif
static const uint32_t kSerialDelayMs = 5000;
static const uint32_t kGpsStartupProbeMs = 20000;
static const uint32_t kMinuteMs = 60000;
static XPowersLibInterface* g_pmu = nullptr;
static StartupSdManager g_sd(Serial);
static U8G2_SH1106_128X64_NONAME_F_HW_I2C g_oled(U8G2_R0, /* reset=*/U8X8_PIN_NONE);
static HardwareSerial g_gpsSerial(1);
static uint32_t g_logSeq = 0;
static uint32_t g_lastMinuteReportMs = 0;
static bool g_prevHadSatellites = false;
static bool g_prevHadValidUtc = false;
static bool g_satellitesAcquiredAnnounced = false;
static bool g_timeAcquiredAnnounced = false;
static char g_gpsLine[128];
static size_t g_gpsLineLen = 0;
enum class GpsModuleKind : uint8_t {
UNKNOWN = 0,
L76K,
QUECTEL_TODO
};
struct RtcDateTime {
uint16_t year;
uint8_t month;
uint8_t day;
uint8_t hour;
uint8_t minute;
uint8_t second;
};
struct GpsState {
GpsModuleKind module = GpsModuleKind::UNKNOWN;
bool sawAnySentence = false;
uint8_t satsUsed = 0;
uint8_t satsInView = 0;
bool hasValidUtc = false;
uint16_t utcYear = 0;
uint8_t utcMonth = 0;
uint8_t utcDay = 0;
uint8_t utcHour = 0;
uint8_t utcMinute = 0;
uint8_t utcSecond = 0;
};
static GpsState g_gps;
static void logf(const char* fmt, ...) {
char msg[220];
va_list args;
va_start(args, fmt);
vsnprintf(msg, sizeof(msg), fmt, args);
va_end(args);
Serial.printf("[%10lu][%06lu] %s\r\n", (unsigned long)millis(), (unsigned long)g_logSeq++, msg);
}
static void oledShowLines(const char* l1,
const char* l2 = nullptr,
const char* l3 = nullptr,
const char* l4 = nullptr,
const char* l5 = nullptr) {
g_oled.clearBuffer();
g_oled.setFont(u8g2_font_5x8_tf);
if (l1) g_oled.drawUTF8(0, 12, l1);
if (l2) g_oled.drawUTF8(0, 24, l2);
if (l3) g_oled.drawUTF8(0, 36, l3);
if (l4) g_oled.drawUTF8(0, 48, l4);
if (l5) g_oled.drawUTF8(0, 60, l5);
g_oled.sendBuffer();
}
static uint8_t fromBcd(uint8_t b) {
return ((b >> 4U) * 10U) + (b & 0x0FU);
}
static bool rtcRead(RtcDateTime& out, bool& lowVoltageFlag) {
Wire1.beginTransmission(RTC_I2C_ADDR);
Wire1.write(0x02);
if (Wire1.endTransmission(false) != 0) {
return false;
}
const uint8_t need = 7;
uint8_t got = Wire1.requestFrom((int)RTC_I2C_ADDR, (int)need);
if (got != need) {
return false;
}
uint8_t sec = Wire1.read();
uint8_t min = Wire1.read();
uint8_t hour = Wire1.read();
uint8_t day = Wire1.read();
(void)Wire1.read(); // weekday
uint8_t month = Wire1.read();
uint8_t year = Wire1.read();
lowVoltageFlag = (sec & 0x80U) != 0;
out.second = fromBcd(sec & 0x7FU);
out.minute = fromBcd(min & 0x7FU);
out.hour = fromBcd(hour & 0x3FU);
out.day = fromBcd(day & 0x3FU);
out.month = fromBcd(month & 0x1FU);
uint8_t yy = fromBcd(year);
bool century = (month & 0x80U) != 0;
out.year = century ? (1900U + yy) : (2000U + yy);
return true;
}
static String formatRtcNow() {
RtcDateTime now{};
bool lowV = false;
if (!rtcRead(now, lowV)) {
return "RTC: read failed";
}
char buf[48];
snprintf(buf,
sizeof(buf),
"RTC %04u-%02u-%02u %02u:%02u:%02u%s",
(unsigned)now.year,
(unsigned)now.month,
(unsigned)now.day,
(unsigned)now.hour,
(unsigned)now.minute,
(unsigned)now.second,
lowV ? " !LOWV" : "");
return String(buf);
}
static String gpsModuleToString(GpsModuleKind kind) {
if (kind == GpsModuleKind::L76K) {
return "L76K";
}
if (kind == GpsModuleKind::QUECTEL_TODO) {
return "Quectel/TODO";
}
return "Unknown";
}
static bool parseUInt2(const char* s, uint8_t& out) {
if (!s || !isdigit((unsigned char)s[0]) || !isdigit((unsigned char)s[1])) {
return false;
}
out = (uint8_t)((s[0] - '0') * 10 + (s[1] - '0'));
return true;
}
static void detectModuleFromText(const char* text) {
if (!text || text[0] == '\0') {
return;
}
String t(text);
t.toUpperCase();
if (t.indexOf("L76K") >= 0) {
g_gps.module = GpsModuleKind::L76K;
return;
}
if (t.indexOf("QUECTEL") >= 0 || t.indexOf("2021-10-20") >= 0 || t.indexOf("2021/10/20") >= 0) {
if (g_gps.module != GpsModuleKind::L76K) {
g_gps.module = GpsModuleKind::QUECTEL_TODO;
}
}
}
static void parseGga(char* fields[], int count) {
if (count <= 7) {
return;
}
int sats = atoi(fields[7]);
if (sats >= 0 && sats <= 255) {
g_gps.satsUsed = (uint8_t)sats;
}
}
static void parseGsv(char* fields[], int count) {
if (count <= 3) {
return;
}
int sats = atoi(fields[3]);
if (sats >= 0 && sats <= 255) {
g_gps.satsInView = (uint8_t)sats;
}
}
static void parseRmc(char* fields[], int count) {
if (count <= 9) {
return;
}
const char* utc = fields[1];
const char* status = fields[2];
const char* date = fields[9];
if (!status || status[0] != 'A') {
return;
}
if (!utc || strlen(utc) < 6 || !date || strlen(date) < 6) {
return;
}
uint8_t hh = 0, mm = 0, ss = 0;
uint8_t dd = 0, mo = 0, yy = 0;
if (!parseUInt2(utc + 0, hh) || !parseUInt2(utc + 2, mm) || !parseUInt2(utc + 4, ss)) {
return;
}
if (!parseUInt2(date + 0, dd) || !parseUInt2(date + 2, mo) || !parseUInt2(date + 4, yy)) {
return;
}
g_gps.utcHour = hh;
g_gps.utcMinute = mm;
g_gps.utcSecond = ss;
g_gps.utcDay = dd;
g_gps.utcMonth = mo;
g_gps.utcYear = (uint16_t)(2000U + yy);
g_gps.hasValidUtc = true;
}
static void parseTxt(char* fields[], int count) {
if (count <= 4) {
return;
}
detectModuleFromText(fields[4]);
}
static void processNmeaLine(char* line) {
if (!line || line[0] != '$') {
return;
}
g_gps.sawAnySentence = true;
char* star = strchr(line, '*');
if (star) {
*star = '\0';
}
char* fields[24] = {0};
int count = 0;
char* saveptr = nullptr;
char* tok = strtok_r(line, ",", &saveptr);
while (tok && count < 24) {
fields[count++] = tok;
tok = strtok_r(nullptr, ",", &saveptr);
}
if (count <= 0 || !fields[0]) {
return;
}
const char* header = fields[0];
size_t n = strlen(header);
if (n < 6) {
return;
}
const char* type = header + (n - 3);
if (strcmp(type, "GGA") == 0) {
parseGga(fields, count);
} else if (strcmp(type, "GSV") == 0) {
parseGsv(fields, count);
} else if (strcmp(type, "RMC") == 0) {
parseRmc(fields, count);
} else if (strcmp(type, "TXT") == 0) {
parseTxt(fields, count);
}
}
static void pollGpsSerial() {
while (g_gpsSerial.available() > 0) {
char c = (char)g_gpsSerial.read();
if (c == '\r') {
continue;
}
if (c == '\n') {
if (g_gpsLineLen > 0) {
g_gpsLine[g_gpsLineLen] = '\0';
processNmeaLine(g_gpsLine);
g_gpsLineLen = 0;
}
continue;
}
if (g_gpsLineLen + 1 < sizeof(g_gpsLine)) {
g_gpsLine[g_gpsLineLen++] = c;
} else {
g_gpsLineLen = 0;
}
}
}
static uint8_t bestSatelliteCount() {
return (g_gps.satsUsed > g_gps.satsInView) ? g_gps.satsUsed : g_gps.satsInView;
}
static bool isUnsupportedQuectelMode() {
return g_gps.module == GpsModuleKind::QUECTEL_TODO;
}
static void reportStatusToSerial() {
uint8_t sats = bestSatelliteCount();
logf("GPS module: %s", gpsModuleToString(g_gps.module).c_str());
logf("GPS sentences seen: %s", g_gps.sawAnySentence ? "yes" : "no");
logf("GPS satellites: used=%u in-view=%u best=%u",
(unsigned)g_gps.satsUsed,
(unsigned)g_gps.satsInView,
(unsigned)sats);
logf("GPS can provide time from satellites: %s", g_gps.hasValidUtc ? "YES" : "NO");
}
static void maybeAnnounceGpsTransitions() {
if (isUnsupportedQuectelMode()) {
return;
}
uint8_t sats = bestSatelliteCount();
bool hasSats = sats > 0;
bool hasUtc = g_gps.hasValidUtc;
if (!g_satellitesAcquiredAnnounced && !g_prevHadSatellites && hasSats) {
String rtc = formatRtcNow();
char l2[28];
snprintf(l2, sizeof(l2), "Satellites: %u", (unsigned)sats);
oledShowLines("GPS acquired", l2, "Satellite lock found", "Waiting for UTC...", rtc.c_str());
logf("Transition: satellites acquired (%u)", (unsigned)sats);
g_satellitesAcquiredAnnounced = true;
}
if (!g_timeAcquiredAnnounced && !g_prevHadValidUtc && hasUtc) {
char line2[40];
char line3[28];
snprintf(line2,
sizeof(line2),
"%04u-%02u-%02u %02u:%02u:%02u",
(unsigned)g_gps.utcYear,
(unsigned)g_gps.utcMonth,
(unsigned)g_gps.utcDay,
(unsigned)g_gps.utcHour,
(unsigned)g_gps.utcMinute,
(unsigned)g_gps.utcSecond);
snprintf(line3, sizeof(line3), "Satellites: %u", (unsigned)sats);
oledShowLines("GPS UTC acquired", line2, line3, "Source: L76K");
logf("Transition: GPS UTC acquired: %s", line2);
g_timeAcquiredAnnounced = true;
}
g_prevHadSatellites = hasSats;
g_prevHadValidUtc = hasUtc;
}
static void drawMinuteStatus() {
if (isUnsupportedQuectelMode()) {
oledShowLines("GPS module mismatch", "Quectel detected", "L76K required", "TODO: implement", "Quectel support");
logf("GPS module mismatch: Quectel detected but this exercise currently supports only L76K (TODO)");
return;
}
uint8_t sats = bestSatelliteCount();
if (g_gps.hasValidUtc) {
char line2[40];
char line3[28];
snprintf(line2,
sizeof(line2),
"%04u-%02u-%02u %02u:%02u:%02u",
(unsigned)g_gps.utcYear,
(unsigned)g_gps.utcMonth,
(unsigned)g_gps.utcDay,
(unsigned)g_gps.utcHour,
(unsigned)g_gps.utcMinute,
(unsigned)g_gps.utcSecond);
snprintf(line3, sizeof(line3), "Satellites: %u", (unsigned)sats);
oledShowLines("GPS time (UTC)", line2, line3, "Source: L76K");
logf("GPS time (UTC): %s satellites=%u", line2, (unsigned)sats);
return;
}
String rtc = formatRtcNow();
if (sats > 0) {
char line2[28];
snprintf(line2, sizeof(line2), "Satellites: %u", (unsigned)sats);
oledShowLines("GPS signal detected", line2, "GPS UTC not ready", "yet, using RTC", rtc.c_str());
logf("Satellites detected (%u) but GPS UTC not ready. %s", (unsigned)sats, rtc.c_str());
} else {
oledShowLines("Unable to acquire", "satellites", "Take me outside so I", "can see satellites", rtc.c_str());
logf("Unable to acquire satellites. %s", rtc.c_str());
}
}
void setup() {
Serial.begin(115200);
delay(kSerialDelayMs);
Serial.println("\r\n==================================================");
Serial.println("Exercise 09: GPS Time");
Serial.println("==================================================");
if (!tbeam_supreme::initPmuForPeripherals(g_pmu, &Serial)) {
logf("PMU init failed");
}
Wire.begin(OLED_SDA, OLED_SCL);
g_oled.setI2CAddress(OLED_ADDR << 1);
g_oled.begin();
oledShowLines("GPS Time exercise", "Booting...");
SdWatcherConfig sdCfg{};
if (!g_sd.begin(sdCfg, nullptr)) {
logf("SD startup manager begin() failed");
}
#ifdef GPS_WAKEUP_PIN
pinMode(GPS_WAKEUP_PIN, OUTPUT);
digitalWrite(GPS_WAKEUP_PIN, HIGH);
#endif
#ifdef GPS_1PPS_PIN
pinMode(GPS_1PPS_PIN, INPUT);
#endif
#ifdef GPS_RX_PIN
#ifdef GPS_TX_PIN
g_gpsSerial.begin(GPS_BAUD, SERIAL_8N1, GPS_RX_PIN, GPS_TX_PIN);
#else
g_gpsSerial.begin(GPS_BAUD);
#endif
#else
g_gpsSerial.begin(GPS_BAUD);
#endif
logf("GPS UART started at %u baud", (unsigned)GPS_BAUD);
oledShowLines("GPS startup probe", "Checking satellites", "and GPS time...");
uint32_t probeStart = millis();
uint32_t lastProbeUiMs = 0;
while ((uint32_t)(millis() - probeStart) < kGpsStartupProbeMs) {
pollGpsSerial();
g_sd.update();
uint32_t now = millis();
if ((uint32_t)(now - lastProbeUiMs) >= 1000) {
lastProbeUiMs = now;
char l3[28];
char l4[30];
snprintf(l3, sizeof(l3), "Sats: %u", (unsigned)bestSatelliteCount());
snprintf(l4, sizeof(l4), "Module: %s", gpsModuleToString(g_gps.module).c_str());
oledShowLines("GPS startup probe", "Checking satellites", l3, l4);
}
delay(10);
}
reportStatusToSerial();
g_prevHadSatellites = (bestSatelliteCount() > 0);
g_prevHadValidUtc = g_gps.hasValidUtc;
drawMinuteStatus();
g_lastMinuteReportMs = millis();
}
void loop() {
pollGpsSerial();
g_sd.update();
maybeAnnounceGpsTransitions();
uint32_t now = millis();
if ((uint32_t)(now - g_lastMinuteReportMs) >= kMinuteMs) {
g_lastMinuteReportMs = now;
drawMinuteStatus();
}
}

View file

@ -0,0 +1,98 @@
## Exercise 10: Simple GPS (No SD)
Goal: verify GPS satellite and UTC time acquisition on T-Beam Supreme using OLED-only status updates.
## Current behavior
1. Boots PMU, OLED, RTC, and GPS UART.
2. Runs an active startup GPS probe (multi-baud + query commands) to detect GPS serial traffic.
3. Every 30 seconds:
- Shows `Trying to locate satellites` + `NMEA seen: yes/no` + current RTC time.
- Continues parsing GPS NMEA data.
- If GPS UTC is valid, shows GPS UTC + satellite count + `NMEA seen: yes/no`.
- Otherwise shows `Take me outside` + `NMEA seen: yes/no` + RTC.
4. No SD card logic is used in this exercise.
## Walk-through: original approach and why
Initial implementation used a minimal/simple GPS strategy:
1. Power up PMU rails using the existing T-Beam adapter.
2. Start `Serial1` at 9600 baud.
3. Parse incoming NMEA (`GGA/GSV/RMC`) passively.
4. Show periodic OLED status every 30 seconds.
Why this was chosen:
- It is the smallest path to validate basic GPS lock/time behavior.
- It avoids introducing SD complexity while isolating GPS.
- It is easy for field testing (OLED-first, battery-powered).
## What was discovered by comparing with Meshtastic
Meshtastic GPS handling is more defensive and hardware-aware in principle:
1. It uses a board variant that provides explicit GPS pin mapping for the T-Beam Supreme path.
2. It initializes GPS serial with explicit RX/TX pins and larger receive buffers.
3. It performs active startup probing (commands + response checks), not only passive listening.
4. It attempts detection across known module families and may try multiple serial settings.
5. It manages GNSS-related power/standby states deliberately (rather than assuming default UART traffic immediately appears).
## What differed in this exercise and likely caused the issue
The first Exercise 10 version was built on `esp32-s3-devkitc-1` with conditional pin usage.
- If GPS pin macros are not present, `Serial1` can start on default pins.
- That can produce `NMEA seen: no` forever even outdoors, because firmware is listening on the wrong UART pins.
## Corrections applied after Meshtastic review
1. Added explicit GPS pin defines in `platformio.ini`:
- `GPS_RX_PIN=9`
- `GPS_TX_PIN=8`
- `GPS_WAKEUP_PIN=7`
- `GPS_1PPS_PIN=6`
2. Forced UART startup using explicit RX/TX pins.
3. Added startup multi-baud active probe and common GPS query commands.
4. Added OLED `NMEA seen: yes/no` so field tests distinguish:
- `no sky fix yet` vs
- `no GPS serial traffic at all`.
## Field Test Checklist
1. Flash and reboot outdoors with clear sky view.
2. Confirm the OLED updates every 30 seconds.
3. Watch for this expected progression:
- `Trying to locate satellites` + `NMEA seen: no`
- then `Trying to locate satellites` + `NMEA seen: yes`
- then either:
- `GPS lock acquired` with UTC and satellite count, or
- `Take me outside` if no fix yet.
4. Keep unit stationary for 2-5 minutes for first lock after cold start.
Interpretation guide:
- `NMEA seen: no`: likely UART/pin/baud/module-power communication issue.
- `NMEA seen: yes` + no lock: GPS is talking, but no valid fix yet (sky view/time-to-first-fix issue).
- `GPS lock acquired`: fix is valid; UTC and satellites are available from GPS.
- RTC line updates every 30 seconds: loop is alive and retry cycle is running.
If still failing:
1. Capture serial log from boot through at least 2 full 30-second cycles.
2. Note whether `NMEA seen` ever changes from `no` to `yes`.
3. Record whether GPS startup probe reports traffic at any baud rate.
## Build
```bash
source /home/jlpoole/rnsenv/bin/activate
pio run -e node_a
```
## Upload
```bash
source /home/jlpoole/rnsenv/bin/activate
pio run -e node_a -t upload --upload-port /dev/ttyACM0
```

View file

@ -0,0 +1,40 @@
; 20260217 ChatGPT
; $Id$
; $HeadURL$
[platformio]
default_envs = node_a
[env]
platform = espressif32
framework = arduino
board = esp32-s3-devkitc-1
monitor_speed = 115200
lib_deps =
lewisxhe/XPowersLib@0.3.3
Wire
olikraus/U8g2@^2.36.4
build_flags =
-I ../../shared/boards
-I ../../external/microReticulum_Firmware
-D BOARD_MODEL=BOARD_TBEAM_S_V1
-D OLED_SDA=17
-D OLED_SCL=18
-D OLED_ADDR=0x3C
-D GPS_RX_PIN=9
-D GPS_TX_PIN=8
-D GPS_WAKEUP_PIN=7
-D GPS_1PPS_PIN=6
-D ARDUINO_USB_MODE=1
-D ARDUINO_USB_CDC_ON_BOOT=1
[env:node_a]
build_flags =
${env.build_flags}
-D NODE_LABEL=\"A\"
[env:node_b]
build_flags =
${env.build_flags}
-D NODE_LABEL=\"B\"

View file

@ -0,0 +1,431 @@
// 20260217 ChatGPT
// $Id$
// $HeadURL$
#include <Arduino.h>
#include <Wire.h>
#include <U8g2lib.h>
#include "tbeam_supreme_adapter.h"
#ifndef OLED_SDA
#define OLED_SDA 17
#endif
#ifndef OLED_SCL
#define OLED_SCL 18
#endif
#ifndef OLED_ADDR
#define OLED_ADDR 0x3C
#endif
#ifndef RTC_I2C_ADDR
#define RTC_I2C_ADDR 0x51
#endif
#ifndef GPS_BAUD
#define GPS_BAUD 9600
#endif
static const uint32_t kSerialDelayMs = 5000;
static const uint32_t kReportIntervalMs = 30000;
static XPowersLibInterface* g_pmu = nullptr;
static U8G2_SH1106_128X64_NONAME_F_HW_I2C g_oled(U8G2_R0, /* reset=*/U8X8_PIN_NONE);
static HardwareSerial g_gpsSerial(1);
static uint32_t g_logSeq = 0;
static uint32_t g_lastReportMs = 0;
static uint32_t g_gpsBaud = GPS_BAUD;
static char g_gpsLine[128];
static size_t g_gpsLineLen = 0;
struct RtcDateTime {
uint16_t year;
uint8_t month;
uint8_t day;
uint8_t hour;
uint8_t minute;
uint8_t second;
};
struct GpsState {
bool sawAnySentence = false;
uint8_t satsUsed = 0;
uint8_t satsInView = 0;
bool hasValidUtc = false;
uint16_t utcYear = 0;
uint8_t utcMonth = 0;
uint8_t utcDay = 0;
uint8_t utcHour = 0;
uint8_t utcMinute = 0;
uint8_t utcSecond = 0;
};
static GpsState g_gps;
static void logf(const char* fmt, ...) {
char msg[220];
va_list args;
va_start(args, fmt);
vsnprintf(msg, sizeof(msg), fmt, args);
va_end(args);
Serial.printf("[%10lu][%06lu] %s\r\n", (unsigned long)millis(), (unsigned long)g_logSeq++, msg);
}
static void oledShowLines(const char* l1,
const char* l2 = nullptr,
const char* l3 = nullptr,
const char* l4 = nullptr,
const char* l5 = nullptr) {
g_oled.clearBuffer();
g_oled.setFont(u8g2_font_5x8_tf);
if (l1) g_oled.drawUTF8(0, 12, l1);
if (l2) g_oled.drawUTF8(0, 24, l2);
if (l3) g_oled.drawUTF8(0, 36, l3);
if (l4) g_oled.drawUTF8(0, 48, l4);
if (l5) g_oled.drawUTF8(0, 60, l5);
g_oled.sendBuffer();
}
static uint8_t fromBcd(uint8_t b) {
return ((b >> 4U) * 10U) + (b & 0x0FU);
}
static bool rtcRead(RtcDateTime& out, bool& lowVoltageFlag) {
Wire1.beginTransmission(RTC_I2C_ADDR);
Wire1.write(0x02);
if (Wire1.endTransmission(false) != 0) {
return false;
}
const uint8_t need = 7;
uint8_t got = Wire1.requestFrom((int)RTC_I2C_ADDR, (int)need);
if (got != need) {
return false;
}
uint8_t sec = Wire1.read();
uint8_t min = Wire1.read();
uint8_t hour = Wire1.read();
uint8_t day = Wire1.read();
(void)Wire1.read();
uint8_t month = Wire1.read();
uint8_t year = Wire1.read();
lowVoltageFlag = (sec & 0x80U) != 0;
out.second = fromBcd(sec & 0x7FU);
out.minute = fromBcd(min & 0x7FU);
out.hour = fromBcd(hour & 0x3FU);
out.day = fromBcd(day & 0x3FU);
out.month = fromBcd(month & 0x1FU);
uint8_t yy = fromBcd(year);
bool century = (month & 0x80U) != 0;
out.year = century ? (1900U + yy) : (2000U + yy);
return true;
}
static String formatRtcNow() {
RtcDateTime now{};
bool lowV = false;
if (!rtcRead(now, lowV)) {
return "RTC read failed";
}
char buf[48];
snprintf(buf,
sizeof(buf),
"RTC %04u-%02u-%02u %02u:%02u:%02u%s",
(unsigned)now.year,
(unsigned)now.month,
(unsigned)now.day,
(unsigned)now.hour,
(unsigned)now.minute,
(unsigned)now.second,
lowV ? " !LOWV" : "");
return String(buf);
}
static bool parseUInt2(const char* s, uint8_t& out) {
if (!s || !isdigit((unsigned char)s[0]) || !isdigit((unsigned char)s[1])) {
return false;
}
out = (uint8_t)((s[0] - '0') * 10 + (s[1] - '0'));
return true;
}
static void parseGga(char* fields[], int count) {
if (count <= 7) {
return;
}
int sats = atoi(fields[7]);
if (sats >= 0 && sats <= 255) {
g_gps.satsUsed = (uint8_t)sats;
}
}
static void parseGsv(char* fields[], int count) {
if (count <= 3) {
return;
}
int sats = atoi(fields[3]);
if (sats >= 0 && sats <= 255) {
g_gps.satsInView = (uint8_t)sats;
}
}
static void parseRmc(char* fields[], int count) {
if (count <= 9) {
return;
}
const char* utc = fields[1];
const char* status = fields[2];
const char* date = fields[9];
if (!status || status[0] != 'A') {
return;
}
if (!utc || strlen(utc) < 6 || !date || strlen(date) < 6) {
return;
}
uint8_t hh = 0, mm = 0, ss = 0;
uint8_t dd = 0, mo = 0, yy = 0;
if (!parseUInt2(utc + 0, hh) || !parseUInt2(utc + 2, mm) || !parseUInt2(utc + 4, ss)) {
return;
}
if (!parseUInt2(date + 0, dd) || !parseUInt2(date + 2, mo) || !parseUInt2(date + 4, yy)) {
return;
}
g_gps.utcHour = hh;
g_gps.utcMinute = mm;
g_gps.utcSecond = ss;
g_gps.utcDay = dd;
g_gps.utcMonth = mo;
g_gps.utcYear = (uint16_t)(2000U + yy);
g_gps.hasValidUtc = true;
}
static void processNmeaLine(char* line) {
if (!line || line[0] != '$') {
return;
}
g_gps.sawAnySentence = true;
char* star = strchr(line, '*');
if (star) {
*star = '\0';
}
char* fields[24] = {0};
int count = 0;
char* saveptr = nullptr;
char* tok = strtok_r(line, ",", &saveptr);
while (tok && count < 24) {
fields[count++] = tok;
tok = strtok_r(nullptr, ",", &saveptr);
}
if (count <= 0 || !fields[0]) {
return;
}
const char* header = fields[0];
size_t n = strlen(header);
if (n < 6) {
return;
}
const char* type = header + (n - 3);
if (strcmp(type, "GGA") == 0) {
parseGga(fields, count);
} else if (strcmp(type, "GSV") == 0) {
parseGsv(fields, count);
} else if (strcmp(type, "RMC") == 0) {
parseRmc(fields, count);
}
}
static void pollGpsSerial() {
while (g_gpsSerial.available() > 0) {
char c = (char)g_gpsSerial.read();
if (c == '\r') {
continue;
}
if (c == '\n') {
if (g_gpsLineLen > 0) {
g_gpsLine[g_gpsLineLen] = '\0';
processNmeaLine(g_gpsLine);
g_gpsLineLen = 0;
}
continue;
}
if (g_gpsLineLen + 1 < sizeof(g_gpsLine)) {
g_gpsLine[g_gpsLineLen++] = c;
} else {
g_gpsLineLen = 0;
}
}
}
static void startGpsUart(uint32_t baud) {
g_gpsSerial.end();
delay(20);
g_gpsSerial.setRxBufferSize(1024);
g_gpsSerial.begin(baud, SERIAL_8N1, GPS_RX_PIN, GPS_TX_PIN);
g_gpsBaud = baud;
}
static bool collectGpsTraffic(uint32_t windowMs) {
uint32_t start = millis();
size_t bytesSeen = 0;
while ((uint32_t)(millis() - start) < windowMs) {
while (g_gpsSerial.available() > 0) {
(void)g_gpsSerial.read();
bytesSeen++;
}
pollGpsSerial();
delay(2);
}
return bytesSeen > 0 || g_gps.sawAnySentence;
}
static bool probeGpsAtBaud(uint32_t baud) {
startGpsUart(baud);
logf("Probing GPS at %lu baud...", (unsigned long)baud);
if (collectGpsTraffic(700)) {
return true;
}
// Try common query/wake commands used by MTK/L76K and related chipsets.
g_gpsSerial.write("$PCAS06,0*1B\r\n"); // Request module SW text
g_gpsSerial.write("$PMTK605*31\r\n"); // MTK firmware query
g_gpsSerial.write("$PQTMVERNO*58\r\n"); // Quectel LC86 query
g_gpsSerial.write("$PMTK353,1,1,1,1,1*2A\r\n");
g_gpsSerial.write("$PMTK314,0,1,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0*29\r\n");
return collectGpsTraffic(1200);
}
static void initialGpsProbe() {
const uint32_t bauds[] = {GPS_BAUD, 115200, 38400, 57600, 19200};
for (size_t i = 0; i < sizeof(bauds) / sizeof(bauds[0]); ++i) {
if (probeGpsAtBaud(bauds[i])) {
logf("GPS traffic detected at %lu baud", (unsigned long)g_gpsBaud);
return;
}
}
logf("No GPS traffic detected during startup probe");
}
static uint8_t bestSatelliteCount() {
return (g_gps.satsUsed > g_gps.satsInView) ? g_gps.satsUsed : g_gps.satsInView;
}
static void showTryingMessage() {
String rtc = formatRtcNow();
oledShowLines("Trying to locate",
"satellites",
g_gps.sawAnySentence ? "NMEA seen: yes" : "NMEA seen: no",
rtc.c_str());
logf("Trying to locate satellites. %s", rtc.c_str());
}
static void showStatusMessage() {
uint8_t sats = bestSatelliteCount();
if (g_gps.hasValidUtc) {
char line2[40];
char line3[28];
snprintf(line2,
sizeof(line2),
"GPS UTC %04u-%02u-%02u",
(unsigned)g_gps.utcYear,
(unsigned)g_gps.utcMonth,
(unsigned)g_gps.utcDay);
snprintf(line3,
sizeof(line3),
"%02u:%02u:%02u sats:%u",
(unsigned)g_gps.utcHour,
(unsigned)g_gps.utcMinute,
(unsigned)g_gps.utcSecond,
(unsigned)sats);
oledShowLines("GPS lock acquired",
line2,
line3,
g_gps.sawAnySentence ? "NMEA seen: yes" : "NMEA seen: no");
logf("GPS lock acquired. %s sats=%u", line3, (unsigned)sats);
return;
}
String rtc = formatRtcNow();
oledShowLines("Take me outside",
"No GPS time/sats yet",
g_gps.sawAnySentence ? "NMEA seen: yes" : "NMEA seen: no",
rtc.c_str());
logf("Take me outside. sats=%u, has_utc=%s, nmea_seen=%s. %s",
(unsigned)sats,
g_gps.hasValidUtc ? "yes" : "no",
g_gps.sawAnySentence ? "yes" : "no",
rtc.c_str());
}
void setup() {
Serial.begin(115200);
delay(kSerialDelayMs);
Serial.println("\r\n==================================================");
Serial.println("Exercise 10: Simple GPS (No SD)");
Serial.println("==================================================");
if (!tbeam_supreme::initPmuForPeripherals(g_pmu, &Serial)) {
logf("PMU init failed");
}
Wire.begin(OLED_SDA, OLED_SCL);
g_oled.setI2CAddress(OLED_ADDR << 1);
g_oled.begin();
oledShowLines("Simple GPS", "Booting...");
#ifdef GPS_1PPS_PIN
pinMode(GPS_1PPS_PIN, INPUT);
#endif
#ifdef GPS_WAKEUP_PIN
// Keep wake pin in a neutral state similar to Meshtastic behavior.
pinMode(GPS_WAKEUP_PIN, INPUT);
#endif
startGpsUart(GPS_BAUD);
logf("GPS UART started: RX=%d TX=%d baud=%lu", GPS_RX_PIN, GPS_TX_PIN, (unsigned long)g_gpsBaud);
initialGpsProbe();
showTryingMessage();
g_lastReportMs = millis();
}
void loop() {
pollGpsSerial();
uint32_t now = millis();
if ((uint32_t)(now - g_lastReportMs) >= kReportIntervalMs) {
g_lastReportMs = now;
showTryingMessage();
uint32_t start = millis();
while ((uint32_t)(millis() - start) < 2000) {
pollGpsSerial();
delay(5);
}
showStatusMessage();
}
}