microReticulumTbeam/exercises/09_GPS_Time/src/main.cpp

1488 lines
36 KiB
C++

// 20260217 ChatGPT
// $Id$
// $HeadURL$
#include <Arduino.h>
#include <SPIFFS.h>
#include <Wire.h>
#include <U8g2lib.h>
#include "StartupSdManager.h"
#include "tbeam_supreme_adapter.h"
#ifndef OLED_SDA
#define OLED_SDA 17
#endif
#ifndef OLED_SCL
#define OLED_SCL 18
#endif
#ifndef OLED_ADDR
#define OLED_ADDR 0x3C
#endif
#ifndef RTC_I2C_ADDR
#define RTC_I2C_ADDR 0x51
#endif
#ifndef GPS_BAUD
#define GPS_BAUD 9600
#endif
static const uint32_t kSerialDelayMs = 5000;
static const uint32_t kMinuteMs = 60000;
static const uint32_t kGpsDiagnosticLogMs = 15000;
static const char *kGpsLogDir = "/gpsdiag";
static const char *kGpsLogPath = "/gpsdiag/current.log";
static const char *kBuildDate = __DATE__;
static const char *kBuildTime = __TIME__;
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_lastGpsDiagnosticLogMs = 0;
static uint32_t g_gpsBaud = GPS_BAUD;
static int g_gpsRxPin = GPS_RX_PIN;
static int g_gpsTxPin = GPS_TX_PIN;
static bool g_spiffsReady = false;
static bool g_ubloxConfigAttempted = false;
static bool g_ubloxConfigured = false;
static bool g_ubloxIsM10 = false;
static bool g_prevHadSatellites = false;
static bool g_prevHadValidUtc = false;
static bool g_satellitesAcquiredAnnounced = false;
static bool g_timeAcquiredAnnounced = false;
static uint8_t g_lastDrawnSatsUsed = 255;
static uint8_t g_lastDrawnSatsView = 255;
static bool g_lastDrawnValidUtc = false;
static bool g_haveLastDrawnState = false;
static uint32_t g_lastDisplayRefreshMs = 0;
static char g_gpsLine[128];
static size_t g_gpsLineLen = 0;
static char g_serialLine[128];
static size_t g_serialLineLen = 0;
static uint8_t g_rawLogGgaCount = 0;
static uint8_t g_rawLogGsaCount = 0;
static uint8_t g_rawLogGsvCount = 0;
static uint8_t g_rawLogRmcCount = 0;
static uint8_t g_rawLogPubxCount = 0;
enum class GpsModuleKind : uint8_t
{
UNKNOWN = 0,
L76K,
UBLOX
};
#if defined(GPS_UBLOX)
static const GpsModuleKind kExpectedGpsModule = GpsModuleKind::UBLOX;
#elif defined(GPS_L76K)
static const GpsModuleKind kExpectedGpsModule = GpsModuleKind::L76K;
#else
static const GpsModuleKind kExpectedGpsModule = GpsModuleKind::UNKNOWN;
#endif
struct RtcDateTime
{
uint16_t year;
uint8_t month;
uint8_t day;
uint8_t hour;
uint8_t minute;
uint8_t second;
};
struct GpsState
{
GpsModuleKind module = kExpectedGpsModule;
bool sawAnySentence = false;
uint8_t satsUsed = 0;
uint8_t satsInView = 0;
uint8_t satsUsedWindowMax = 0;
uint8_t satsInViewWindowMax = 0;
uint32_t satsUsedWindowMs = 0;
uint32_t satsInViewWindowMs = 0;
bool hasValidUtc = false;
uint32_t utcFixMs = 0;
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 const uint32_t kSatelliteWindowMs = 2000;
static const uint32_t kDisplayRefreshMinMs = 1000;
static const uint32_t kFixFreshMs = 5000;
static String gpsModuleToString(GpsModuleKind kind);
static GpsModuleKind activeGpsModule();
static uint8_t bestSatelliteCount();
static uint8_t displayedSatsUsed();
static uint8_t displayedSatsInView();
static bool displayHasFreshUtc();
static String formatRtcNow();
static bool ensureGpsLogDirectory()
{
if (!g_spiffsReady)
{
return false;
}
if (SPIFFS.exists(kGpsLogDir))
{
return true;
}
return SPIFFS.mkdir(kGpsLogDir);
}
static bool gpsDiagAppendLine(const char *line)
{
if (!g_spiffsReady || !line)
{
return false;
}
File file = SPIFFS.open(kGpsLogPath, FILE_APPEND);
if (!file)
{
return false;
}
file.print(line);
file.print("\r\n");
file.close();
return true;
}
static void formatGpsSnapshot(char *out, size_t outSize, const char *event)
{
if (!out || outSize == 0)
{
return;
}
const uint8_t sats = bestSatelliteCount();
const char *ev = event ? event : "sample";
if (g_gps.hasValidUtc)
{
snprintf(out,
outSize,
"ms=%lu event=%s module=%s nmea=%s sats_used=%u sats_view=%u sats_best=%u utc=%04u-%02u-%02uT%02u:%02u:%02u rx=%d tx=%d baud=%lu",
(unsigned long)millis(),
ev,
gpsModuleToString(activeGpsModule()).c_str(),
g_gps.sawAnySentence ? "yes" : "no",
(unsigned)g_gps.satsUsed,
(unsigned)g_gps.satsInView,
(unsigned)sats,
(unsigned)g_gps.utcYear,
(unsigned)g_gps.utcMonth,
(unsigned)g_gps.utcDay,
(unsigned)g_gps.utcHour,
(unsigned)g_gps.utcMinute,
(unsigned)g_gps.utcSecond,
g_gpsRxPin,
g_gpsTxPin,
(unsigned long)g_gpsBaud);
}
else
{
String rtc = formatRtcNow();
snprintf(out,
outSize,
"ms=%lu event=%s module=%s nmea=%s sats_used=%u sats_view=%u sats_best=%u utc=NO rtc=\"%s\" rx=%d tx=%d baud=%lu",
(unsigned long)millis(),
ev,
gpsModuleToString(activeGpsModule()).c_str(),
g_gps.sawAnySentence ? "yes" : "no",
(unsigned)g_gps.satsUsed,
(unsigned)g_gps.satsInView,
(unsigned)sats,
rtc.c_str(),
g_gpsRxPin,
g_gpsTxPin,
(unsigned long)g_gpsBaud);
}
}
static void appendGpsSnapshot(const char *event)
{
char line[256];
formatGpsSnapshot(line, sizeof(line), event);
(void)gpsDiagAppendLine(line);
}
static String buildStampShort()
{
char buf[32];
snprintf(buf, sizeof(buf), "%s %.5s", kBuildDate, kBuildTime);
return String(buf);
}
static void maybeLogRawSentence(const char *type, const char *sentence)
{
if (!type || !sentence || !g_spiffsReady)
{
return;
}
uint8_t *counter = nullptr;
if (strcmp(type, "GGA") == 0)
{
counter = &g_rawLogGgaCount;
}
else if (strcmp(type, "GSA") == 0)
{
counter = &g_rawLogGsaCount;
}
else if (strcmp(type, "GSV") == 0)
{
counter = &g_rawLogGsvCount;
}
else if (strcmp(type, "RMC") == 0)
{
counter = &g_rawLogRmcCount;
}
else if (strcmp(type, "PUBX") == 0)
{
counter = &g_rawLogPubxCount;
}
if (!counter || *counter >= 12)
{
return;
}
(*counter)++;
char line[220];
snprintf(line,
sizeof(line),
"ms=%lu event=raw_%s idx=%u sentence=%s",
(unsigned long)millis(),
type,
(unsigned)*counter,
sentence);
(void)gpsDiagAppendLine(line);
}
static void clearGpsSerialInput()
{
g_gpsLineLen = 0;
while (g_gpsSerial.available() > 0)
{
(void)g_gpsSerial.read();
}
}
static void ubxChecksum(uint8_t *message, size_t length)
{
uint8_t ckA = 0;
uint8_t ckB = 0;
for (size_t i = 2; i < length - 2; ++i)
{
ckA = (uint8_t)((ckA + message[i]) & 0xFF);
ckB = (uint8_t)((ckB + ckA) & 0xFF);
}
message[length - 2] = ckA;
message[length - 1] = ckB;
}
static size_t makeUbxPacket(uint8_t *out,
size_t outSize,
uint8_t classId,
uint8_t msgId,
const uint8_t *payload,
uint16_t payloadSize)
{
if (!out || outSize < (size_t)payloadSize + 8U)
{
return 0;
}
out[0] = 0xB5;
out[1] = 0x62;
out[2] = classId;
out[3] = msgId;
out[4] = (uint8_t)(payloadSize & 0xFF);
out[5] = (uint8_t)((payloadSize >> 8) & 0xFF);
for (uint16_t i = 0; i < payloadSize; ++i)
{
out[6 + i] = payload ? payload[i] : 0;
}
out[6 + payloadSize] = 0;
out[7 + payloadSize] = 0;
ubxChecksum(out, payloadSize + 8U);
return (size_t)payloadSize + 8U;
}
static bool waitForUbxAck(uint8_t classId, uint8_t msgId, uint32_t waitMs)
{
uint8_t ack[10] = {0xB5, 0x62, 0x05, 0x01, 0x02, 0x00, classId, msgId, 0x00, 0x00};
ubxChecksum(ack, sizeof(ack));
uint8_t ackPos = 0;
uint32_t deadline = millis() + waitMs;
while ((int32_t)(deadline - millis()) > 0)
{
if (g_gpsSerial.available() <= 0)
{
delay(2);
continue;
}
uint8_t b = (uint8_t)g_gpsSerial.read();
if (b == ack[ackPos])
{
ackPos++;
if (ackPos == sizeof(ack))
{
return true;
}
}
else
{
ackPos = (b == ack[0]) ? 1 : 0;
}
}
return false;
}
static int waitForUbxPayload(uint8_t *buffer,
uint16_t bufferSize,
uint8_t classId,
uint8_t msgId,
uint32_t waitMs)
{
uint16_t framePos = 0;
uint16_t needRead = 0;
uint32_t deadline = millis() + waitMs;
while ((int32_t)(deadline - millis()) > 0)
{
if (g_gpsSerial.available() <= 0)
{
delay(2);
continue;
}
int c = g_gpsSerial.read();
switch (framePos)
{
case 0:
framePos = (c == 0xB5) ? 1 : 0;
break;
case 1:
framePos = (c == 0x62) ? 2 : 0;
break;
case 2:
framePos = (c == classId) ? 3 : 0;
break;
case 3:
framePos = (c == msgId) ? 4 : 0;
break;
case 4:
needRead = (uint16_t)c;
framePos = 5;
break;
case 5:
needRead |= (uint16_t)(c << 8);
if (needRead == 0 || needRead >= bufferSize)
{
framePos = 0;
break;
}
if (g_gpsSerial.readBytes(buffer, needRead) != needRead)
{
framePos = 0;
break;
}
if (g_gpsSerial.available() >= 2)
{
(void)g_gpsSerial.read();
(void)g_gpsSerial.read();
}
return (int)needRead;
default:
framePos = 0;
break;
}
}
return 0;
}
static bool detectUbloxM10()
{
uint8_t packet[8];
uint8_t payload[256] = {0};
size_t len = makeUbxPacket(packet, sizeof(packet), 0x0A, 0x04, nullptr, 0);
if (len == 0)
{
return false;
}
clearGpsSerialInput();
g_gpsSerial.write(packet, len);
int payloadLen = waitForUbxPayload(payload, sizeof(payload), 0x0A, 0x04, 1200);
if (payloadLen < 40)
{
appendGpsSnapshot("ubx_monver_timeout");
return false;
}
char hwVersion[11] = {0};
memcpy(hwVersion, payload + 30, 10);
char line[160];
snprintf(line, sizeof(line), "ms=%lu event=ubx_monver hw=%s", (unsigned long)millis(), hwVersion);
(void)gpsDiagAppendLine(line);
if (strncmp(hwVersion, "000A0000", 8) == 0)
{
return true;
}
for (int pos = 40; pos + 30 <= payloadLen; pos += 30)
{
if (strncmp((const char *)(payload + pos), "PROTVER=", 8) == 0)
{
int prot = atoi((const char *)(payload + pos + 8));
snprintf(line, sizeof(line), "ms=%lu event=ubx_monver prot=%d", (unsigned long)millis(), prot);
(void)gpsDiagAppendLine(line);
if (prot >= 27)
{
return true;
}
}
}
return false;
}
static bool sendUbxValset(uint8_t classId,
uint8_t msgId,
const uint8_t *payload,
uint16_t payloadLen,
uint32_t ackMs,
const char *eventName)
{
uint8_t packet[96];
size_t len = makeUbxPacket(packet, sizeof(packet), classId, msgId, payload, payloadLen);
if (len == 0)
{
return false;
}
clearGpsSerialInput();
g_gpsSerial.write(packet, len);
bool ok = waitForUbxAck(classId, msgId, ackMs);
char line[160];
snprintf(line,
sizeof(line),
"ms=%lu event=%s ack=%s",
(unsigned long)millis(),
eventName ? eventName : "ubx_cfg",
ok ? "yes" : "no");
(void)gpsDiagAppendLine(line);
delay(150);
return ok;
}
static bool configureUbloxM10()
{
static const uint8_t kValsetDisableTxtRam[] = {0x00, 0x01, 0x00, 0x00, 0x07, 0x00, 0x92, 0x20, 0x03};
static const uint8_t kValsetDisableTxtBbr[] = {0x00, 0x02, 0x00, 0x00, 0x07, 0x00, 0x92, 0x20, 0x03};
static const uint8_t kValsetEnableNmeaRam[] = {0x00, 0x01, 0x00, 0x00, 0xbb, 0x00, 0x91, 0x20, 0x01, 0xac, 0x00, 0x91, 0x20, 0x01};
static const uint8_t kValsetEnableNmeaBbr[] = {0x00, 0x02, 0x00, 0x00, 0xbb, 0x00, 0x91, 0x20, 0x01, 0xac, 0x00, 0x91, 0x20, 0x01};
static const uint8_t kSave10[] = {0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01};
bool ok = true;
ok &= sendUbxValset(0x06, 0x8A, kValsetDisableTxtRam, sizeof(kValsetDisableTxtRam), 300, "ubx_m10_disable_txt_ram");
ok &= sendUbxValset(0x06, 0x8A, kValsetDisableTxtBbr, sizeof(kValsetDisableTxtBbr), 300, "ubx_m10_disable_txt_bbr");
ok &= sendUbxValset(0x06, 0x8A, kValsetEnableNmeaBbr, sizeof(kValsetEnableNmeaBbr), 400, "ubx_m10_enable_nmea_bbr");
ok &= sendUbxValset(0x06, 0x8A, kValsetEnableNmeaRam, sizeof(kValsetEnableNmeaRam), 400, "ubx_m10_enable_nmea_ram");
ok &= sendUbxValset(0x06, 0x09, kSave10, sizeof(kSave10), 800, "ubx_m10_save");
appendGpsSnapshot(ok ? "ubx_m10_configured" : "ubx_m10_config_failed");
return ok;
}
static void maybeConfigureUblox()
{
if (g_ubloxConfigAttempted || kExpectedGpsModule != GpsModuleKind::UBLOX)
{
return;
}
g_ubloxConfigAttempted = true;
appendGpsSnapshot("ubx_config_attempt");
g_ubloxIsM10 = detectUbloxM10();
if (!g_ubloxIsM10)
{
appendGpsSnapshot("ubx_non_m10_or_unknown");
return;
}
g_ubloxConfigured = configureUbloxM10();
}
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::UBLOX)
{
return "UBLOX";
}
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)
{
g_gps.module = GpsModuleKind::L76K;
}
}
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;
if ((uint8_t)sats > g_gps.satsUsedWindowMax)
{
g_gps.satsUsedWindowMax = (uint8_t)sats;
}
g_gps.satsUsedWindowMs = millis();
}
}
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 ((uint8_t)sats > g_gps.satsInViewWindowMax)
{
g_gps.satsInViewWindowMax = (uint8_t)sats;
}
g_gps.satsInViewWindowMs = millis();
}
}
static void parseGsa(char *fields[], int count)
{
if (count <= 3)
{
return;
}
}
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;
g_gps.utcFixMs = millis();
}
static void parseTxt(char *fields[], int count)
{
if (count <= 4)
{
return;
}
detectModuleFromText(fields[4]);
}
static int splitCsvPreserveEmpty(char *line, char *fields[], int maxFields)
{
if (!line || !fields || maxFields <= 0)
{
return 0;
}
int count = 0;
char *p = line;
fields[count++] = p;
while (*p && count < maxFields)
{
if (*p == ',')
{
*p = '\0';
fields[count++] = p + 1;
}
++p;
}
return count;
}
static void processNmeaLine(char *line)
{
if (!line || line[0] != '$')
{
return;
}
g_gps.sawAnySentence = true;
char rawLine[128];
strncpy(rawLine, line, sizeof(rawLine) - 1);
rawLine[sizeof(rawLine) - 1] = '\0';
char *star = strchr(line, '*');
if (star)
{
*star = '\0';
}
char *fields[24] = {0};
int count = splitCsvPreserveEmpty(line, fields, 24);
if (count <= 0 || !fields[0])
{
return;
}
const char *header = fields[0];
if (strcmp(header, "$PUBX") == 0)
{
g_gps.module = GpsModuleKind::UBLOX;
maybeLogRawSentence("PUBX", rawLine);
return;
}
size_t n = strlen(header);
if (n < 6)
{
return;
}
const char *type = header + (n - 3);
maybeLogRawSentence(type, rawLine);
if (strcmp(type, "GGA") == 0)
{
parseGga(fields, count);
}
else if (strcmp(type, "GSA") == 0)
{
parseGsa(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 showGpsLogHelp()
{
Serial.println("Command list:");
Serial.println(" help - show command menu");
Serial.println(" stat - show current GPS log file info");
Serial.println(" list - list files in /gpsdiag");
Serial.println(" read - dump current GPS log");
Serial.println(" clear - erase current GPS log");
}
static void gpsLogStat()
{
Serial.printf("SPIFFS: %s\r\n", g_spiffsReady ? "ready" : "not ready");
Serial.printf("Path: %s\r\n", kGpsLogPath);
if (!g_spiffsReady || !SPIFFS.exists(kGpsLogPath))
{
Serial.println("Current GPS log does not exist");
return;
}
File file = SPIFFS.open(kGpsLogPath, FILE_READ);
if (!file)
{
Serial.println("Unable to open current GPS log");
return;
}
Serial.printf("Size: %u bytes\r\n", (unsigned)file.size());
file.close();
Serial.printf("Flash total=%u used=%u free=%u\r\n",
(unsigned)SPIFFS.totalBytes(),
(unsigned)SPIFFS.usedBytes(),
(unsigned)(SPIFFS.totalBytes() - SPIFFS.usedBytes()));
}
static void gpsLogList()
{
if (!g_spiffsReady)
{
Serial.println("SPIFFS not ready");
return;
}
File dir = SPIFFS.open(kGpsLogDir);
if (!dir || !dir.isDirectory())
{
Serial.printf("Unable to open %s\r\n", kGpsLogDir);
return;
}
Serial.printf("Files in %s:\r\n", kGpsLogDir);
File file = dir.openNextFile();
while (file)
{
Serial.printf(" %s (%u bytes)\r\n", file.name(), (unsigned)file.size());
file = dir.openNextFile();
}
}
static void gpsLogRead()
{
if (!g_spiffsReady || !SPIFFS.exists(kGpsLogPath))
{
Serial.println("Current GPS log is not available");
return;
}
File file = SPIFFS.open(kGpsLogPath, FILE_READ);
if (!file)
{
Serial.println("Unable to read current GPS log");
return;
}
Serial.printf("Reading %s:\r\n", kGpsLogPath);
while (file.available())
{
Serial.write(file.read());
}
if (file.size() > 0)
{
Serial.println();
}
file.close();
}
static void gpsLogClear()
{
if (!g_spiffsReady)
{
Serial.println("SPIFFS not ready");
return;
}
File file = SPIFFS.open(kGpsLogPath, FILE_WRITE);
if (!file)
{
Serial.println("Unable to clear current GPS log");
return;
}
file.close();
Serial.printf("Cleared %s\r\n", kGpsLogPath);
}
// Process a command received on the serial console.
static void processSerialCommand(const char *line)
{
if (!line || line[0] == '\0')
{
return;
}
// Echo the command back to the console for clarity and posterity.
Serial.printf("-->%s\r\n", line);
if (strcasecmp(line, "help") == 0)
{
showGpsLogHelp();
}
else if (strcasecmp(line, "stat") == 0)
{
gpsLogStat();
}
else if (strcasecmp(line, "list") == 0)
{
gpsLogList();
}
else if (strcasecmp(line, "read") == 0)
{
gpsLogRead();
}
else if (strcasecmp(line, "clear") == 0)
{
gpsLogClear();
}
else
{
Serial.println("Unknown command (help for list)");
}
}
static void pollSerialConsole()
{
while (Serial.available() > 0)
{
int c = Serial.read();
if (c < 0)
{
continue;
}
if (c == '\r' || c == '\n')
{
if (g_serialLineLen > 0)
{
g_serialLine[g_serialLineLen] = '\0';
processSerialCommand(g_serialLine);
g_serialLineLen = 0;
}
continue;
}
if (g_serialLineLen + 1 < sizeof(g_serialLine))
{
g_serialLine[g_serialLineLen++] = (char)c;
}
else
{
g_serialLineLen = 0;
}
}
}
static void startGpsUart(uint32_t baud, int rxPin, int txPin)
{
g_gpsSerial.end();
delay(20);
g_gpsSerial.setRxBufferSize(1024);
g_gpsSerial.begin(baud, SERIAL_8N1, rxPin, txPin);
g_gpsBaud = baud;
g_gpsRxPin = rxPin;
g_gpsTxPin = txPin;
}
static bool collectGpsTraffic(uint32_t windowMs, bool updateSd)
{
uint32_t start = millis();
bool sawBytes = false;
while ((uint32_t)(millis() - start) < windowMs)
{
if (g_gpsSerial.available() > 0)
{
sawBytes = true;
}
pollGpsSerial();
if (updateSd)
{
g_sd.update();
}
delay(2);
}
return sawBytes || g_gps.sawAnySentence;
}
static bool probeGpsAtBaud(uint32_t baud, int rxPin, int txPin)
{
startGpsUart(baud, rxPin, txPin);
logf("Probing GPS at %lu baud on RX=%d TX=%d...", (unsigned long)baud, rxPin, txPin);
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("$PUBX,00*33\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};
int pinCandidates[2][2] = {
{GPS_RX_PIN, GPS_TX_PIN},
{34, 12}, // Legacy T-Beam UBLOX mapping.
};
size_t pinCount = 1;
if (kExpectedGpsModule == GpsModuleKind::UBLOX &&
!(GPS_RX_PIN == 34 && GPS_TX_PIN == 12))
{
pinCount = 2;
}
for (size_t p = 0; p < pinCount; ++p)
{
int rxPin = pinCandidates[p][0];
int txPin = pinCandidates[p][1];
for (size_t i = 0; i < sizeof(bauds) / sizeof(bauds[0]); ++i)
{
if (probeGpsAtBaud(bauds[i], rxPin, txPin))
{
logf("GPS traffic detected at %lu baud on RX=%d TX=%d",
(unsigned long)g_gpsBaud, g_gpsRxPin, g_gpsTxPin);
return;
}
}
}
logf("No GPS traffic detected during startup probe");
}
static uint32_t startupProbeWindowMs()
{
return (kExpectedGpsModule == GpsModuleKind::UBLOX) ? 45000U : 20000U;
}
static GpsModuleKind activeGpsModule()
{
if (g_gps.module != GpsModuleKind::UNKNOWN)
{
return g_gps.module;
}
return kExpectedGpsModule;
}
static uint8_t bestSatelliteCount()
{
uint32_t now = millis();
if ((uint32_t)(now - g_gps.satsUsedWindowMs) > kSatelliteWindowMs)
{
g_gps.satsUsedWindowMax = g_gps.satsUsed;
}
if ((uint32_t)(now - g_gps.satsInViewWindowMs) > kSatelliteWindowMs)
{
g_gps.satsInViewWindowMax = g_gps.satsInView;
}
uint8_t used = (g_gps.satsUsedWindowMax > g_gps.satsUsed) ? g_gps.satsUsedWindowMax : g_gps.satsUsed;
uint8_t inView = (g_gps.satsInViewWindowMax > g_gps.satsInView) ? g_gps.satsInViewWindowMax : g_gps.satsInView;
return (used > inView) ? used : inView;
}
static uint8_t displayedSatsUsed()
{
if ((uint32_t)(millis() - g_gps.satsUsedWindowMs) > kFixFreshMs)
{
return 0;
}
return g_gps.satsUsed;
}
static uint8_t displayedSatsInView()
{
if ((uint32_t)(millis() - g_gps.satsInViewWindowMs) > kFixFreshMs)
{
return 0;
}
return g_gps.satsInView;
}
static bool displayHasFreshUtc()
{
return g_gps.hasValidUtc && (uint32_t)(millis() - g_gps.utcFixMs) <= kFixFreshMs;
}
static bool isUnsupportedGpsMode()
{
GpsModuleKind active = activeGpsModule();
if (kExpectedGpsModule == GpsModuleKind::UNKNOWN || active == GpsModuleKind::UNKNOWN)
{
return false;
}
return active != kExpectedGpsModule;
}
static void reportStatusToSerial()
{
uint8_t satsUsed = displayedSatsUsed();
uint8_t satsView = displayedSatsInView();
logf("GPS module active: %s", gpsModuleToString(activeGpsModule()).c_str());
logf("GPS module expected: %s", gpsModuleToString(kExpectedGpsModule).c_str());
logf("GPS sentences seen: %s", g_gps.sawAnySentence ? "yes" : "no");
logf("GPS satellites: used=%u in-view=%u recent-best=%u",
(unsigned)satsUsed,
(unsigned)satsView,
(unsigned)bestSatelliteCount());
logf("GPS can provide time from satellites: %s", displayHasFreshUtc() ? "YES" : "NO");
appendGpsSnapshot("status");
}
static void maybeAnnounceGpsTransitions()
{
if (isUnsupportedGpsMode())
{
return;
}
uint8_t satsUsed = displayedSatsUsed();
uint8_t satsView = displayedSatsInView();
uint8_t sats = satsUsed > 0 ? satsUsed : satsView;
bool hasSats = sats > 0;
bool hasUtc = displayHasFreshUtc();
if (!g_satellitesAcquiredAnnounced && !g_prevHadSatellites && hasSats)
{
String rtc = formatRtcNow();
char l2[28];
char l3[28];
snprintf(l2, sizeof(l2), "Used: %u", (unsigned)satsUsed);
snprintf(l3, sizeof(l3), "View: %u", (unsigned)satsView);
oledShowLines("GPS acquired", l2, l3, "Waiting for UTC...", rtc.c_str());
logf("Transition: satellites acquired (%u)", (unsigned)sats);
appendGpsSnapshot("satellites_acquired");
g_satellitesAcquiredAnnounced = true;
}
if (!g_timeAcquiredAnnounced && !g_prevHadValidUtc && hasUtc)
{
char line2[40];
char line3[28];
char line4[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), "Used:%u View:%u", (unsigned)satsUsed, (unsigned)satsView);
snprintf(line4, sizeof(line4), "Source: %s", gpsModuleToString(activeGpsModule()).c_str());
oledShowLines("GPS UTC acquired", line2, line3, line4);
logf("Transition: GPS UTC acquired: %s", line2);
appendGpsSnapshot("utc_acquired");
g_timeAcquiredAnnounced = true;
}
g_prevHadSatellites = hasSats;
g_prevHadValidUtc = hasUtc;
}
static void drawMinuteStatus()
{
if (isUnsupportedGpsMode())
{
oledShowLines("GPS module mismatch",
("Expected: " + gpsModuleToString(kExpectedGpsModule)).c_str(),
("Detected: " + gpsModuleToString(activeGpsModule())).c_str(),
"Check node profile");
logf("GPS module mismatch: expected=%s detected=%s",
gpsModuleToString(kExpectedGpsModule).c_str(),
gpsModuleToString(activeGpsModule()).c_str());
return;
}
uint8_t satsUsed = displayedSatsUsed();
uint8_t satsView = displayedSatsInView();
if (displayHasFreshUtc())
{
char line2[40];
char line3[28];
char line4[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), "Used:%u View:%u", (unsigned)satsUsed, (unsigned)satsView);
snprintf(line4, sizeof(line4), "Source: %s", gpsModuleToString(activeGpsModule()).c_str());
oledShowLines("GPS time (UTC)", line2, line3, line4);
logf("GPS time (UTC): %s used=%u view=%u", line2, (unsigned)satsUsed, (unsigned)satsView);
return;
}
String rtc = formatRtcNow();
if (satsUsed > 0 || satsView > 0)
{
char line2[28];
char line3[28];
snprintf(line2, sizeof(line2), "Used: %u", (unsigned)satsUsed);
snprintf(line3, sizeof(line3), "View: %u", (unsigned)satsView);
oledShowLines("GPS signal detected", line2, line3, "GPS UTC not ready", rtc.c_str());
logf("Satellites detected (used=%u view=%u) but GPS UTC not ready. %s",
(unsigned)satsUsed,
(unsigned)satsView,
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());
}
}
static bool shouldRefreshDisplay()
{
uint32_t now = millis();
if (g_lastDisplayRefreshMs != 0 && (uint32_t)(now - g_lastDisplayRefreshMs) < kDisplayRefreshMinMs)
{
return false;
}
uint8_t satsUsed = displayedSatsUsed();
uint8_t satsView = displayedSatsInView();
bool hasUtc = displayHasFreshUtc();
if (!g_haveLastDrawnState)
{
return true;
}
return satsUsed != g_lastDrawnSatsUsed ||
satsView != g_lastDrawnSatsView ||
hasUtc != g_lastDrawnValidUtc;
}
static void markDisplayStateDrawn()
{
g_lastDrawnSatsUsed = displayedSatsUsed();
g_lastDrawnSatsView = displayedSatsInView();
g_lastDrawnValidUtc = displayHasFreshUtc();
g_haveLastDrawnState = true;
g_lastDisplayRefreshMs = millis();
}
void setup()
{
Serial.begin(115200);
delay(kSerialDelayMs);
Serial.println("\r\n==================================================");
Serial.println("Exercise 09: GPS Time");
Serial.println("==================================================");
Serial.printf("Build: %s %s\r\n", kBuildDate, kBuildTime);
if (!tbeam_supreme::initPmuForPeripherals(g_pmu, &Serial))
{
logf("PMU init failed");
}
// SPI Flash File System ("SPIFFS") is used for logging GPS diagnostics,
// which may be helpful for analyzing GPS behavior in different
// environments and over time.
g_spiffsReady = SPIFFS.begin(true);
if (!g_spiffsReady)
{
logf("SPIFFS mount failed");
}
else if (!ensureGpsLogDirectory())
{
logf("GPS log directory create/open failed");
}
else
{
File file = SPIFFS.open(kGpsLogPath, FILE_WRITE);
if (file)
{
file.println("Exercise 09 GPS diagnostics");
file.printf("Build: %s %s\r\n", kBuildDate, kBuildTime);
file.close();
}
else
{
logf("GPS log file open failed: %s", kGpsLogPath);
}
}
// Initialize the OLED display and show a boot message with build info.
Wire.begin(OLED_SDA, OLED_SCL);
g_oled.setI2CAddress(OLED_ADDR << 1);
g_oled.begin();
String buildStamp = buildStampShort();
oledShowLines("09_GPS_Time", buildStamp.c_str(), "Booting...");
// The GPS startup probe may take a while,
// especially for a cold start. Log some
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, GPS_RX_PIN, GPS_TX_PIN);
logf("GPS UART started: RX=%d TX=%d baud=%lu", g_gpsRxPin, g_gpsTxPin, (unsigned long)g_gpsBaud);
appendGpsSnapshot("uart_started");
initialGpsProbe();
appendGpsSnapshot("startup_probe_complete");
maybeConfigureUblox();
oledShowLines("GPS startup probe", "Checking satellites", "and GPS time...");
uint32_t probeWindowMs = startupProbeWindowMs();
if (kExpectedGpsModule == GpsModuleKind::UBLOX)
{
logf("UBLOX startup window: %lu ms (allowing cold start acquisition)",
(unsigned long)probeWindowMs);
}
uint32_t probeStart = millis();
uint32_t lastProbeUiMs = 0;
while ((uint32_t)(millis() - probeStart) < probeWindowMs)
{
pollSerialConsole();
pollGpsSerial();
g_sd.update();
uint32_t now = millis();
if ((uint32_t)(now - g_lastGpsDiagnosticLogMs) >= kGpsDiagnosticLogMs)
{
g_lastGpsDiagnosticLogMs = now;
appendGpsSnapshot("startup_wait");
}
if ((uint32_t)(now - lastProbeUiMs) >= 1000)
{
lastProbeUiMs = now;
char l3[28];
char l4[30];
char l5[24];
snprintf(l3, sizeof(l3), "Used:%u View:%u", (unsigned)displayedSatsUsed(), (unsigned)displayedSatsInView());
snprintf(l4, sizeof(l4), "Module: %s", gpsModuleToString(activeGpsModule()).c_str());
snprintf(l5, sizeof(l5), "NMEA:%s %d/%d", g_gps.sawAnySentence ? "yes" : "no", g_gpsRxPin, g_gpsTxPin);
oledShowLines("GPS startup probe", "Checking satellites", l3, l4, l5);
}
delay(10);
}
reportStatusToSerial();
g_prevHadSatellites = (displayedSatsUsed() > 0 || displayedSatsInView() > 0);
g_prevHadValidUtc = displayHasFreshUtc();
drawMinuteStatus();
markDisplayStateDrawn();
g_lastMinuteReportMs = millis();
g_lastGpsDiagnosticLogMs = millis();
}
void loop()
{
pollSerialConsole();
pollGpsSerial();
g_sd.update();
maybeAnnounceGpsTransitions();
uint32_t now = millis();
if (shouldRefreshDisplay())
{
drawMinuteStatus();
markDisplayStateDrawn();
}
if ((uint32_t)(now - g_lastGpsDiagnosticLogMs) >= kGpsDiagnosticLogMs)
{
g_lastGpsDiagnosticLogMs = now;
appendGpsSnapshot("periodic");
}
if ((uint32_t)(now - g_lastMinuteReportMs) >= kMinuteMs)
{
g_lastMinuteReportMs = now;
drawMinuteStatus();
markDisplayStateDrawn();
appendGpsSnapshot("minute_status");
}
}