diff --git a/exercises/09_GPS_Time/README.md b/exercises/09_GPS_Time/README.md deleted file mode 100644 index a8a1a81..0000000 --- a/exercises/09_GPS_Time/README.md +++ /dev/null @@ -1,49 +0,0 @@ -## Exercise 09: GPS Time (L76K) - -This exercise boots the T-Beam Supreme and verifies GPS behavior at startup. - -Important sequence note: - -- Exercise 10 (`10_Simple_GPS`) should be completed before this exercise. -- Exercise 10 README contains the detailed pin-configuration explanation and troubleshooting rationale for why explicit GPS pin mapping is critical on this hardware. -- If GPS behavior is unexpected here, review Exercise 10 README first, then return to Exercise 9. - -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. - - Uses explicit GPS UART pins and an active startup probe (multi-baud + common GPS query commands), aligned with the approach validated in Exercise 10. -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 -``` diff --git a/exercises/09_GPS_Time/lib/startup_sd/StartupSdManager.cpp b/exercises/09_GPS_Time/lib/startup_sd/StartupSdManager.cpp deleted file mode 100644 index 1e8791c..0000000 --- a/exercises/09_GPS_Time/lib/startup_sd/StartupSdManager.cpp +++ /dev/null @@ -1,360 +0,0 @@ -#include "StartupSdManager.h" - -#include -#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; -} diff --git a/exercises/09_GPS_Time/lib/startup_sd/StartupSdManager.h b/exercises/09_GPS_Time/lib/startup_sd/StartupSdManager.h deleted file mode 100644 index be9ef27..0000000 --- a/exercises/09_GPS_Time/lib/startup_sd/StartupSdManager.h +++ /dev/null @@ -1,90 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#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; -}; diff --git a/exercises/09_GPS_Time/lib/startup_sd/library.json b/exercises/09_GPS_Time/lib/startup_sd/library.json deleted file mode 100644 index 4978fdd..0000000 --- a/exercises/09_GPS_Time/lib/startup_sd/library.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "startup_sd", - "version": "0.1.0", - "dependencies": [ - { - "name": "XPowersLib" - }, - { - "name": "Wire" - } - ] -} diff --git a/exercises/09_GPS_Time/lib/system_startup/SystemStartup.cpp b/exercises/09_GPS_Time/lib/system_startup/SystemStartup.cpp deleted file mode 100644 index 3a82e1f..0000000 --- a/exercises/09_GPS_Time/lib/system_startup/SystemStartup.cpp +++ /dev/null @@ -1,96 +0,0 @@ -#include "SystemStartup.h" - -#include -#include - -#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(); -} diff --git a/exercises/09_GPS_Time/lib/system_startup/SystemStartup.h b/exercises/09_GPS_Time/lib/system_startup/SystemStartup.h deleted file mode 100644 index cd83857..0000000 --- a/exercises/09_GPS_Time/lib/system_startup/SystemStartup.h +++ /dev/null @@ -1,46 +0,0 @@ -#pragma once - -#include -#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_; -}; diff --git a/exercises/09_GPS_Time/lib/system_startup/library.json b/exercises/09_GPS_Time/lib/system_startup/library.json deleted file mode 100644 index 0c2c1cf..0000000 --- a/exercises/09_GPS_Time/lib/system_startup/library.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "system_startup", - "version": "0.1.0", - "dependencies": [ - { - "name": "startup_sd" - }, - { - "name": "U8g2" - }, - { - "name": "Wire" - } - ] -} diff --git a/exercises/09_GPS_Time/platformio.ini b/exercises/09_GPS_Time/platformio.ini deleted file mode 100644 index fff0d30..0000000 --- a/exercises/09_GPS_Time/platformio.ini +++ /dev/null @@ -1,41 +0,0 @@ -; 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 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\" diff --git a/exercises/09_GPS_Time/src/main.cpp b/exercises/09_GPS_Time/src/main.cpp deleted file mode 100644 index 36b0624..0000000 --- a/exercises/09_GPS_Time/src/main.cpp +++ /dev/null @@ -1,556 +0,0 @@ -// 20260217 ChatGPT -// $Id$ -// $HeadURL$ - -#include -#include -#include - -#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 uint32_t g_gpsBaud = GPS_BAUD; - -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 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, bool updateSd) { - 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(); - if (updateSd) { - g_sd.update(); - } - 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, true)) { - return true; - } - - // Common commands for MTK/L76K and related chipsets. - g_gpsSerial.write("$PCAS06,0*1B\r\n"); - g_gpsSerial.write("$PMTK605*31\r\n"); - g_gpsSerial.write("$PQTMVERNO*58\r\n"); - 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, true); -} - -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 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 - // Keep wake pin neutral; avoid forcing an unknown standby state. - pinMode(GPS_WAKEUP_PIN, INPUT); -#endif - -#ifdef GPS_1PPS_PIN - pinMode(GPS_1PPS_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(); - - 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]; - char l5[24]; - snprintf(l3, sizeof(l3), "Sats: %u", (unsigned)bestSatelliteCount()); - snprintf(l4, sizeof(l4), "Module: %s", gpsModuleToString(g_gps.module).c_str()); - snprintf(l5, sizeof(l5), "NMEA:%s %lu", g_gps.sawAnySentence ? "yes" : "no", (unsigned long)g_gpsBaud); - oledShowLines("GPS startup probe", "Checking satellites", l3, l4, l5); - } - 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(); - } -} diff --git a/exercises/10_Simple_GPS/README.md b/exercises/10_Simple_GPS/README.md deleted file mode 100644 index 6fc5c73..0000000 --- a/exercises/10_Simple_GPS/README.md +++ /dev/null @@ -1,98 +0,0 @@ -## 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 -``` diff --git a/exercises/10_Simple_GPS/platformio.ini b/exercises/10_Simple_GPS/platformio.ini deleted file mode 100644 index a183c3b..0000000 --- a/exercises/10_Simple_GPS/platformio.ini +++ /dev/null @@ -1,40 +0,0 @@ -; 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\" diff --git a/exercises/10_Simple_GPS/src/main.cpp b/exercises/10_Simple_GPS/src/main.cpp deleted file mode 100644 index 7456125..0000000 --- a/exercises/10_Simple_GPS/src/main.cpp +++ /dev/null @@ -1,431 +0,0 @@ -// 20260217 ChatGPT -// $Id$ -// $HeadURL$ - -#include -#include -#include - -#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(); - } -} diff --git a/exercises/11_Set_RTC2GPS/README.md b/exercises/11_Set_RTC2GPS/README.md deleted file mode 100644 index 462c2d2..0000000 --- a/exercises/11_Set_RTC2GPS/README.md +++ /dev/null @@ -1,33 +0,0 @@ -## Exercise 11: Set RTC to GPS (1PPS Discipline) - -This exercise extends Exercise 9 behavior (GPS + SD + OLED) and disciplines the onboard RTC from GPS UTC using the GPS `1PPS` (pulse-per-second) timing signal. - -Implemented behavior: - -1. Boots PMU, OLED, SD watcher, and GPS UART using the same T-Beam Supreme pin mapping from prior exercises. -2. Parses NMEA (`RMC`, `GGA`, `GSV`) to track UTC validity and satellite counts. -3. Every 1 minute, attempts to set RTC from GPS: - - Uses latest valid GPS UTC. - - Waits for next `1PPS` rising edge. - - Sets RTC to GPS time aligned to that edge (UTC + 1 second). -4. Appends event records to SD file: - - Path: `/gps/discipline_rtc.log` - - Append-only writes (`FILE_APPEND`) - - Format: - - `YYYYMMDD_HH24MISS_z\t set RTC to GPS using 1PPS pulse-per-second discipline\trtc-gps drift=+/-Ns` -5. OLED success message shows RTC disciplined confirmation and timestamp. -6. If GPS time cannot be determined (or 1PPS edge is not seen in timeout), OLED shows failure status and the loop delays 30 seconds before retry. - -## 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 -``` diff --git a/exercises/11_Set_RTC2GPS/lib/startup_sd/StartupSdManager.cpp b/exercises/11_Set_RTC2GPS/lib/startup_sd/StartupSdManager.cpp deleted file mode 100644 index 1e8791c..0000000 --- a/exercises/11_Set_RTC2GPS/lib/startup_sd/StartupSdManager.cpp +++ /dev/null @@ -1,360 +0,0 @@ -#include "StartupSdManager.h" - -#include -#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; -} diff --git a/exercises/11_Set_RTC2GPS/lib/startup_sd/StartupSdManager.h b/exercises/11_Set_RTC2GPS/lib/startup_sd/StartupSdManager.h deleted file mode 100644 index be9ef27..0000000 --- a/exercises/11_Set_RTC2GPS/lib/startup_sd/StartupSdManager.h +++ /dev/null @@ -1,90 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#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; -}; diff --git a/exercises/11_Set_RTC2GPS/lib/startup_sd/library.json b/exercises/11_Set_RTC2GPS/lib/startup_sd/library.json deleted file mode 100644 index 4978fdd..0000000 --- a/exercises/11_Set_RTC2GPS/lib/startup_sd/library.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "startup_sd", - "version": "0.1.0", - "dependencies": [ - { - "name": "XPowersLib" - }, - { - "name": "Wire" - } - ] -} diff --git a/exercises/11_Set_RTC2GPS/platformio.ini b/exercises/11_Set_RTC2GPS/platformio.ini deleted file mode 100644 index a91b0bc..0000000 --- a/exercises/11_Set_RTC2GPS/platformio.ini +++ /dev/null @@ -1,42 +0,0 @@ -; 20260213 ChatGPT -; $Id$ -; $HeadURL$ - -[platformio] -default_envs = node_a - -[env] -platform = espressif32 -framework = arduino -board = esp32-s3-devkitc-1 -monitor_speed = 115200 -extra_scripts = pre:scripts/set_build_epoch.py -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 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\" diff --git a/exercises/11_Set_RTC2GPS/scripts/set_build_epoch.py b/exercises/11_Set_RTC2GPS/scripts/set_build_epoch.py deleted file mode 100644 index 3011129..0000000 --- a/exercises/11_Set_RTC2GPS/scripts/set_build_epoch.py +++ /dev/null @@ -1,12 +0,0 @@ -import time -Import("env") - -epoch = int(time.time()) -utc_tag = time.strftime("%Y%m%d_%H%M%S_z", time.gmtime(epoch)) - -env.Append( - CPPDEFINES=[ - ("FW_BUILD_EPOCH", str(epoch)), - ("FW_BUILD_UTC", '\\"%s\\"' % utc_tag), - ] -) diff --git a/exercises/11_Set_RTC2GPS/src/main.cpp b/exercises/11_Set_RTC2GPS/src/main.cpp deleted file mode 100644 index 9e54ea7..0000000 --- a/exercises/11_Set_RTC2GPS/src/main.cpp +++ /dev/null @@ -1,786 +0,0 @@ -// 20260217 ChatGPT -// $Id$ -// $HeadURL$ - -#include -#include -#include -#include - -#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 - -#ifndef FILE_APPEND -#define FILE_APPEND FILE_WRITE -#endif - -#ifndef FW_BUILD_EPOCH -#define FW_BUILD_EPOCH 0 -#endif - -#ifndef FW_BUILD_UTC -#define FW_BUILD_UTC "unknown" -#endif - -static const uint32_t kSerialDelayMs = 5000; -static const uint32_t kLoopMsDiscipline = 60000; -static const uint32_t kNoTimeDelayMs = 30000; -static const uint32_t kGpsStartupProbeMs = 20000; -static const uint32_t kPpsWaitTimeoutMs = 1500; - -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_nextDisciplineMs = 0; -static bool g_gpsPathReady = false; - -static char g_gpsLine[128]; -static size_t g_gpsLineLen = 0; - -static volatile uint32_t g_ppsEdgeCount = 0; - -struct DateTime { - uint16_t year; - uint8_t month; - uint8_t day; - uint8_t hour; - uint8_t minute; - uint8_t second; -}; - -struct GpsState { - bool sawAnySentence = false; - bool hasValidUtc = false; - bool hasValidPosition = false; - bool hasValidAltitude = false; - uint8_t satsUsed = 0; - uint8_t satsInView = 0; - float hdop = -1.0f; - float altitudeM = 0.0f; - double latitudeDeg = 0.0; - double longitudeDeg = 0.0; - DateTime utc{}; - uint32_t lastUtcMs = 0; -}; - -static GpsState g_gps; - -static void logf(const char* fmt, ...) { - char msg[240]; - 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 toBcd(uint8_t v) { - return (uint8_t)(((v / 10U) << 4U) | (v % 10U)); -} - -static uint8_t fromBcd(uint8_t b) { - return (uint8_t)(((b >> 4U) * 10U) + (b & 0x0FU)); -} - -static bool rtcRead(DateTime& 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 bool rtcWrite(const DateTime& dt) { - Wire1.beginTransmission(RTC_I2C_ADDR); - Wire1.write(0x02); - Wire1.write(toBcd(dt.second & 0x7FU)); - Wire1.write(toBcd(dt.minute)); - Wire1.write(toBcd(dt.hour)); - Wire1.write(toBcd(dt.day)); - Wire1.write(0x00); - - uint8_t monthReg = toBcd(dt.month); - if (dt.year < 2000U) { - monthReg |= 0x80U; - } - Wire1.write(monthReg); - Wire1.write(toBcd((uint8_t)(dt.year % 100U))); - - return Wire1.endTransmission() == 0; -} - -static bool isLeapYear(uint16_t y) { - return ((y % 4U) == 0U && (y % 100U) != 0U) || ((y % 400U) == 0U); -} - -static uint8_t daysInMonth(uint16_t year, uint8_t month) { - static const uint8_t kDays[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; - if (month == 2) { - return (uint8_t)(isLeapYear(year) ? 29 : 28); - } - if (month >= 1 && month <= 12) { - return kDays[month - 1]; - } - return 30; -} - -static bool isValidDateTime(const DateTime& dt) { - if (dt.year < 2000U || dt.year > 2099U) return false; - if (dt.month < 1 || dt.month > 12) return false; - if (dt.day < 1 || dt.day > daysInMonth(dt.year, dt.month)) return false; - if (dt.hour > 23 || dt.minute > 59 || dt.second > 59) return false; - return true; -} - -static int64_t daysFromCivil(int y, unsigned m, unsigned d) { - y -= (m <= 2); - const int era = (y >= 0 ? y : y - 399) / 400; - const unsigned yoe = (unsigned)(y - era * 400); - const unsigned doy = (153 * (m + (m > 2 ? (unsigned)-3 : 9)) + 2) / 5 + d - 1; - const unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; - return era * 146097 + (int)doe - 719468; -} - -static int64_t toEpochSeconds(const DateTime& dt) { - int64_t days = daysFromCivil((int)dt.year, dt.month, dt.day); - return days * 86400LL + (int64_t)dt.hour * 3600LL + (int64_t)dt.minute * 60LL + (int64_t)dt.second; -} - -static bool fromEpochSeconds(int64_t sec, DateTime& out) { - if (sec < 0) { - return false; - } - - int64_t days = sec / 86400LL; - int64_t rem = sec % 86400LL; - if (rem < 0) { - rem += 86400LL; - days -= 1; - } - - out.hour = (uint8_t)(rem / 3600LL); - rem %= 3600LL; - out.minute = (uint8_t)(rem / 60LL); - out.second = (uint8_t)(rem % 60LL); - - days += 719468; - const int era = (days >= 0 ? days : days - 146096) / 146097; - const unsigned doe = (unsigned)(days - era * 146097); - const unsigned yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; - int y = (int)yoe + era * 400; - const unsigned doy = doe - (365 * yoe + yoe / 4 - yoe / 100); - const unsigned mp = (5 * doy + 2) / 153; - const unsigned d = doy - (153 * mp + 2) / 5 + 1; - const unsigned m = mp + (mp < 10 ? 3 : (unsigned)-9); - y += (m <= 2); - - out.year = (uint16_t)y; - out.month = (uint8_t)m; - out.day = (uint8_t)d; - return isValidDateTime(out); -} - -static void addOneSecond(DateTime& dt) { - int64_t t = toEpochSeconds(dt); - DateTime out{}; - if (fromEpochSeconds(t + 1, out)) { - dt = out; - } -} - -static void formatUtcCompact(const DateTime& dt, char* out, size_t outLen) { - snprintf(out, - outLen, - "%04u%02u%02u_%02u%02u%02u_z", - (unsigned)dt.year, - (unsigned)dt.month, - (unsigned)dt.day, - (unsigned)dt.hour, - (unsigned)dt.minute, - (unsigned)dt.second); -} - -static void formatUtcHuman(const DateTime& dt, char* out, size_t outLen) { - snprintf(out, - outLen, - "%04u-%02u-%02u %02u:%02u:%02u UTC", - (unsigned)dt.year, - (unsigned)dt.month, - (unsigned)dt.day, - (unsigned)dt.hour, - (unsigned)dt.minute, - (unsigned)dt.second); -} - -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 bool parseNmeaCoordToDecimal(const char* raw, const char* hemi, bool isLat, double& outDeg) { - if (!raw || !hemi || raw[0] == '\0' || hemi[0] == '\0') { - return false; - } - - // NMEA uses ddmm.mmmm (lat) and dddmm.mmmm (lon), with leading zeros preserved. - // Parse from string slices so longitudes like 071xx.xxxx do not collapse to 7xx.xxxx. - int degDigits = isLat ? 2 : 3; - size_t n = strlen(raw); - if (n <= (size_t)degDigits + 2) { - return false; - } - - for (int i = 0; i < degDigits; ++i) { - if (!isdigit((unsigned char)raw[i])) { - return false; - } - } - - char degBuf[4] = {0}; - memcpy(degBuf, raw, degDigits); - int deg = atoi(degBuf); - - const char* minPtr = raw + degDigits; - double minutes = atof(minPtr); - if (minutes < 0.0 || minutes >= 60.0) { - return false; - } - - double dec = (double)deg + (minutes / 60.0); - char h = (char)toupper((unsigned char)hemi[0]); - if (h == 'S' || h == 'W') { - dec = -dec; - } else if (h != 'N' && h != 'E') { - return false; - } - - outDeg = dec; - return true; -} - -static void parseGga(char* fields[], int count) { - if (count <= 7) { - return; - } - const char* latRaw = (count > 2) ? fields[2] : nullptr; - const char* latHem = (count > 3) ? fields[3] : nullptr; - const char* lonRaw = (count > 4) ? fields[4] : nullptr; - const char* lonHem = (count > 5) ? fields[5] : nullptr; - int sats = atoi(fields[7]); - if (sats >= 0 && sats <= 255) { - g_gps.satsUsed = (uint8_t)sats; - } - if (count > 8 && fields[8] && fields[8][0] != '\0') { - g_gps.hdop = (float)atof(fields[8]); - } - if (count > 9 && fields[9] && fields[9][0] != '\0') { - g_gps.altitudeM = (float)atof(fields[9]); - g_gps.hasValidAltitude = true; - } - - // Position fallback from GGA so we still log coordinates if RMC position is missing. - double lat = 0.0; - double lon = 0.0; - if (parseNmeaCoordToDecimal(latRaw, latHem, true, lat) && - parseNmeaCoordToDecimal(lonRaw, lonHem, false, lon)) { - g_gps.latitudeDeg = lat; - g_gps.longitudeDeg = lon; - g_gps.hasValidPosition = true; - } -} - -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* latRaw = fields[3]; - const char* latHem = fields[4]; - const char* lonRaw = fields[5]; - const char* lonHem = fields[6]; - 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.utc.hour = hh; - g_gps.utc.minute = mm; - g_gps.utc.second = ss; - g_gps.utc.day = dd; - g_gps.utc.month = mo; - g_gps.utc.year = (uint16_t)(2000U + yy); - g_gps.hasValidUtc = true; - g_gps.lastUtcMs = millis(); - - double lat = 0.0; - double lon = 0.0; - if (parseNmeaCoordToDecimal(latRaw, latHem, true, lat) && - parseNmeaCoordToDecimal(lonRaw, lonHem, false, lon)) { - g_gps.latitudeDeg = lat; - g_gps.longitudeDeg = lon; - g_gps.hasValidPosition = 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 bool collectGpsTraffic(uint32_t windowMs) { - uint32_t start = millis(); - bool sawBytes = false; - while ((uint32_t)(millis() - start) < windowMs) { - if (g_gpsSerial.available() > 0) { - sawBytes = true; - } - pollGpsSerial(); - g_sd.update(); - delay(2); - } - return sawBytes || g_gps.sawAnySentence; -} - -static void initialGpsProbe() { - logf("GPS startup probe at %u baud", (unsigned)GPS_BAUD); - (void)collectGpsTraffic(kGpsStartupProbeMs); - logf("GPS probe complete: nmea=%s sats_used=%u sats_view=%u utc=%s", - g_gps.sawAnySentence ? "yes" : "no", - (unsigned)g_gps.satsUsed, - (unsigned)g_gps.satsInView, - g_gps.hasValidUtc ? "yes" : "no"); -} - -static IRAM_ATTR void onPpsEdge() { - g_ppsEdgeCount++; -} - -static uint8_t bestSatelliteCount() { - return (g_gps.satsUsed > g_gps.satsInView) ? g_gps.satsUsed : g_gps.satsInView; -} - -static bool ensureGpsLogPathReady() { - if (!g_sd.isMounted()) { - g_gpsPathReady = false; - return false; - } - - if (g_gpsPathReady) { - return true; - } - - if (!g_sd.ensureDirRecursive("/gps")) { - logf("Could not create /gps directory"); - return false; - } - - // Touch the log file so a clean SD card is prepared before first discipline event. - File f = SD.open("/gps/discipline_rtc.log", FILE_APPEND); - if (!f) { - logf("Could not open /gps/discipline_rtc.log for append"); - return false; - } - f.close(); - - g_gpsPathReady = true; - return true; -} - -static bool appendDisciplineLog(const DateTime& gpsUtc, - bool havePriorRtc, - int64_t rtcMinusGpsSeconds, - uint8_t sats, - uint32_t utcAgeMs, - uint32_t ppsEdges, - char* outTs, - size_t outTsLen) { - if (!ensureGpsLogPathReady()) { - logf("SD not mounted, skipping append to gps/discipline_rtc.log"); - return false; - } - - File f = SD.open("/gps/discipline_rtc.log", FILE_APPEND); - if (!f) { - logf("Could not open /gps/discipline_rtc.log for append"); - return false; - } - - char ts[32]; - formatUtcCompact(gpsUtc, ts, sizeof(ts)); - if (outTs && outTsLen > 0) { - snprintf(outTs, outTsLen, "%s", ts); - } - - char drift[40]; - if (havePriorRtc) { - snprintf(drift, sizeof(drift), "%+lld s", (long long)rtcMinusGpsSeconds); - } else { - snprintf(drift, sizeof(drift), "RTC_unset"); - } - - char pos[64]; - if (g_gps.hasValidPosition) { - snprintf(pos, sizeof(pos), "lat=%.6f lon=%.6f", g_gps.latitudeDeg, g_gps.longitudeDeg); - } else { - snprintf(pos, sizeof(pos), "lat=NA lon=NA"); - } - - char hdop[16]; - if (g_gps.hdop > 0.0f) { - snprintf(hdop, sizeof(hdop), "%.1f", g_gps.hdop); - } else { - snprintf(hdop, sizeof(hdop), "NA"); - } - - char alt[16]; - if (g_gps.hasValidAltitude) { - snprintf(alt, sizeof(alt), "%.1f", g_gps.altitudeM); - } else { - snprintf(alt, sizeof(alt), "NA"); - } - - char line[320]; - snprintf(line, - sizeof(line), - "%s\t set RTC to GPS using 1PPS pulse-per-second discipline\t" - "rtc-gps drift=%s; sats=%u; %s; alt_m=%s; hdop=%s; utc_age_ms=%lu; pps_edges=%lu; " - "fw_epoch=%lu; fw_build_utc=%s", - ts, - drift, - (unsigned)sats, - pos, - alt, - hdop, - (unsigned long)utcAgeMs, - (unsigned long)ppsEdges, - (unsigned long)FW_BUILD_EPOCH, - FW_BUILD_UTC); - - size_t wrote = f.println(line); - f.close(); - if (wrote == 0) { - logf("Append write failed: /gps/discipline_rtc.log"); - return false; - } - return true; -} - -static bool gpsUtcIsFresh() { - if (!g_gps.hasValidUtc) { - return false; - } - return (uint32_t)(millis() - g_gps.lastUtcMs) <= 2000; -} - -static bool waitForNextPps(uint32_t timeoutMs) { - uint32_t startCount = g_ppsEdgeCount; - uint32_t startMs = millis(); - while ((uint32_t)(millis() - startMs) < timeoutMs) { - pollGpsSerial(); - g_sd.update(); - if (g_ppsEdgeCount != startCount) { - return true; - } - delay(2); - } - return false; -} - -static void waitWithUpdates(uint32_t delayMs) { - uint32_t start = millis(); - while ((uint32_t)(millis() - start) < delayMs) { - pollGpsSerial(); - g_sd.update(); - delay(10); - } -} - -static void showNoTimeAndDelay() { - uint8_t sats = bestSatelliteCount(); - char l3[24]; - snprintf(l3, sizeof(l3), "Satellites: %u", (unsigned)sats); - oledShowLines("GPS time unavailable", "RTC NOT disciplined", l3, "Retry in 30 seconds"); - logf("GPS UTC unavailable. satellites=%u. Waiting 30 seconds.", (unsigned)sats); - waitWithUpdates(kNoTimeDelayMs); -} - -static bool disciplineRtcToGps() { - if (!gpsUtcIsFresh()) { - showNoTimeAndDelay(); - return false; - } - - DateTime priorRtc{}; - bool lowV = false; - bool havePriorRtc = rtcRead(priorRtc, lowV); - if (havePriorRtc && (lowV || !isValidDateTime(priorRtc))) { - havePriorRtc = false; - } - - DateTime gpsSnap = g_gps.utc; - if (!waitForNextPps(kPpsWaitTimeoutMs)) { - oledShowLines("GPS 1PPS missing", "RTC NOT disciplined", "Retry in 30 seconds"); - logf("No 1PPS edge observed within timeout. Waiting 30 seconds."); - waitWithUpdates(kNoTimeDelayMs); - return false; - } - - DateTime target = gpsSnap; - addOneSecond(target); - - if (!rtcWrite(target)) { - oledShowLines("RTC write failed", "Could not set from GPS"); - logf("RTC write failed"); - return false; - } - - int64_t driftSec = 0; - if (havePriorRtc) { - driftSec = toEpochSeconds(priorRtc) - toEpochSeconds(target); - } - - uint8_t sats = bestSatelliteCount(); - uint32_t utcAgeMs = (uint32_t)(millis() - g_gps.lastUtcMs); - uint32_t ppsEdges = g_ppsEdgeCount; - char tsCompact[32]; - bool logOk = appendDisciplineLog(target, - havePriorRtc, - driftSec, - sats, - utcAgeMs, - ppsEdges, - tsCompact, - sizeof(tsCompact)); - - char utcLine[36]; - char driftLine[36]; - char logLine[36]; - formatUtcHuman(target, utcLine, sizeof(utcLine)); - if (havePriorRtc) { - snprintf(driftLine, sizeof(driftLine), "rtc-gps drift: %+lld s", (long long)driftSec); - } else { - snprintf(driftLine, sizeof(driftLine), "rtc-gps drift: RTC_unset"); - } - snprintf(logLine, sizeof(logLine), "Log:%s sats:%u", logOk ? "ok" : "fail", (unsigned)sats); - - oledShowLines("RTC disciplined to GPS", utcLine, driftLine, logLine, tsCompact); - - logf("RTC disciplined to GPS with 1PPS. %s drift=%+llds lowV=%s", - utcLine, - (long long)driftSec, - lowV ? "yes" : "no"); - return true; -} - -void setup() { - Serial.begin(115200); - delay(kSerialDelayMs); - - Serial.println("\r\n=================================================="); - Serial.println("Exercise 11: Set RTC to GPS with 1PPS discipline"); - 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("Exercise 11", "RTC <- GPS (1PPS)", "Booting..."); - - SdWatcherConfig sdCfg{}; - if (!g_sd.begin(sdCfg, nullptr)) { - logf("SD startup manager begin() failed"); - } - (void)ensureGpsLogPathReady(); - -#ifdef GPS_WAKEUP_PIN - pinMode(GPS_WAKEUP_PIN, INPUT); -#endif -#ifdef GPS_1PPS_PIN - pinMode(GPS_1PPS_PIN, INPUT); - attachInterrupt(digitalPinToInterrupt(GPS_1PPS_PIN), onPpsEdge, RISING); -#endif - - g_gpsSerial.setRxBufferSize(1024); - g_gpsSerial.begin(GPS_BAUD, SERIAL_8N1, GPS_RX_PIN, GPS_TX_PIN); - logf("GPS UART started: RX=%d TX=%d baud=%u", GPS_RX_PIN, GPS_TX_PIN, (unsigned)GPS_BAUD); - - oledShowLines("GPS startup probe", "Checking UTC + 1PPS"); - initialGpsProbe(); - - g_nextDisciplineMs = millis(); -} - -void loop() { - pollGpsSerial(); - g_sd.update(); - - if (g_sd.consumeMountedEvent()) { - g_gpsPathReady = false; - (void)ensureGpsLogPathReady(); - } - if (g_sd.consumeRemovedEvent()) { - g_gpsPathReady = false; - } - - uint32_t now = millis(); - if ((int32_t)(now - g_nextDisciplineMs) >= 0) { - bool ok = disciplineRtcToGps(); - g_nextDisciplineMs = now + (ok ? kLoopMsDiscipline : kNoTimeDelayMs); - } - - delay(5); -}