// 20260219 ChatGPT // Exercise 12_FiveTalk #include #include #include #include #include #include #include #include #include #include "StartupSdManager.h" #include "tbeam_supreme_adapter.h" #ifdef SX1262 #undef SX1262 #endif #ifndef NODE_LABEL #define NODE_LABEL "UNNAMED" #endif #ifndef NODE_SHORT #define NODE_SHORT "?" #endif #ifndef NODE_SLOT_INDEX #define NODE_SLOT_INDEX 0 #endif #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 #if (NODE_SLOT_INDEX < 0) || (NODE_SLOT_INDEX > 4) #error "NODE_SLOT_INDEX must be 0..4" #endif static const uint32_t kSerialDelayMs = 1000; static const uint32_t kDisciplineMaxAgeSec = 24UL * 60UL * 60UL; static const uint32_t kDisciplineRetryMs = 5000; static const uint32_t kPpsWaitTimeoutMs = 1500; static const uint32_t kSdMessagePeriodMs = 1200; static const uint32_t kNoGpsMessagePeriodMs = 1500; static const uint32_t kHealthCheckPeriodMs = 60000; static const uint32_t kSlotSeconds = 2; static XPowersLibInterface* g_pmu = nullptr; static StartupSdManager g_sd(Serial); static U8G2_SH1106_128X64_NONAME_F_HW_I2C g_oled(U8G2_R0, U8X8_PIN_NONE); static HardwareSerial g_gpsSerial(1); static SX1262 g_radio = new Module(LORA_CS, LORA_DIO1, LORA_RESET, LORA_BUSY); static volatile bool g_rxFlag = false; static volatile uint32_t g_ppsEdgeCount = 0; static uint32_t g_logSeq = 0; static uint32_t g_lastWarnMs = 0; static uint32_t g_lastDisciplineTryMs = 0; static uint32_t g_lastHealthCheckMs = 0; static int64_t g_lastDisciplineEpoch = -1; static int64_t g_lastTxEpochSecond = -1; static uint32_t g_txCount = 0; static bool g_radioReady = false; static bool g_sessionReady = false; static bool g_gpsPathReady = false; static char g_sessionStamp[20] = {0}; static char g_sentPath[64] = {0}; static char g_recvPath[64] = {0}; static File g_sentFile; static File g_recvFile; static char g_gpsLine[128]; static size_t g_gpsLineLen = 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; uint8_t satsUsed = 0; uint8_t satsInView = 0; uint32_t lastUtcMs = 0; DateTime utc{}; }; static GpsState g_gps; enum class AppPhase : uint8_t { WAIT_SD = 0, WAIT_DISCIPLINE, RUN }; static AppPhase g_phase = AppPhase::WAIT_SD; static uint8_t bestSatelliteCount() { return (g_gps.satsUsed > g_gps.satsInView) ? g_gps.satsUsed : g_gps.satsInView; } static void logf(const char* fmt, ...) { char msg[256]; 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 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 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 getCurrentUtc(DateTime& dt, int64_t& epoch) { bool lowV = false; if (!rtcRead(dt, lowV)) return false; if (lowV || !isValidDateTime(dt)) return false; epoch = toEpochSeconds(dt); return true; } 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 void formatUtcCompact(const DateTime& dt, char* out, size_t outLen) { snprintf(out, outLen, "%04u%02u%02u_%02u%02u%02u", (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 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 || !date || strlen(utc) < 6 || 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(); } 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 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, "RMC") == 0) parseRmc(fields, count); else if (strcmp(type, "GGA") == 0) parseGga(fields, count); else if (strcmp(type, "GSV") == 0) parseGsv(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 gpsUtcIsFresh() { return g_gps.hasValidUtc && ((uint32_t)(millis() - g_gps.lastUtcMs) <= 2000U); } static IRAM_ATTR void onPpsEdge() { g_ppsEdgeCount++; } static bool waitForNextPps(uint32_t timeoutMs) { uint32_t startEdges = g_ppsEdgeCount; uint32_t startMs = millis(); while ((uint32_t)(millis() - startMs) < timeoutMs) { pollGpsSerial(); g_sd.update(); if (g_ppsEdgeCount != startEdges) return true; delay(2); } return false; } static bool ensureGpsLogPathReady() { if (!g_sd.isMounted()) { g_gpsPathReady = false; return false; } if (g_gpsPathReady) return true; if (!g_sd.ensureDirRecursive("/gps")) return false; File f = SD.open("/gps/discipline_rtc.log", FILE_APPEND); if (!f) return false; f.close(); g_gpsPathReady = true; return true; } static bool appendDisciplineLog(const DateTime& gpsUtc, int64_t rtcMinusGpsSeconds, bool hadPriorRtc) { if (!ensureGpsLogPathReady()) return false; File f = SD.open("/gps/discipline_rtc.log", FILE_APPEND); if (!f) return false; char ts[24]; snprintf(ts, sizeof(ts), "%04u%02u%02u_%02u%02u%02u_z", (unsigned)gpsUtc.year, (unsigned)gpsUtc.month, (unsigned)gpsUtc.day, (unsigned)gpsUtc.hour, (unsigned)gpsUtc.minute, (unsigned)gpsUtc.second); char line[256]; if (hadPriorRtc) { snprintf(line, sizeof(line), "%s\t set RTC to GPS for FiveTalk\trtc-gps drift=%+lld s; sats=%u; fw_build_utc=%s", ts, (long long)rtcMinusGpsSeconds, (unsigned)bestSatelliteCount(), FW_BUILD_UTC); } else { snprintf(line, sizeof(line), "%s\t set RTC to GPS for FiveTalk\trtc-gps drift=RTC_unset; sats=%u; fw_build_utc=%s", ts, (unsigned)bestSatelliteCount(), FW_BUILD_UTC); } size_t wrote = f.println(line); f.close(); return wrote > 0; } static bool disciplineRtcToGps() { if (!gpsUtcIsFresh()) return false; DateTime prior{}; bool lowV = false; bool havePriorRtc = rtcRead(prior, lowV) && !lowV && isValidDateTime(prior); DateTime gpsSnap = g_gps.utc; if (!waitForNextPps(kPpsWaitTimeoutMs)) return false; int64_t snapEpoch = toEpochSeconds(gpsSnap); DateTime target{}; if (!fromEpochSeconds(snapEpoch + 1, target)) return false; if (!rtcWrite(target)) return false; int64_t driftSec = 0; if (havePriorRtc) driftSec = toEpochSeconds(prior) - toEpochSeconds(target); if (!appendDisciplineLog(target, driftSec, havePriorRtc)) { logf("WARN: Failed to append /gps/discipline_rtc.log"); } g_lastDisciplineEpoch = toEpochSeconds(target); char human[32]; formatUtcHuman(target, human, sizeof(human)); logf("RTC disciplined to GPS (%s), sats=%u", human, (unsigned)bestSatelliteCount()); return true; } static bool parseLogTimestampToken(const char* token, int64_t& epochOut) { if (!token) return false; unsigned y = 0, m = 0, d = 0, hh = 0, mm = 0, ss = 0; if (sscanf(token, "%4u%2u%2u_%2u%2u%2u", &y, &m, &d, &hh, &mm, &ss) != 6) return false; DateTime dt{}; dt.year = (uint16_t)y; dt.month = (uint8_t)m; dt.day = (uint8_t)d; dt.hour = (uint8_t)hh; dt.minute = (uint8_t)mm; dt.second = (uint8_t)ss; if (!isValidDateTime(dt)) return false; epochOut = toEpochSeconds(dt); return true; } static bool loadLastDisciplineEpoch(int64_t& epochOut) { epochOut = -1; if (!g_sd.isMounted()) return false; if (!SD.exists("/gps/discipline_rtc.log")) return false; File f = SD.open("/gps/discipline_rtc.log", FILE_READ); if (!f) return false; while (f.available()) { String line = f.readStringUntil('\n'); line.trim(); if (line.length() == 0) continue; int sep = line.indexOf('\t'); String token = (sep >= 0) ? line.substring(0, sep) : line; char buf[32]; size_t n = token.length(); if (n >= sizeof(buf)) n = sizeof(buf) - 1; memcpy(buf, token.c_str(), n); buf[n] = '\0'; int64_t parsed = -1; if (parseLogTimestampToken(buf, parsed)) epochOut = parsed; } f.close(); return epochOut >= 0; } static bool isDisciplineStale() { DateTime now{}; int64_t nowEpoch = 0; if (!getCurrentUtc(now, nowEpoch)) return true; int64_t lastEpoch = -1; if (!loadLastDisciplineEpoch(lastEpoch)) { if (g_lastDisciplineEpoch < 0) return true; lastEpoch = g_lastDisciplineEpoch; } g_lastDisciplineEpoch = lastEpoch; if (lastEpoch < 0) return true; int64_t age = nowEpoch - lastEpoch; return age < 0 || age > (int64_t)kDisciplineMaxAgeSec; } static void readBattery(float& voltageV, bool& present) { voltageV = -1.0f; present = false; if (!g_pmu) return; present = g_pmu->isBatteryConnect(); voltageV = g_pmu->getBattVoltage() / 1000.0f; } static void closeSessionLogs() { if (g_sentFile) g_sentFile.close(); if (g_recvFile) g_recvFile.close(); g_sessionReady = false; } static bool openSessionLogs() { closeSessionLogs(); DateTime now{}; int64_t nowEpoch = 0; if (!getCurrentUtc(now, nowEpoch)) { logf("Cannot open session logs: RTC unavailable"); return false; } formatUtcCompact(now, g_sessionStamp, sizeof(g_sessionStamp)); snprintf(g_sentPath, sizeof(g_sentPath), "/%s_sent_%s.log", NODE_SHORT, g_sessionStamp); snprintf(g_recvPath, sizeof(g_recvPath), "/%s_received_%s.log", NODE_SHORT, g_sessionStamp); g_sentFile = SD.open(g_sentPath, FILE_APPEND); g_recvFile = SD.open(g_recvPath, FILE_APPEND); if (!g_sentFile || !g_recvFile) { logf("Failed to open session logs: %s | %s", g_sentPath, g_recvPath); closeSessionLogs(); return false; } char human[32]; formatUtcHuman(now, human, sizeof(human)); g_sentFile.printf("# session_start_epoch=%lld utc=%s node=%s (%s) fw_build=%s\n", (long long)nowEpoch, human, NODE_SHORT, NODE_LABEL, FW_BUILD_UTC); g_recvFile.printf("# session_start_epoch=%lld utc=%s node=%s (%s) fw_build=%s\n", (long long)nowEpoch, human, NODE_SHORT, NODE_LABEL, FW_BUILD_UTC); g_sentFile.flush(); g_recvFile.flush(); logf("Session logs ready: %s | %s", g_sentPath, g_recvPath); g_sessionReady = true; return true; } static void writeSentLog(int64_t epoch, const DateTime& dt) { if (!g_sessionReady || !g_sentFile) return; float battV = -1.0f; bool battPresent = false; readBattery(battV, battPresent); char human[32]; formatUtcHuman(dt, human, sizeof(human)); g_sentFile.printf("epoch=%lld\tutc=%s\tunit=%s\tmsg=%s\ttx_count=%lu\tbatt_present=%u\tbatt_v=%.3f\n", (long long)epoch, human, NODE_SHORT, NODE_SHORT, (unsigned long)g_txCount, battPresent ? 1U : 0U, battV); g_sentFile.flush(); } static void writeRecvLog(int64_t epoch, const DateTime& dt, const char* msg, float rssi, float snr) { if (!g_sessionReady || !g_recvFile) return; float battV = -1.0f; bool battPresent = false; readBattery(battV, battPresent); char human[32]; formatUtcHuman(dt, human, sizeof(human)); g_recvFile.printf("epoch=%lld\tutc=%s\tunit=%s\trx_msg=%s\trssi=%.1f\tsnr=%.1f\tbatt_present=%u\tbatt_v=%.3f\n", (long long)epoch, human, NODE_SHORT, msg ? msg : "", rssi, snr, battPresent ? 1U : 0U, battV); g_recvFile.flush(); } static void onLoRaDio1Rise() { g_rxFlag = true; } static bool initRadio() { SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS); int state = g_radio.begin(915.0, 125.0, 7, 5, 0x12, 14); if (state != RADIOLIB_ERR_NONE) { logf("radio.begin failed code=%d", state); return false; } g_radio.setDio1Action(onLoRaDio1Rise); state = g_radio.startReceive(); if (state != RADIOLIB_ERR_NONE) { logf("radio.startReceive failed code=%d", state); return false; } logf("Radio ready for %s (%s), slot=%d sec=%d", NODE_LABEL, NODE_SHORT, NODE_SLOT_INDEX, NODE_SLOT_INDEX * 2); return true; } static void showRxOnOled(const DateTime& dt, const char* msg) { char hhmmss[16]; snprintf(hhmmss, sizeof(hhmmss), "%02u:%02u:%02u", (unsigned)dt.hour, (unsigned)dt.minute, (unsigned)dt.second); char line[32]; snprintf(line, sizeof(line), "[%s, %s]", msg ? msg : "", NODE_SHORT); oledShowLines(hhmmss, line); } static void runTxScheduler() { DateTime now{}; int64_t epoch = 0; if (!getCurrentUtc(now, epoch)) return; int slotSecond = NODE_SLOT_INDEX * (int)kSlotSeconds; int secInFrame = now.second % 10; if (secInFrame != slotSecond) return; int64_t epochSecond = epoch; if (epochSecond == g_lastTxEpochSecond) return; g_lastTxEpochSecond = epochSecond; g_rxFlag = false; g_radio.clearDio1Action(); int tx = g_radio.transmit(NODE_SHORT); if (tx == RADIOLIB_ERR_NONE) { g_txCount++; writeSentLog(epoch, now); logf("TX %s count=%lu", NODE_SHORT, (unsigned long)g_txCount); } else { logf("TX failed code=%d", tx); } g_rxFlag = false; g_radio.setDio1Action(onLoRaDio1Rise); g_radio.startReceive(); } static void runRxHandler() { if (!g_rxFlag) return; g_rxFlag = false; String rx; int rc = g_radio.readData(rx); if (rc != RADIOLIB_ERR_NONE) { g_radio.startReceive(); return; } DateTime now{}; int64_t epoch = 0; if (getCurrentUtc(now, epoch)) { writeRecvLog(epoch, now, rx.c_str(), g_radio.getRSSI(), g_radio.getSNR()); showRxOnOled(now, rx.c_str()); } g_radio.startReceive(); } static void enterWaitSdState() { if (g_phase == AppPhase::WAIT_SD) return; g_phase = AppPhase::WAIT_SD; closeSessionLogs(); logf("State -> WAIT_SD"); } static void enterWaitDisciplineState() { if (g_phase == AppPhase::WAIT_DISCIPLINE) return; g_phase = AppPhase::WAIT_DISCIPLINE; closeSessionLogs(); logf("State -> WAIT_DISCIPLINE"); } static void enterRunState() { if (g_phase == AppPhase::RUN) return; if (!openSessionLogs()) return; g_lastTxEpochSecond = -1; g_lastHealthCheckMs = millis(); g_phase = AppPhase::RUN; logf("State -> RUN"); } static void updateWaitSd() { if (g_sd.isMounted()) { g_lastWarnMs = 0; g_gpsPathReady = false; enterWaitDisciplineState(); return; } uint32_t now = millis(); if ((uint32_t)(now - g_lastWarnMs) >= kSdMessagePeriodMs) { g_lastWarnMs = now; oledShowLines("Reinsert SD Card", NODE_SHORT, NODE_LABEL); } } static void updateWaitDiscipline() { if (!g_sd.isMounted()) { enterWaitSdState(); return; } if (!isDisciplineStale()) { enterRunState(); return; } uint32_t now = millis(); if ((uint32_t)(now - g_lastWarnMs) >= kNoGpsMessagePeriodMs) { g_lastWarnMs = now; char satsLine[24]; snprintf(satsLine, sizeof(satsLine), "Satellites: %u", (unsigned)bestSatelliteCount()); oledShowLines("Take me outside", "Need GPS time sync", satsLine); } if ((uint32_t)(now - g_lastDisciplineTryMs) < kDisciplineRetryMs) return; g_lastDisciplineTryMs = now; if (disciplineRtcToGps()) { g_lastWarnMs = 0; enterRunState(); } } static void updateRun() { if (!g_sd.isMounted()) { enterWaitSdState(); return; } uint32_t now = millis(); if ((uint32_t)(now - g_lastHealthCheckMs) >= kHealthCheckPeriodMs) { g_lastHealthCheckMs = now; if (isDisciplineStale()) { enterWaitDisciplineState(); return; } } runTxScheduler(); runRxHandler(); } void setup() { Serial.begin(115200); delay(kSerialDelayMs); Serial.println("\r\n=================================================="); Serial.println("Exercise 12: FiveTalk"); Serial.println("=================================================="); if (!tbeam_supreme::initPmuForPeripherals(g_pmu, &Serial)) { logf("WARN: PMU init failed"); } Wire.begin(OLED_SDA, OLED_SCL); g_oled.setI2CAddress(OLED_ADDR << 1); g_oled.begin(); oledShowLines("Exercise 12", "FiveTalk startup", NODE_SHORT, NODE_LABEL); SdWatcherConfig sdCfg{}; if (!g_sd.begin(sdCfg, nullptr)) { logf("WARN: SD watcher begin failed"); } #ifdef GPS_1PPS_PIN pinMode(GPS_1PPS_PIN, INPUT); attachInterrupt(digitalPinToInterrupt(GPS_1PPS_PIN), onPpsEdge, RISING); #endif #ifdef GPS_WAKEUP_PIN pinMode(GPS_WAKEUP_PIN, INPUT); #endif g_gpsSerial.setRxBufferSize(1024); g_gpsSerial.begin(GPS_BAUD, SERIAL_8N1, GPS_RX_PIN, GPS_TX_PIN); g_radioReady = initRadio(); if (!g_radioReady) { oledShowLines("LoRa init failed", "Check radio pins"); } g_phase = g_sd.isMounted() ? AppPhase::WAIT_DISCIPLINE : AppPhase::WAIT_SD; } void loop() { pollGpsSerial(); g_sd.update(); if (g_sd.consumeMountedEvent()) { logf("SD mounted"); g_gpsPathReady = false; } if (g_sd.consumeRemovedEvent()) { logf("SD removed"); g_gpsPathReady = false; } if (!g_radioReady) { delay(50); return; } switch (g_phase) { case AppPhase::WAIT_SD: updateWaitSd(); break; case AppPhase::WAIT_DISCIPLINE: updateWaitDiscipline(); break; case AppPhase::RUN: updateRun(); break; } delay(5); }