diff --git a/exercises/18_GPS_Field_QA/lib/field_qa/Config.h b/exercises/18_GPS_Field_QA/lib/field_qa/Config.h index 04232f9..e217cc8 100644 --- a/exercises/18_GPS_Field_QA/lib/field_qa/Config.h +++ b/exercises/18_GPS_Field_QA/lib/field_qa/Config.h @@ -84,6 +84,8 @@ static constexpr float kExcellentHdop = 1.5f; static constexpr size_t kBufferedSamples = 10; static constexpr size_t kMaxSatellites = 64; static constexpr size_t kStorageBufferBytes = 4096; +static constexpr uint8_t kStorageWriteRetryCount = 3; +static constexpr uint32_t kStorageWriteRetryDelayMs = 25; static constexpr uint32_t kClockDisciplineRetryMs = 5000; static constexpr uint32_t kClockPpsWaitTimeoutMs = 1500; static constexpr uint32_t kClockFreshSampleMs = 2000; diff --git a/exercises/18_GPS_Field_QA/lib/field_qa/StorageManager.cpp b/exercises/18_GPS_Field_QA/lib/field_qa/StorageManager.cpp index 8a4471a..33ffa17 100644 --- a/exercises/18_GPS_Field_QA/lib/field_qa/StorageManager.cpp +++ b/exercises/18_GPS_Field_QA/lib/field_qa/StorageManager.cpp @@ -178,6 +178,17 @@ bool StorageManager::openFile() { return true; } +bool StorageManager::reopenFileForAppend() { + if (m_file) { + m_file.close(); + } + m_file = SD.open(m_path.c_str(), FILE_WRITE); + if (!m_file) { + return false; + } + return true; +} + void StorageManager::writeHeader(const char* runId, const char* bootTimestampUtc) { if (!m_file || !m_newFile) { return; @@ -206,9 +217,7 @@ bool StorageManager::writePendingBuffer() { if (!m_bufferPending[i] || m_bufferLengths[i] == 0) { continue; } - const size_t wrote = m_file.write((const uint8_t*)m_buffers[i], m_bufferLengths[i]); - if (wrote != m_bufferLengths[i]) { - m_lastError = "SD.write failed"; + if (!writeFully((const uint8_t*)m_buffers[i], m_bufferLengths[i], "buffer")) { m_ready = false; return false; } @@ -226,9 +235,7 @@ bool StorageManager::appendBytes(const char* data, size_t len) { if (!writePendingBuffer()) { return false; } - const size_t wrote = m_file.write((const uint8_t*)data, len); - if (wrote != len) { - m_lastError = "SD.write large block failed"; + if (!writeFully((const uint8_t*)data, len, "large")) { m_ready = false; return false; } @@ -256,6 +263,57 @@ bool StorageManager::appendBytes(const char* data, size_t len) { return true; } +bool StorageManager::writeFully(const uint8_t* data, size_t len, const char* context) { + if (!m_file || !data || len == 0) { + m_lastError = "writeFully invalid state"; + return false; + } + + size_t totalWrote = 0; + for (uint8_t attempt = 0; attempt <= kStorageWriteRetryCount; ++attempt) { + const size_t filePos = (size_t)m_file.position(); + const size_t fileSize = (size_t)m_file.size(); + const size_t wrote = m_file.write(data + totalWrote, len - totalWrote); + if (wrote > 0) { + totalWrote += wrote; + if (totalWrote >= len) { + if (attempt > 0) { + m_lastError = ""; + } + return true; + } + } + + if (attempt >= kStorageWriteRetryCount) { + const bool stillMounted = mounted(); + char err[192]; + snprintf(err, + sizeof(err), + "SD.write failed ctx=%s wrote=%u/%u pos=%u size=%u mounted=%s attempts=%u", + context ? context : "?", + (unsigned)totalWrote, + (unsigned)len, + (unsigned)filePos, + (unsigned)fileSize, + stillMounted ? "yes" : "no", + (unsigned)(attempt + 1)); + m_lastError = err; + return false; + } + + delay(kStorageWriteRetryDelayMs); + + if (mounted()) { + if (!reopenFileForAppend()) { + delay(kStorageWriteRetryDelayMs); + } + } + } + + m_lastError = "SD.write retry loop exhausted"; + return false; +} + bool StorageManager::appendLine(const String& line) { if (line.endsWith("\n")) { return appendBytes(line.c_str(), line.length()); diff --git a/exercises/18_GPS_Field_QA/lib/field_qa/StorageManager.h b/exercises/18_GPS_Field_QA/lib/field_qa/StorageManager.h index 9645284..facff7c 100644 --- a/exercises/18_GPS_Field_QA/lib/field_qa/StorageManager.h +++ b/exercises/18_GPS_Field_QA/lib/field_qa/StorageManager.h @@ -40,11 +40,13 @@ class StorageManager { private: bool ensureDir(); bool openFile(); + bool reopenFileForAppend(); void writeHeader(const char* runId, const char* bootTimestampUtc); String makeFilePath(const char* runId) const; bool appendLine(const String& line); bool appendBytes(const char* data, size_t len); bool writePendingBuffer(); + bool writeFully(const uint8_t* data, size_t len, const char* context); size_t countLogsRecursive(const char* path) const; void listFilesRecursive(File& dir, Stream& out); void eraseLogsRecursive(File& dir); diff --git a/exercises/18_GPS_Field_QA/src/main.cpp b/exercises/18_GPS_Field_QA/src/main.cpp index 15a2c20..7f1f130 100644 --- a/exercises/18_GPS_Field_QA/src/main.cpp +++ b/exercises/18_GPS_Field_QA/src/main.cpp @@ -140,6 +140,25 @@ void setLastHaltReason(const char* reason) { strlcpy(g_lastHaltReason, (reason && reason[0] != '\0') ? reason : "none", sizeof(g_lastHaltReason)); } +const char* currentLoggingState() { + if (!g_clockDisciplined) { + return "waiting_clock"; + } + if (!g_storageMounted) { + return "sd_absent"; + } + if (g_loggingEnabled && g_storageReady) { + return "recording"; + } + if (g_lastHaltReason[0] != '\0' && strcmp(g_lastHaltReason, "none") != 0) { + return "halted"; + } + if (!g_storageReady) { + return "storage_not_ready"; + } + return "idle"; +} + void wakeDisplay(uint32_t durationMs = kDisplayWakeMs) { g_display.setPowerSave(false); g_displayWakeUntilMs = millis() + durationMs; @@ -248,6 +267,7 @@ 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("logging_state=%s\n", currentLoggingState()); 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"); @@ -495,6 +515,10 @@ void sampleAndMaybeLog() { 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", g_lastHaltReason, "Safe to power off", g_battery.voltageText()); + } else if (!g_storageMounted) { + g_display.showBoot("No logging", "SD not mounted", g_runId, g_battery.voltageText()); + } else if (!g_storageReady && !g_loggingEnabled) { + g_display.showBoot("No logging", "Storage not ready", g_runId, g_battery.voltageText()); } else if (!g_display.powerSave()) { g_display.showSample(sample, g_stats, g_loggingEnabled, g_battery.voltageText()); } @@ -601,6 +625,8 @@ void handleWebIndex() { html += htmlEscape(String(g_storage.lastError())); html += "
Current log: "; html += htmlEscape(String(g_storage.currentPath())); + html += "
Logging state: "; + html += htmlEscape(String(currentLoggingState())); html += "
Halt reason: "; html += htmlEscape(String(g_lastHaltReason)); html += "
Battery: "; @@ -718,6 +744,8 @@ void handleWebCommand() { response += g_storageReady ? "yes" : "no"; response += "\nsd_state="; response += g_storageMounted ? "mounted" : "absent"; + response += "\nlogging_state="; + response += currentLoggingState(); response += "\nhalt_reason="; response += g_lastHaltReason; } else { @@ -862,17 +890,28 @@ void setup() { g_stats.begin(millis()); g_gnss.begin(); (void)g_gnss.probeAtStartup(Serial); - startWebServer(); SdWatcherConfig sdCfg; + sdCfg.recoveryRailOffMs = 400; + sdCfg.recoveryRailOnSettleMs = 1200; + sdCfg.startupWarmupMs = 2500; if (!g_sd.begin(sdCfg)) { Serial.println("WARNING: SD watcher init failed"); } g_storageMounted = g_sd.isMounted(); + if (!g_storageMounted) { + Serial.println("INFO: cold-boot SD second-chance remount"); + delay(750); + if (g_sd.forceRemount()) { + g_storageMounted = true; + } + } if (g_storageMounted) { g_sd.printCardInfo(); } + startWebServer(); + #ifdef GPS_1PPS_PIN attachInterrupt(digitalPinToInterrupt(GPS_1PPS_PIN), onPpsEdge, RISING); #endif