From 82db6c9a3898435d556cc9be2c346bf474e09a05 Mon Sep 17 00:00:00 2001 From: John Poole Date: Fri, 20 Feb 2026 17:41:51 -0800 Subject: [PATCH] each transmit attempt now has its own monotonic tx_id, independent from g_txCount (success counter). --- exercises/12_FiveTalk/src/main.cpp | 744 +++++++++++++++++++---------- 1 file changed, 499 insertions(+), 245 deletions(-) diff --git a/exercises/12_FiveTalk/src/main.cpp b/exercises/12_FiveTalk/src/main.cpp index 3b1d749..4a77047 100644 --- a/exercises/12_FiveTalk/src/main.cpp +++ b/exercises/12_FiveTalk/src/main.cpp @@ -76,7 +76,7 @@ static const uint32_t kNoGpsMessagePeriodMs = 1500; static const uint32_t kHealthCheckPeriodMs = 60000; static const uint32_t kSlotSeconds = 2; -static XPowersLibInterface* g_pmu = nullptr; +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); @@ -93,6 +93,7 @@ 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 uint32_t g_txAttemptId = 0; static bool g_radioReady = false; static bool g_sessionReady = false; @@ -108,7 +109,8 @@ static File g_recvFile; static char g_gpsLine[128]; static size_t g_gpsLineLen = 0; -struct DateTime { +struct DateTime +{ uint16_t year; uint8_t month; uint8_t day; @@ -117,7 +119,8 @@ struct DateTime { uint8_t second; }; -struct GpsState { +struct GpsState +{ bool sawAnySentence = false; bool hasValidUtc = false; bool hasValidPosition = false; @@ -133,9 +136,14 @@ struct GpsState { static GpsState g_gps; -static void parsePayloadCoords(const char* msg, char* latOut, size_t latLen, char* lonOut, size_t lonLen, char* altOut, size_t altLen); +static void parsePayloadFields(const char *msg, + char *txIdOut, size_t txIdLen, + char *latOut, size_t latLen, + char *lonOut, size_t lonLen, + char *altOut, size_t altLen); -enum class AppPhase : uint8_t { +enum class AppPhase : uint8_t +{ WAIT_SD = 0, WAIT_DISCIPLINE, RUN @@ -143,11 +151,13 @@ enum class AppPhase : uint8_t { static AppPhase g_phase = AppPhase::WAIT_SD; -static uint8_t bestSatelliteCount() { +static uint8_t bestSatelliteCount() +{ return (g_gps.satsUsed > g_gps.satsInView) ? g_gps.satsUsed : g_gps.satsInView; } -static void logf(const char* fmt, ...) { +static void logf(const char *fmt, ...) +{ char msg[256]; va_list args; va_start(args, fmt); @@ -156,49 +166,67 @@ static void logf(const char* fmt, ...) { 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) { +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); + 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) { +static uint8_t toBcd(uint8_t v) +{ return (uint8_t)(((v / 10U) << 4U) | (v % 10U)); } -static uint8_t fromBcd(uint8_t b) { +static uint8_t fromBcd(uint8_t b) +{ return (uint8_t)(((b >> 4U) * 10U) + (b & 0x0FU)); } -static bool isLeapYear(uint16_t y) { +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 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]; + 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; +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) { +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); @@ -207,17 +235,21 @@ static int64_t daysFromCivil(int y, unsigned m, unsigned d) { return era * 146097 + (int)doe - 719468; } -static int64_t toEpochSeconds(const DateTime& dt) { +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; +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) { + if (rem < 0) + { rem += 86400LL; days -= 1; } @@ -244,14 +276,17 @@ static bool fromEpochSeconds(int64_t sec, DateTime& out) { return isValidDateTime(out); } -static bool rtcRead(DateTime& out, bool& lowVoltageFlag) { +static bool rtcRead(DateTime &out, bool &lowVoltageFlag) +{ Wire1.beginTransmission(RTC_I2C_ADDR); Wire1.write(0x02); - if (Wire1.endTransmission(false) != 0) return false; + 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; + if (got != need) + return false; uint8_t sec = Wire1.read(); uint8_t min = Wire1.read(); @@ -274,7 +309,8 @@ static bool rtcRead(DateTime& out, bool& lowVoltageFlag) { return true; } -static bool rtcWrite(const DateTime& dt) { +static bool rtcWrite(const DateTime &dt) +{ Wire1.beginTransmission(RTC_I2C_ADDR); Wire1.write(0x02); Wire1.write(toBcd(dt.second & 0x7FU)); @@ -284,22 +320,27 @@ static bool rtcWrite(const DateTime& dt) { Wire1.write(0x00); uint8_t monthReg = toBcd(dt.month); - if (dt.year < 2000U) monthReg |= 0x80U; + 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) { +static bool getCurrentUtc(DateTime &dt, int64_t &epoch) +{ bool lowV = false; - if (!rtcRead(dt, lowV)) return false; - if (lowV || !isValidDateTime(dt)) return 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) { +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, @@ -309,7 +350,8 @@ static void formatUtcHuman(const DateTime& dt, char* out, size_t outLen) { (unsigned)dt.second); } -static void formatUtcCompact(const DateTime& dt, char* out, size_t outLen) { +static void formatUtcCompact(const DateTime &dt, char *out, size_t outLen) +{ snprintf(out, outLen, "%04u%02u%02u_%02u%02u%02u", @@ -321,36 +363,47 @@ static void formatUtcCompact(const DateTime& dt, char* out, size_t outLen) { (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; +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; +static bool parseNmeaCoordToDecimal(const char *raw, const char *hemi, bool isLat, double &outDeg) +{ + if (!raw || !hemi || raw[0] == '\0' || hemi[0] == '\0') + return false; int degDigits = isLat ? 2 : 3; size_t n = strlen(raw); - if (n <= (size_t)degDigits + 2) return false; + if (n <= (size_t)degDigits + 2) + return false; - for (int i = 0; i < degDigits; ++i) { - if (!isdigit((unsigned char)raw[i])) 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; + const char *minPtr = raw + degDigits; double minutes = atof(minPtr); - if (minutes < 0.0 || minutes >= 60.0) return false; + 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') { + if (h == 'S' || h == 'W') + { dec = -dec; - } else if (h != 'N' && h != 'E') { + } + else if (h != 'N' && h != 'E') + { return false; } @@ -358,24 +411,30 @@ static bool parseNmeaCoordToDecimal(const char* raw, const char* hemi, bool isLa return true; } -static void parseRmc(char* fields[], int count) { - if (count <= 9) return; +static void parseRmc(char *fields[], int count) +{ + if (count <= 9) + return; - const char* utc = fields[1]; - const char* status = fields[2]; - const char* latRaw = (count > 3) ? fields[3] : nullptr; - const char* latHem = (count > 4) ? fields[4] : nullptr; - const char* lonRaw = (count > 5) ? fields[5] : nullptr; - const char* lonHem = (count > 6) ? fields[6] : nullptr; - const char* date = fields[9]; + const char *utc = fields[1]; + const char *status = fields[2]; + const char *latRaw = (count > 3) ? fields[3] : nullptr; + const char *latHem = (count > 4) ? fields[4] : nullptr; + const char *lonRaw = (count > 5) ? fields[5] : nullptr; + const char *lonHem = (count > 6) ? fields[6] : nullptr; + const char *date = fields[9]; - if (!status || status[0] != 'A') return; - if (!utc || !date || strlen(utc) < 6 || strlen(date) < 6) return; + 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; + 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; @@ -389,23 +448,28 @@ static void parseRmc(char* fields[], int count) { double lat = 0.0; double lon = 0.0; if (parseNmeaCoordToDecimal(latRaw, latHem, true, lat) && - parseNmeaCoordToDecimal(lonRaw, lonHem, false, lon)) { + parseNmeaCoordToDecimal(lonRaw, lonHem, false, lon)) + { g_gps.latitudeDeg = lat; g_gps.longitudeDeg = lon; g_gps.hasValidPosition = 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; +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 (sats >= 0 && sats <= 255) + g_gps.satsUsed = (uint8_t)sats; - if (count > 9 && fields[9] && fields[9][0] != '\0') { + if (count > 9 && fields[9] && fields[9][0] != '\0') + { g_gps.altitudeM = (float)atof(fields[9]); g_gps.hasValidAltitude = true; } @@ -413,52 +477,70 @@ static void parseGga(char* fields[], int count) { double lat = 0.0; double lon = 0.0; if (parseNmeaCoordToDecimal(latRaw, latHem, true, lat) && - parseNmeaCoordToDecimal(lonRaw, lonHem, false, lon)) { + 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; +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; + if (sats >= 0 && sats <= 255) + g_gps.satsInView = (uint8_t)sats; } -static void processNmeaLine(char* line) { - if (!line || line[0] != '$') return; +static void processNmeaLine(char *line) +{ + if (!line || line[0] != '$') + return; g_gps.sawAnySentence = true; - char* star = strchr(line, '*'); - if (star) *star = '\0'; + char *star = strchr(line, '*'); + if (star) + *star = '\0'; - char* fields[24] = {0}; + char *fields[24] = {0}; int count = 0; - char* saveptr = nullptr; - char* tok = strtok_r(line, ",", &saveptr); - while (tok && count < 24) { + 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; + if (count == 0 || !fields[0]) + return; - const char* header = fields[0]; + const char *header = fields[0]; size_t n = strlen(header); - if (n < 6) return; + 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); + 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) { +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) { + if (c == '\r') + continue; + if (c == '\n') + { + if (g_gpsLineLen > 0) + { g_gpsLine[g_gpsLineLen] = '\0'; processNmeaLine(g_gpsLine); g_gpsLineLen = 0; @@ -466,51 +548,66 @@ static void pollGpsSerial() { continue; } - if (g_gpsLineLen + 1 < sizeof(g_gpsLine)) g_gpsLine[g_gpsLineLen++] = c; - else g_gpsLineLen = 0; + if (g_gpsLineLen + 1 < sizeof(g_gpsLine)) + g_gpsLine[g_gpsLineLen++] = c; + else + g_gpsLineLen = 0; } } -static bool gpsUtcIsFresh() { +static bool gpsUtcIsFresh() +{ return g_gps.hasValidUtc && ((uint32_t)(millis() - g_gps.lastUtcMs) <= 2000U); } -static IRAM_ATTR void onPpsEdge() { +static IRAM_ATTR void onPpsEdge() +{ g_ppsEdgeCount++; } -static bool waitForNextPps(uint32_t timeoutMs) { +static bool waitForNextPps(uint32_t timeoutMs) +{ uint32_t startEdges = g_ppsEdgeCount; uint32_t startMs = millis(); - while ((uint32_t)(millis() - startMs) < timeoutMs) { + while ((uint32_t)(millis() - startMs) < timeoutMs) + { pollGpsSerial(); g_sd.update(); - if (g_ppsEdgeCount != startEdges) return true; + if (g_ppsEdgeCount != startEdges) + return true; delay(2); } return false; } -static bool ensureGpsLogPathReady() { - if (!g_sd.isMounted()) { +static bool ensureGpsLogPathReady() +{ + if (!g_sd.isMounted()) + { g_gpsPathReady = false; return false; } - if (g_gpsPathReady) return true; + if (g_gpsPathReady) + return true; - if (!g_sd.ensureDirRecursive("/gps")) return false; + if (!g_sd.ensureDirRecursive("/gps")) + return false; File f = SD.open("/gps/discipline_rtc.log", FILE_APPEND); - if (!f) return false; + 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; +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; + if (!f) + return false; char ts[24]; snprintf(ts, @@ -524,7 +621,8 @@ static bool appendDisciplineLog(const DateTime& gpsUtc, int64_t rtcMinusGpsSecon (unsigned)gpsUtc.second); char line[256]; - if (hadPriorRtc) { + 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", @@ -532,7 +630,9 @@ static bool appendDisciplineLog(const DateTime& gpsUtc, int64_t rtcMinusGpsSecon (long long)rtcMinusGpsSeconds, (unsigned)bestSatelliteCount(), FW_BUILD_UTC); - } else { + } + else + { snprintf(line, sizeof(line), "%s\t set RTC to GPS for FiveTalk\trtc-gps drift=RTC_unset; sats=%u; fw_build_utc=%s", @@ -546,25 +646,32 @@ static bool appendDisciplineLog(const DateTime& gpsUtc, int64_t rtcMinusGpsSecon return wrote > 0; } -static bool disciplineRtcToGps() { - if (!gpsUtcIsFresh()) return false; +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; + if (!waitForNextPps(kPpsWaitTimeoutMs)) + return false; int64_t snapEpoch = toEpochSeconds(gpsSnap); DateTime target{}; - if (!fromEpochSeconds(snapEpoch + 1, target)) return false; - if (!rtcWrite(target)) return false; + if (!fromEpochSeconds(snapEpoch + 1, target)) + return false; + if (!rtcWrite(target)) + return false; int64_t driftSec = 0; - if (havePriorRtc) driftSec = toEpochSeconds(prior) - toEpochSeconds(target); + if (havePriorRtc) + driftSec = toEpochSeconds(prior) - toEpochSeconds(target); - if (!appendDisciplineLog(target, driftSec, havePriorRtc)) { + if (!appendDisciplineLog(target, driftSec, havePriorRtc)) + { logf("WARN: Failed to append /gps/discipline_rtc.log"); } @@ -575,11 +682,14 @@ static bool disciplineRtcToGps() { return true; } -static bool parseLogTimestampToken(const char* token, int64_t& epochOut) { - if (!token) return false; +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; + 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; @@ -588,81 +698,102 @@ static bool parseLogTimestampToken(const char* token, int64_t& epochOut) { dt.hour = (uint8_t)hh; dt.minute = (uint8_t)mm; dt.second = (uint8_t)ss; - if (!isValidDateTime(dt)) return false; + if (!isValidDateTime(dt)) + return false; epochOut = toEpochSeconds(dt); return true; } -static bool loadLastDisciplineEpoch(int64_t& epochOut) { +static bool loadLastDisciplineEpoch(int64_t &epochOut) +{ epochOut = -1; - if (!g_sd.isMounted()) return false; - if (!SD.exists("/gps/discipline_rtc.log")) return false; + 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; + if (!f) + return false; - while (f.available()) { + while (f.available()) + { String line = f.readStringUntil('\n'); line.trim(); - if (line.length() == 0) continue; + 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; + 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; + if (parseLogTimestampToken(buf, parsed)) + epochOut = parsed; } f.close(); return epochOut >= 0; } -static bool isDisciplineStale() { +static bool isDisciplineStale() +{ DateTime now{}; int64_t nowEpoch = 0; - if (!getCurrentUtc(now, nowEpoch)) return true; + if (!getCurrentUtc(now, nowEpoch)) + return true; int64_t lastEpoch = -1; - if (!loadLastDisciplineEpoch(lastEpoch)) { - if (g_lastDisciplineEpoch < 0) return true; + if (!loadLastDisciplineEpoch(lastEpoch)) + { + if (g_lastDisciplineEpoch < 0) + return true; lastEpoch = g_lastDisciplineEpoch; } g_lastDisciplineEpoch = lastEpoch; - if (lastEpoch < 0) return true; + if (lastEpoch < 0) + return true; int64_t age = nowEpoch - lastEpoch; return age < 0 || age > (int64_t)kDisciplineMaxAgeSec; } -static void readBattery(float& voltageV, bool& present) { +static void readBattery(float &voltageV, bool &present) +{ voltageV = -1.0f; present = false; - if (!g_pmu) return; + 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(); +static void closeSessionLogs() +{ + if (g_sentFile) + g_sentFile.close(); + if (g_recvFile) + g_recvFile.close(); g_sessionReady = false; } -static bool openSessionLogs() { +static bool openSessionLogs() +{ closeSessionLogs(); DateTime now{}; int64_t nowEpoch = 0; - if (!getCurrentUtc(now, nowEpoch)) { + if (!getCurrentUtc(now, nowEpoch)) + { logf("Cannot open session logs: RTC unavailable"); return false; } @@ -673,7 +804,8 @@ static bool openSessionLogs() { g_sentFile = SD.open(g_sentPath, FILE_APPEND); g_recvFile = SD.open(g_recvPath, FILE_APPEND); - if (!g_sentFile || !g_recvFile) { + if (!g_sentFile || !g_recvFile) + { logf("Failed to open session logs: %s | %s", g_sentPath, g_recvPath); closeSessionLogs(); return false; @@ -701,22 +833,33 @@ static bool openSessionLogs() { return true; } -static void gpsFieldStrings(char* latOut, size_t latLen, char* lonOut, size_t lonLen, char* altOut, size_t altLen) { - if (latOut && latLen > 0) latOut[0] = '\0'; - if (lonOut && lonLen > 0) lonOut[0] = '\0'; - if (altOut && altLen > 0) altOut[0] = '\0'; +static void gpsFieldStrings(char *latOut, size_t latLen, char *lonOut, size_t lonLen, char *altOut, size_t altLen) +{ + if (latOut && latLen > 0) + latOut[0] = '\0'; + if (lonOut && lonLen > 0) + lonOut[0] = '\0'; + if (altOut && altLen > 0) + altOut[0] = '\0'; - if (g_gps.hasValidPosition) { - if (latOut && latLen > 0) snprintf(latOut, latLen, "%.6f", g_gps.latitudeDeg); - if (lonOut && lonLen > 0) snprintf(lonOut, lonLen, "%.6f", g_gps.longitudeDeg); + if (g_gps.hasValidPosition) + { + if (latOut && latLen > 0) + snprintf(latOut, latLen, "%.6f", g_gps.latitudeDeg); + if (lonOut && lonLen > 0) + snprintf(lonOut, lonLen, "%.6f", g_gps.longitudeDeg); } - if (g_gps.hasValidAltitude) { - if (altOut && altLen > 0) snprintf(altOut, altLen, "%.2f", g_gps.altitudeM); + if (g_gps.hasValidAltitude) + { + if (altOut && altLen > 0) + snprintf(altOut, altLen, "%.2f", g_gps.altitudeM); } } -static void writeSentLog(int64_t epoch, const DateTime& dt) { - if (!g_sessionReady || !g_sentFile) return; +static void writeSentLog(int64_t epoch, const DateTime &dt, uint32_t txId, const char *payload, bool txOk) +{ + if (!g_sessionReady || !g_sentFile) + return; float battV = -1.0f; bool battPresent = false; @@ -728,11 +871,13 @@ static void writeSentLog(int64_t epoch, const DateTime& dt) { char lat[24], lon[24], alt[24]; gpsFieldStrings(lat, sizeof(lat), lon, sizeof(lon), alt, sizeof(alt)); - g_sentFile.printf("epoch=%lld\tutc=%s\tunit=%s\tmsg=%s\tlat=%s\tlon=%s\talt_m=%s\ttx_count=%lu\tbatt_present=%u\tbatt_v=%.3f\n", + g_sentFile.printf("epoch=%lld\tutc=%s\tunit=%s\tmsg=%s\ttx_id=%lu\ttx_ok=%u\tlat=%s\tlon=%s\talt_m=%s\ttx_count=%lu\tbatt_present=%u\tbatt_v=%.3f\n", (long long)epoch, human, NODE_SHORT, - NODE_SHORT, + payload ? payload : "", + (unsigned long)txId, + txOk ? 1U : 0U, lat, lon, alt, @@ -742,8 +887,10 @@ static void writeSentLog(int64_t epoch, const DateTime& dt) { 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; +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; @@ -752,14 +899,15 @@ static void writeRecvLog(int64_t epoch, const DateTime& dt, const char* msg, flo char human[32]; formatUtcHuman(dt, human, sizeof(human)); - char lat[24], lon[24], alt[24]; - parsePayloadCoords(msg, lat, sizeof(lat), lon, sizeof(lon), alt, sizeof(alt)); + char txId[24], lat[24], lon[24], alt[24]; + parsePayloadFields(msg, txId, sizeof(txId), lat, sizeof(lat), lon, sizeof(lon), alt, sizeof(alt)); - g_recvFile.printf("epoch=%lld\tutc=%s\tunit=%s\trx_msg=%s\trx_lat=%s\trx_lon=%s\trx_alt_m=%s\trssi=%.1f\tsnr=%.1f\tbatt_present=%u\tbatt_v=%.3f\n", + g_recvFile.printf("epoch=%lld\tutc=%s\tunit=%s\trx_msg=%s\trx_tx_id=%s\trx_lat=%s\trx_lon=%s\trx_alt_m=%s\trssi=%.1f\tsnr=%.1f\tbatt_present=%u\tbatt_v=%.3f\n", (long long)epoch, human, NODE_SHORT, msg ? msg : "", + txId, lat, lon, alt, @@ -770,57 +918,114 @@ static void writeRecvLog(int64_t epoch, const DateTime& dt, const char* msg, flo g_recvFile.flush(); } -static void buildTxPayload(char* out, size_t outLen) { - if (!out || outLen == 0) return; +static void buildTxPayload(char *out, size_t outLen, uint32_t txId) +{ + if (!out || outLen == 0) + return; out[0] = '\0'; char lat[24], lon[24], alt[24]; gpsFieldStrings(lat, sizeof(lat), lon, sizeof(lon), alt, sizeof(alt)); - snprintf(out, outLen, "%s,%s,%s,%s", NODE_SHORT, lat, lon, alt); + snprintf(out, outLen, "%s,%lu,%s,%s,%s", NODE_SHORT, (unsigned long)txId, lat, lon, alt); } -static void parsePayloadCoords(const char* msg, char* latOut, size_t latLen, char* lonOut, size_t lonLen, char* altOut, size_t altLen) { - if (latOut && latLen > 0) latOut[0] = '\0'; - if (lonOut && lonLen > 0) lonOut[0] = '\0'; - if (altOut && altLen > 0) altOut[0] = '\0'; - if (!msg || msg[0] == '\0') return; +static bool isAllDigits(const char *s) +{ + if (!s || s[0] == '\0') + return false; + for (size_t i = 0; s[i] != '\0'; ++i) + { + if (!isdigit((unsigned char)s[i])) + return false; + } + return true; +} + +static void parsePayloadFields(const char *msg, + char *txIdOut, size_t txIdLen, + char *latOut, size_t latLen, + char *lonOut, size_t lonLen, + char *altOut, size_t altLen) +{ + if (txIdOut && txIdLen > 0) + txIdOut[0] = '\0'; + if (latOut && latLen > 0) + latOut[0] = '\0'; + if (lonOut && lonLen > 0) + lonOut[0] = '\0'; + if (altOut && altLen > 0) + altOut[0] = '\0'; + if (!msg || msg[0] == '\0') + return; char buf[128]; size_t n = strlen(msg); - if (n >= sizeof(buf)) n = sizeof(buf) - 1; + if (n >= sizeof(buf)) + n = sizeof(buf) - 1; memcpy(buf, msg, n); buf[n] = '\0'; - char* saveptr = nullptr; - char* token = strtok_r(buf, ",", &saveptr); // unit label + char *saveptr = nullptr; + char *token = strtok_r(buf, ",", &saveptr); // unit label (void)token; - token = strtok_r(nullptr, ",", &saveptr); // lat - if (token && latOut && latLen > 0) snprintf(latOut, latLen, "%s", token); + token = strtok_r(nullptr, ",", &saveptr); // tx_id or lat (legacy) + if (!token) + return; - token = strtok_r(nullptr, ",", &saveptr); // lon - if (token && lonOut && lonLen > 0) snprintf(lonOut, lonLen, "%s", token); + if (isAllDigits(token)) + { + if (txIdOut && txIdLen > 0) + snprintf(txIdOut, txIdLen, "%s", token); - token = strtok_r(nullptr, ",", &saveptr); // alt - if (token && altOut && altLen > 0) snprintf(altOut, altLen, "%s", token); + token = strtok_r(nullptr, ",", &saveptr); // lat + if (token && latOut && latLen > 0) + snprintf(latOut, latLen, "%s", token); + + token = strtok_r(nullptr, ",", &saveptr); // lon + if (token && lonOut && lonLen > 0) + snprintf(lonOut, lonLen, "%s", token); + + token = strtok_r(nullptr, ",", &saveptr); // alt + if (token && altOut && altLen > 0) + snprintf(altOut, altLen, "%s", token); + } + else + { + // Backward compatibility: older payloads were "UNIT,lat,lon,alt". + if (latOut && latLen > 0) + snprintf(latOut, latLen, "%s", token); + + token = strtok_r(nullptr, ",", &saveptr); // lon + if (token && lonOut && lonLen > 0) + snprintf(lonOut, lonLen, "%s", token); + + token = strtok_r(nullptr, ",", &saveptr); // alt + if (token && altOut && altLen > 0) + snprintf(altOut, altLen, "%s", token); + } } -static void onLoRaDio1Rise() { +static void onLoRaDio1Rise() +{ g_rxFlag = true; } -static bool initRadio() { +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) { + 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) { + if (state != RADIOLIB_ERR_NONE) + { logf("radio.startReceive failed code=%d", state); return false; } @@ -829,7 +1034,8 @@ static bool initRadio() { return true; } -static void showRxOnOled(const DateTime& dt, const char* msg) { +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); @@ -838,31 +1044,40 @@ static void showRxOnOled(const DateTime& dt, const char* msg) { oledShowLines(hhmmss, line); } -static void runTxScheduler() { +static void runTxScheduler() +{ DateTime now{}; int64_t epoch = 0; - if (!getCurrentUtc(now, epoch)) return; + if (!getCurrentUtc(now, epoch)) + return; int slotSecond = NODE_SLOT_INDEX * (int)kSlotSeconds; int secInFrame = now.second % 10; - if (secInFrame != slotSecond) return; + if (secInFrame != slotSecond) + return; int64_t epochSecond = epoch; - if (epochSecond == g_lastTxEpochSecond) return; + if (epochSecond == g_lastTxEpochSecond) + return; g_lastTxEpochSecond = epochSecond; + uint32_t txId = ++g_txAttemptId; g_rxFlag = false; g_radio.clearDio1Action(); char payload[96]; - buildTxPayload(payload, sizeof(payload)); + buildTxPayload(payload, sizeof(payload), txId); int tx = g_radio.transmit(payload); - if (tx == RADIOLIB_ERR_NONE) { + if (tx == RADIOLIB_ERR_NONE) + { g_txCount++; - writeSentLog(epoch, now); - logf("TX %s count=%lu payload=%s", NODE_SHORT, (unsigned long)g_txCount, payload); - } else { + writeSentLog(epoch, now, txId, payload, true); + logf("TX %s tx_id=%lu success_count=%lu payload=%s", NODE_SHORT, (unsigned long)txId, (unsigned long)g_txCount, payload); + } + else + { + writeSentLog(epoch, now, txId, payload, false); logf("TX failed code=%d", tx); } @@ -871,20 +1086,24 @@ static void runTxScheduler() { g_radio.startReceive(); } -static void runRxHandler() { - if (!g_rxFlag) return; +static void runRxHandler() +{ + if (!g_rxFlag) + return; g_rxFlag = false; String rx; int rc = g_radio.readData(rx); - if (rc != RADIOLIB_ERR_NONE) { + if (rc != RADIOLIB_ERR_NONE) + { g_radio.startReceive(); return; } DateTime now{}; int64_t epoch = 0; - if (getCurrentUtc(now, epoch)) { + if (getCurrentUtc(now, epoch)) + { writeRecvLog(epoch, now, rx.c_str(), g_radio.getRSSI(), g_radio.getSNR()); showRxOnOled(now, rx.c_str()); } @@ -892,31 +1111,40 @@ static void runRxHandler() { g_radio.startReceive(); } -static void enterWaitSdState() { - if (g_phase == AppPhase::WAIT_SD) return; +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; +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; +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()) { +static void updateWaitSd() +{ + if (g_sd.isMounted()) + { g_lastWarnMs = 0; g_gpsPathReady = false; enterWaitDisciplineState(); @@ -924,56 +1152,70 @@ static void updateWaitSd() { } uint32_t now = millis(); - if ((uint32_t)(now - g_lastWarnMs) >= kSdMessagePeriodMs) { + 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()) { +static void updateWaitDiscipline() +{ + if (!g_sd.isMounted()) + { enterWaitSdState(); return; } - if (!isDisciplineStale()) { + if (!isDisciplineStale()) + { enterRunState(); return; } uint32_t now = millis(); - if ((uint32_t)(now - g_lastWarnMs) >= kNoGpsMessagePeriodMs) { + 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; + if ((uint32_t)(now - g_lastDisciplineTryMs) < kDisciplineRetryMs) + return; g_lastDisciplineTryMs = now; - if (disciplineRtcToGps()) { + if (disciplineRtcToGps()) + { g_lastWarnMs = 0; enterRunState(); } } -static void updateRun() { +static void updateRun() +{ uint32_t now = millis(); - if (!g_sd.isMounted()) { - if ((uint32_t)(now - g_lastWarnMs) >= kSdMessagePeriodMs) { + if (!g_sd.isMounted()) + { + if ((uint32_t)(now - g_lastWarnMs) >= kSdMessagePeriodMs) + { g_lastWarnMs = now; oledShowLines("SD removed", "Logging paused", "LoRa continues"); } - } else if (!g_sessionReady) { + } + else if (!g_sessionReady) + { // Card came back while running. Resume append logging without pausing radio work. (void)openSessionLogs(); } - if ((uint32_t)(now - g_lastHealthCheckMs) >= kHealthCheckPeriodMs) { + if ((uint32_t)(now - g_lastHealthCheckMs) >= kHealthCheckPeriodMs) + { g_lastHealthCheckMs = now; - if (isDisciplineStale()) { + if (isDisciplineStale()) + { enterWaitDisciplineState(); return; } @@ -983,7 +1225,8 @@ static void updateRun() { runRxHandler(); } -void setup() { +void setup() +{ Serial.begin(115200); delay(kSerialDelayMs); @@ -991,7 +1234,8 @@ void setup() { Serial.println("Exercise 12: FiveTalk"); Serial.println("=================================================="); - if (!tbeam_supreme::initPmuForPeripherals(g_pmu, &Serial)) { + if (!tbeam_supreme::initPmuForPeripherals(g_pmu, &Serial)) + { logf("WARN: PMU init failed"); } @@ -1001,7 +1245,8 @@ void setup() { oledShowLines("Exercise 12", "FiveTalk startup", NODE_SHORT, NODE_LABEL); SdWatcherConfig sdCfg{}; - if (!g_sd.begin(sdCfg, nullptr)) { + if (!g_sd.begin(sdCfg, nullptr)) + { logf("WARN: SD watcher begin failed"); } @@ -1017,52 +1262,61 @@ void setup() { g_gpsSerial.begin(GPS_BAUD, SERIAL_8N1, GPS_RX_PIN, GPS_TX_PIN); g_radioReady = initRadio(); - if (!g_radioReady) { + if (!g_radioReady) + { oledShowLines("LoRa init failed", "Check radio pins"); } g_phase = g_sd.isMounted() ? AppPhase::WAIT_DISCIPLINE : AppPhase::WAIT_SD; } -void loop() { +void loop() +{ pollGpsSerial(); g_sd.update(); - if (g_sd.consumeMountedEvent()) { + if (g_sd.consumeMountedEvent()) + { logf("SD mounted"); g_gpsPathReady = false; - if (g_phase == AppPhase::RUN) { + if (g_phase == AppPhase::RUN) + { g_lastWarnMs = 0; - if (!g_sessionReady) { + if (!g_sessionReady) + { (void)openSessionLogs(); } } } - if (g_sd.consumeRemovedEvent()) { + if (g_sd.consumeRemovedEvent()) + { logf("SD removed"); g_gpsPathReady = false; - if (g_phase == AppPhase::RUN) { + if (g_phase == AppPhase::RUN) + { closeSessionLogs(); g_lastWarnMs = 0; oledShowLines("SD removed", "Logging paused", "LoRa continues"); } } - if (!g_radioReady) { + 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; + switch (g_phase) + { + case AppPhase::WAIT_SD: + updateWaitSd(); + break; + case AppPhase::WAIT_DISCIPLINE: + updateWaitDiscipline(); + break; + case AppPhase::RUN: + updateRun(); + break; } delay(5);