had OLED sleep ability and better SD card mount monitoring and ring buffer for error messages

This commit is contained in:
John Poole 2026-04-07 20:48:12 -07:00
commit 15a5dbe006
8 changed files with 223 additions and 40 deletions

9
.gitignore vendored
View file

@ -18,3 +18,12 @@
.platformio_local/
.codex
exercises/09_GPS_Time/Doxyfile
exercises/09_GPS_Time/Doxyfile_ORIG
exercises/09_GPS_Time/html/
exercises/18_GPS_Field_QA/lib/field_qa/BatteryMonitor.cpp
exercises/18_GPS_Field_QA/lib/field_qa/BatteryMonitor.h
exercises/AMY.log
exercises/AMY_purged.log

View file

@ -46,6 +46,10 @@
#define FW_BUILD_UTC unknown
#endif
#ifndef LOG_AP_IP_OCTET
#define LOG_AP_IP_OCTET 23
#endif
#define FIELD_QA_STR_INNER(x) #x
#define FIELD_QA_STR(x) FIELD_QA_STR_INNER(x)
@ -59,7 +63,7 @@ static constexpr const char* kStorageName = "SD";
static constexpr const char* kLogDir = "/logs";
static constexpr const char* kLogApPrefix = "GPSQA-";
static constexpr const char* kLogApPassword = "";
static constexpr uint8_t kLogApIpOctet = 23;
static constexpr uint8_t kLogApIpOctet = LOG_AP_IP_OCTET;
static constexpr uint32_t kSerialDelayMs = 4000;
static constexpr uint32_t kSamplePeriodMs = 1000;
static constexpr uint32_t kLogFlushPeriodMs = 10000;

View file

