1488 lines
36 KiB
C++
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");
|
|
}
|
|
}
|