@ -21,6 +21,8 @@ void DisplayManager::begin() {
Wire.begin(OLED_SDA, OLED_SCL);
m_oled.setI2CAddress(OLED_ADDR << 1);
m_oled.begin();
m_oled.setPowerSave(0);
m_powerSave = false;
}
void DisplayManager::drawLines(const char* l1,
@ -40,15 +42,24 @@ void DisplayManager::drawLines(const char* l1,
m_oled.sendBuffer();
}
void DisplayManager::showBoot(const char* line2, const char* line3) {
drawLines(kExerciseName, kFirmwareVersion, line2, line3);
void DisplayManager::showBoot(const char* line2, const char* line3, const char* line4, const char* batteryText) {
char header[24];
if (batteryText && batteryText[0] != '\0') {
snprintf(header, sizeof(header), "%s %s", kExerciseName, batteryText);
drawLines(header, kFirmwareVersion, line2, line3, line4);
} else {
drawLines(kExerciseName, kFirmwareVersion, line2, line3, line4);
}
}
void DisplayManager::showError(const char* line1, const char* line2) {
drawLines(kExerciseName, "ERROR", line1, line2);
}
void DisplayManager::showSample(const GnssSample& sample, const RunStats& stats, bool recording) {
void DisplayManager::showSample(const GnssSample& sample,
const RunStats& stats,
bool recording,
const char* batteryText) {
char l1[24];
char l2[20];
char l3[20];
@ -56,7 +67,11 @@ void DisplayManager::showSample(const GnssSample& sample, const RunStats& stats,
char l5[20];
char l6[20];
snprintf(l1, sizeof(l1), "%s", recording ? "*RECORDING" : "Halted");
snprintf(l1,
sizeof(l1),
"%s %s",
recording ? "*RECORDING" : "Halted",
(batteryText && batteryText[0] != '\0') ? batteryText : "--.-v");
snprintf(l2, sizeof(l2), "FIX: %s", fixTypeToString(sample.fixType));
snprintf(l3, sizeof(l3), "USED: %d/%d", sample.satsUsed < 0 ? 0 : sample.satsUsed, sample.satsInView < 0 ? 0 : sample.satsInView);
if (sample.validHdop) {
@ -69,4 +84,16 @@ void DisplayManager::showSample(const GnssSample& sample, const RunStats& stats,
drawLines(l1, l2, l3, l4, l5, l6);
}
void DisplayManager::setPowerSave(bool enabled) {
if (m_powerSave == enabled) {
return;
}
m_oled.setPowerSave(enabled ? 1 : 0);
m_powerSave = enabled;
}
bool DisplayManager::powerSave() const {
return m_powerSave;
}
} // namespace field_qa

View file

@ -10,9 +10,14 @@ namespace field_qa {
class DisplayManager {
public:
void begin();
void showBoot(const char* line2, const char* line3 = nullptr);
void showBoot(const char* line2,
const char* line3 = nullptr,
const char* line4 = nullptr,
const char* batteryText = nullptr);
void showError(const char* line1, const char* line2 = nullptr);
void showSample(const GnssSample& sample, const RunStats& stats, bool recording);
void showSample(const GnssSample& sample, const RunStats& stats, bool recording, const char* batteryText);
void setPowerSave(bool enabled);
bool powerSave() const;
private:
void drawLines(const char* l1,
@ -23,6 +28,7 @@ class DisplayManager {
const char* l6 = nullptr);
U8G2_SH1106_128X64_NONAME_F_HW_I2C m_oled{U8G2_R0, U8X8_PIN_NONE};
bool m_powerSave = false;
};
} // namespace field_qa

View file

@ -265,13 +265,14 @@ bool StorageManager::appendLine(const String& line) {
return appendBytes(record.c_str(), record.length());
}
void StorageManager::appendSampleCsv(const GnssSample& sample,
bool StorageManager::appendSampleCsv(const GnssSample& sample,
uint32_t sampleSeq,
uint32_t msSinceRunStart,
const char* runId,
const char* bootTimestampUtc) {
if (!m_file) {
return;
m_lastError = "log file not open";
return false;
}
if (m_file.size() == 0) {
writeHeader(runId, bootTimestampUtc);
@ -350,10 +351,10 @@ void StorageManager::appendSampleCsv(const GnssSample& sample,
line += kLogFieldDelimiter;
line += String(sample.longestNoFixMs);
line += ",,,,,,,";
(void)appendLine(line);
return appendLine(line);
}
void StorageManager::appendSatelliteCsv(const GnssSample& sample,
bool StorageManager::appendSatelliteCsv(const GnssSample& sample,
uint32_t sampleSeq,
uint32_t msSinceRunStart,
const SatelliteInfo* satellites,
@ -361,7 +362,10 @@ void StorageManager::appendSatelliteCsv(const GnssSample& sample,
const char* runId,
const char* bootTimestampUtc) {
if (!satellites || satelliteCount == 0 || !m_file) {
return;
if (!m_file) {
m_lastError = "log file not open";
}
return false;
}
if (m_file.size() == 0) {
writeHeader(runId, bootTimestampUtc);
@ -458,26 +462,30 @@ void StorageManager::appendSatelliteCsv(const GnssSample& sample,
line += String(sat.snr);
line += kLogFieldDelimiter;
line += sat.usedInSolution ? "1" : "0";
(void)appendLine(line);
if (!appendLine(line)) {
return false;
}
}
return true;
}
void StorageManager::flush() {
bool StorageManager::flush() {
if (!m_file) {
return;
return false;
}
if (m_bufferLengths[m_activeBuffer] > 0) {
m_bufferPending[m_activeBuffer] = true;
}
if (!writePendingBuffer()) {
return;
return false;
}
m_file.flush();
return true;
}
void StorageManager::close() {
flush();
(void)flush();
if (m_file) {
m_file.close();
}

View file

@ -17,19 +17,19 @@ class StorageManager {
bool fileOpen() const;
size_t bufferedBytes() const;
size_t logFileCount() const;
void appendSampleCsv(const GnssSample& sample,
bool appendSampleCsv(const GnssSample& sample,
uint32_t sampleSeq,
uint32_t msSinceRunStart,
const char* runId,
const char* bootTimestampUtc);
void appendSatelliteCsv(const GnssSample& sample,
bool appendSatelliteCsv(const GnssSample& sample,
uint32_t sampleSeq,
uint32_t msSinceRunStart,
const SatelliteInfo* satellites,
size_t satelliteCount,
const char* runId,
const char* bootTimestampUtc);
void flush();
bool flush();
void close();
void listFiles(Stream& out);
void catFile(Stream& out, const char* path);

View file

@ -40,6 +40,7 @@ build_flags =
-D NODE_LABEL=\"Amy\"
-D NODE_SHORT=\"A\"
-D NODE_SLOT_INDEX=0
-D LOG_AP_IP_OCTET=23
-D GNSS_CHIP_NAME=\"L76K\"
[env:bob]
@ -50,6 +51,7 @@ build_flags =
-D NODE_LABEL=\"Bob\"
-D NODE_SHORT=\"B\"
-D NODE_SLOT_INDEX=1
-D LOG_AP_IP_OCTET=24
-D GNSS_CHIP_NAME=\"L76K\"
[env:cy]
@ -60,6 +62,7 @@ build_flags =
-D NODE_LABEL=\"Cy\"
-D NODE_SHORT=\"C\"
-D NODE_SLOT_INDEX=2
-D LOG_AP_IP_OCTET=25
-D GNSS_CHIP_NAME=\"L76K\"
[env:dan]
@ -70,6 +73,7 @@ build_flags =
-D NODE_LABEL=\"Dan\"
-D NODE_SHORT=\"D\"
-D NODE_SLOT_INDEX=3
-D LOG_AP_IP_OCTET=26
-D GNSS_CHIP_NAME=\"L76K\"
[env:ed]
@ -80,6 +84,7 @@ build_flags =
-D NODE_LABEL=\"Ed\"
-D NODE_SHORT=\"E\"
-D NODE_SLOT_INDEX=4
-D LOG_AP_IP_OCTET=27
-D GNSS_CHIP_NAME=\"L76K\"
[env:flo]
@ -90,6 +95,7 @@ build_flags =
-D NODE_LABEL=\"Flo\"
-D NODE_SHORT=\"F\"
-D NODE_SLOT_INDEX=5
-D LOG_AP_IP_OCTET=28
-D GNSS_CHIP_NAME=\"L76K\"
[env:guy]
@ -100,5 +106,6 @@ build_flags =
-D NODE_LABEL=\"Guy\"
-D NODE_SHORT=\"G\"
-D NODE_SLOT_INDEX=6
-D LOG_AP_IP_OCTET=29
-D GNSS_CHIP_NAME=\"MAX-M10S\"
-D GPS_UBLOX

View file

@ -5,11 +5,13 @@
#include <ctype.h>
#include <SPI.h>
#include <SD.h>
#include <stdarg.h>
#include <strings.h>
#include <Wire.h>
#include <WiFi.h>
#include <WebServer.h>
#include "BatteryMonitor.h"
#include "ClockDiscipline.h"
#include "Config.h"
#include "DisplayManager.h"
@ -27,6 +29,7 @@ XPowersLibInterface* g_pmu = nullptr;
DisplayManager g_display;
GnssManager g_gnss;
ClockDiscipline g_clock;
BatteryMonitor g_battery;
StorageManager g_storage;
RunStats g_stats;
StartupSdManager g_sd(Serial);
@ -53,6 +56,9 @@ bool g_buttonHoldHandled = false;
uint32_t g_buttonPressedMs = 0;
uint32_t g_buttonConfirmDeadlineMs = 0;
uint32_t g_buttonStopMessageUntilMs = 0;
char g_lastHaltReason[32] = "none";
bool g_displayAutoSleepEnabled = false;
uint32_t g_displayWakeUntilMs = 0;
uint32_t g_lastSampleMs = 0;
uint32_t g_lastFlushMs = 0;
@ -64,11 +70,66 @@ volatile uint32_t g_ppsEdgeCount = 0;
static constexpr uint32_t kButtonHoldPromptMs = 1500;
static constexpr uint32_t kButtonConfirmWindowMs = 3000;
static constexpr uint32_t kButtonStopMessageMs = 4000;
static constexpr uint32_t kDisplayWakeMs = 120000;
static constexpr size_t kEventLogCapacity = 128;
static constexpr size_t kEventLogLineBytes = 96;
char g_eventLog[kEventLogCapacity][kEventLogLineBytes] = {};
size_t g_eventLogHead = 0;
size_t g_eventLogCount = 0;
void IRAM_ATTR onPpsEdge() {
++g_ppsEdgeCount;
}
String htmlEscape(const String& in);
void recordEvent(const char* fmt, ...) {
char msg[72];
va_list args;
va_start(args, fmt);
vsnprintf(msg, sizeof(msg), fmt, args);
va_end(args);
char line[kEventLogLineBytes];
snprintf(line, sizeof(line), "%10lu %s", (unsigned long)millis(), msg);
strlcpy(g_eventLog[g_eventLogHead], line, sizeof(g_eventLog[g_eventLogHead]));
g_eventLogHead = (g_eventLogHead + 1) % kEventLogCapacity;
if (g_eventLogCount < kEventLogCapacity) {
++g_eventLogCount;
}
Serial.printf("EVENT: %s\n", msg);
}
void setLastHaltReason(const char* reason) {
strlcpy(g_lastHaltReason, (reason && reason[0] != '\0') ? reason : "none", sizeof(g_lastHaltReason));
}
void wakeDisplay(uint32_t durationMs = kDisplayWakeMs) {
g_display.setPowerSave(false);
g_displayWakeUntilMs = millis() + durationMs;
}
void updateDisplayPowerState() {
if (!g_displayAutoSleepEnabled) {
g_display.setPowerSave(false);
return;
}
if ((int32_t)(millis() - g_displayWakeUntilMs) >= 0) {
g_display.setPowerSave(true);
}
}
void appendRecentEventsHtml(String& html) {
html += "<h2>Recent Events</h2><pre>";
const size_t start = (g_eventLogCount < kEventLogCapacity) ? 0 : g_eventLogHead;
for (size_t i = 0; i < g_eventLogCount; ++i) {
const size_t idx = (start + i) % kEventLogCapacity;
html += htmlEscape(String(g_eventLog[idx]));
html += "\n";
}
html += "</pre>";
}
String htmlEscape(const String& in) {
String out;
out.reserve(in.length() + 16);
@ -151,6 +212,8 @@ void printSummary() {
Serial.printf("storage_used_bytes=%u\n", g_storageMounted ? (unsigned)SD.usedBytes() : 0U);
Serial.printf("storage_buffered_bytes=%u\n", (unsigned)g_storage.bufferedBytes());
Serial.printf("storage_log_count=%u\n", (unsigned)g_logFileCount);
Serial.printf("halt_reason=%s\n", g_lastHaltReason);
Serial.printf("battery_voltage=%s\n", g_battery.voltageText());
Serial.printf("web_ready=%s\n", g_webReady ? "yes" : "no");
Serial.printf("web_url=http://192.168.%u.1/\n", (unsigned)kLogApIpOctet);
}
@ -207,7 +270,11 @@ bool ensureStorageReady() {
g_sampleSeq = 0;
g_runStartMs = millis();
g_loggingEnabled = true;
setLastHaltReason("none");
g_displayAutoSleepEnabled = true;
wakeDisplay();
g_logFileCount = g_storage.logFileCount();
recordEvent("logging_started path=%s", g_storage.currentPath());
Serial.printf("logging started: %s\n", g_storage.currentPath());
return true;
}
@ -216,10 +283,14 @@ void stopLoggingCleanly(const char* reason) {
if (!g_loggingEnabled && !g_storageReady) {
return;
}
g_storage.flush();
(void)g_storage.flush();
g_storage.close();
g_storageReady = false;
g_loggingEnabled = false;
setLastHaltReason(reason);
g_displayAutoSleepEnabled = true;
wakeDisplay();
recordEvent("logging_stopped reason=%s", g_lastHaltReason);
if (reason && reason[0] != '\0') {
Serial.printf("logging stopped: %s\n", reason);
} else {
@ -228,16 +299,36 @@ void stopLoggingCleanly(const char* reason) {
g_buttonStopMessageUntilMs = millis() + kButtonStopMessageMs;
}
void stopLoggingForStorageFailure(const char* phase) {
g_storageReady = false;
g_loggingEnabled = false;
setLastHaltReason("sd_write_failed");
g_displayAutoSleepEnabled = true;
wakeDisplay();
recordEvent("sd_write_failed phase=%s err=%s",
phase ? phase : "unknown",
g_storage.lastError()[0] ? g_storage.lastError() : "none");
Serial.printf("ERROR: sd_write_failed phase=%s err=%s\n",
phase ? phase : "unknown",
g_storage.lastError()[0] ? g_storage.lastError() : "none");
g_buttonStopMessageUntilMs = millis() + kButtonStopMessageMs;
}
bool rescanSdCard() {
g_storage.flush();
recordEvent("sd_rescan requested");
(void)g_storage.flush();
g_storage.close();
g_storageReady = false;
g_loggingEnabled = false;
const bool mounted = g_sd.forceRemount();
g_storageMounted = g_sd.isMounted();
wakeDisplay();
if (mounted) {
recordEvent("sd_rescan mounted");
g_sd.printCardInfo();
(void)ensureStorageReady();
} else {
recordEvent("sd_rescan failed");
}
return mounted;
}
@ -246,6 +337,10 @@ void pollStopButton() {
const uint32_t now = millis();
const bool pressed = (digitalRead(BUTTON_PIN) == LOW);
if (pressed) {
wakeDisplay();
}
if (g_buttonConfirmActive && (int32_t)(now - g_buttonConfirmDeadlineMs) >= 0) {
g_buttonConfirmActive = false;
}
@ -254,7 +349,7 @@ void pollStopButton() {
g_buttonPressedMs = now;
g_buttonHoldHandled = false;
if (g_buttonConfirmActive && g_loggingEnabled) {
stopLoggingCleanly("button confirm");
stopLoggingCleanly("button_confirm");
g_buttonConfirmActive = false;
}
} else if (pressed && !g_buttonHoldHandled && !g_buttonConfirmActive && g_loggingEnabled &&
@ -262,7 +357,8 @@ void pollStopButton() {
g_buttonHoldHandled = true;
g_buttonConfirmActive = true;
g_buttonConfirmDeadlineMs = now + kButtonConfirmWindowMs;
g_display.showBoot("Stop recording?", "Press again in 3s");
recordEvent("button_prompt stop_recording");
g_display.showBoot("Stop recording?", "Press again in 3s", nullptr, g_battery.voltageText());
}
if (!pressed && g_buttonPrevPressed) {
@ -276,11 +372,14 @@ void handleSdStateTransitions() {
g_sd.update();
if (g_sd.consumeMountedEvent()) {
g_storageMounted = true;
recordEvent("sd_mounted");
Serial.println("SD mounted");
g_sd.printCardInfo();
(void)ensureStorageReady();
}
if (g_sd.consumeRemovedEvent()) {
setLastHaltReason("sd_removed");
recordEvent("sd_removed");
Serial.println("SD removed");
g_storageMounted = false;
g_storage.close();
@ -314,6 +413,9 @@ void attemptClockDiscipline(const GnssSample& sample) {
setRunIdentityFromClock(disciplinedUtc);
g_clockDisciplined = true;
g_displayAutoSleepEnabled = true;
wakeDisplay();
recordEvent("clock_disciplined run_id=%s", g_runId);
Serial.printf("RTC disciplined to GPS: %s", g_bootTimestampUtc);
if (hadPriorRtc) {
Serial.printf(" drift=%+llds", (long long)driftSeconds);
@ -324,6 +426,7 @@ void attemptClockDiscipline(const GnssSample& sample) {
}
void sampleAndMaybeLog() {
g_battery.update(millis());
GnssSample sample = g_gnss.makeSample();
g_stats.updateFromSample(sample, millis());
sample.ttffMs = g_stats.ttffMs();
@ -336,8 +439,12 @@ void sampleAndMaybeLog() {
const uint32_t msSinceRunStart = millis() - g_runStartMs;
SatelliteInfo sats[kMaxSatellites];
const size_t satCount = g_gnss.copySatellites(sats, kMaxSatellites);
g_storage.appendSampleCsv(sample, sampleSeq, msSinceRunStart, g_runId, g_bootTimestampUtc);
g_storage.appendSatelliteCsv(sample, sampleSeq, msSinceRunStart, sats, satCount, g_runId, g_bootTimestampUtc);
if (!g_storage.appendSampleCsv(sample, sampleSeq, msSinceRunStart, g_runId, g_bootTimestampUtc)) {
stopLoggingForStorageFailure("append_sample");
} else if (satCount > 0 &&
!g_storage.appendSatelliteCsv(sample, sampleSeq, msSinceRunStart, sats, satCount, g_runId, g_bootTimestampUtc)) {
stopLoggingForStorageFailure("append_satellite");
}
}
if (g_periodicSerialEnabled && (uint32_t)(millis() - g_lastStatusMs) >= kStatusPeriodMs) {
@ -348,16 +455,20 @@ void sampleAndMaybeLog() {
g_lastDisplayMs = millis();
if (g_clockDisciplined) {
if (g_buttonConfirmActive) {
g_display.showBoot("Stop recording?", "Press again in 3s");
g_display.showBoot("Stop recording?", "Press again in 3s", nullptr, g_battery.voltageText());
} else if ((uint32_t)(millis() - g_buttonStopMessageUntilMs) < kButtonStopMessageMs) {
g_display.showBoot("Halted", "Safe to power off");
} else {
g_display.showSample(sample, g_stats, g_loggingEnabled);
g_display.showBoot("Halted", g_lastHaltReason, "Safe to power off", g_battery.voltageText());
} else if (!g_display.powerSave()) {
g_display.showSample(sample, g_stats, g_loggingEnabled, g_battery.voltageText());
}
} else {
g_display.showBoot("Waiting for GPS UTC", sample.validTime ? "Awaiting PPS" : "No valid time yet");
g_display.showBoot("Waiting for GPS UTC",
sample.validTime ? "Awaiting PPS" : "No valid time yet",
nullptr,
g_battery.voltageText());
}
}
updateDisplayPowerState();
}
void buildFileTreeHtml(String& html, const char* path) {
@ -430,7 +541,7 @@ bool normalizeWebPath(const String& input, String& out) {
}
void handleWebIndex() {
g_storage.flush();
(void)g_storage.flush();
String html;
html.reserve(8192);
html += "<!doctype html><html><head><meta charset='utf-8'><title>GPSQA ";
@ -453,6 +564,10 @@ void handleWebIndex() {
html += htmlEscape(String(g_storage.lastError()));
html += "<br>Current log: ";
html += htmlEscape(String(g_storage.currentPath()));
html += "<br>Halt reason: ";
html += htmlEscape(String(g_lastHaltReason));
html += "<br>Battery: ";
html += htmlEscape(String(g_battery.voltageText()));
html += "</p>";
html += "<p><a href='/cmd?status=1'>status</a> ";
html += "<a href='/cmd?flush=1'>flush</a> ";
@ -470,12 +585,13 @@ void handleWebIndex() {
html += "</ul><p>Web commands also accept query forms like ";
html += "<code>/cmd?erase=/logs/20260406_093912_CY.csv</code></p>";
appendRecentEventsHtml(html);
html += "</body></html>";
g_server.send(200, "text/html; charset=utf-8", html);
}
void handleWebDownload() {
g_storage.flush();
(void)g_storage.flush();
String pathArg = g_server.hasArg("path") ? g_server.arg("path") : g_server.arg("name");
String fullPath;
if (!normalizeWebPath(pathArg, fullPath)) {
@ -513,7 +629,7 @@ void handleWebCommand() {
String response;
if (g_server.hasArg("erase")) {
g_storage.flush();
(void)g_storage.flush();
const String path = g_server.arg("erase");
if (g_storage.eraseFile(path.c_str())) {
g_storageReady = g_storage.ready();
@ -526,7 +642,7 @@ void handleWebCommand() {
response = String("erase failed: ") + g_storage.lastError();
}
} else if (g_server.hasArg("erase_logs")) {
g_storage.flush();
(void)g_storage.flush();
g_storage.eraseLogs(Serial);
g_storageReady = g_storage.ready();
if (!g_storageReady) {
@ -535,8 +651,7 @@ void handleWebCommand() {
g_logFileCount = g_storage.logFileCount();
response = "logs erased";
} else if (g_server.hasArg("flush")) {
g_storage.flush();
response = "buffer flushed";
response = g_storage.flush() ? "buffer flushed" : String("flush failed: ") + g_storage.lastError();
} else if (g_server.hasArg("stop")) {
stopLoggingCleanly("web stop");
response = "logging stopped";
@ -566,6 +681,8 @@ void handleWebCommand() {
response += g_storageReady ? "yes" : "no";
response += "\nsd_state=";
response += g_storageMounted ? "mounted" : "absent";
response += "\nhalt_reason=";
response += g_lastHaltReason;
} else {
response = "commands: status flush start stop sd_rescan erase=<path> erase_logs=1";
}
@ -700,9 +817,11 @@ void setup() {
Serial.println("WARNING: PMU init failed");
}
g_battery.begin(g_pmu);
g_display.begin();
pinMode(BUTTON_PIN, INPUT_PULLUP);
g_display.showBoot("Booting...", kBoardId);
wakeDisplay();
g_display.showBoot("Booting...", kBoardId, nullptr, g_battery.voltageText());
g_stats.begin(millis());
g_gnss.begin();
(void)g_gnss.probeAtStartup(Serial);
@ -731,8 +850,9 @@ void setup() {
Serial.println("RTC invalid at boot");
}
recordEvent("boot board=%s", kBoardId);
printProvenance();
g_display.showBoot("Waiting for GPS UTC", "No log before RTC set");
g_display.showBoot("Waiting for GPS UTC", "No log before RTC set", nullptr, g_battery.voltageText());
g_lastSampleMs = millis();
g_lastFlushMs = millis();
@ -752,6 +872,8 @@ void loop() {
}
if (g_storageReady && (uint32_t)(now - g_lastFlushMs) >= kLogFlushPeriodMs) {
g_lastFlushMs = now;
g_storage.flush();
if (!g_storage.flush()) {
stopLoggingForStorageFailure("periodic_flush");
}
}
}