#include "flight_logbook.h" #include "config.h" #include "aircraft.h" #include "aircraft_table.h" #include "settings_store.h" #include "sd_mutex.h" #include #include #include namespace FlightLogbook { namespace { constexpr uint16_t MAX_SEEN = 400; char seenHex[MAX_SEEN][7]; uint16_t seenCount = 0; char currentDateStr[11] = {0}; bool computeDateStr(char* out, size_t outSize) { time_t now = time(nullptr); if (now < 8 * 3600 * 2) return false; struct tm tmNow; localtime_r(&now, &tmNow); snprintf(out, outSize, "%04d-%02d-%02d", tmNow.tm_year + 1900, tmNow.tm_mon + 1, tmNow.tm_mday); return true; } void logFilename(char* out, size_t outSize) { snprintf(out, outSize, "%s/%s.csv", Config::SD_LOG_DIR, currentDateStr); } bool alreadySeen(const char* hex) { for (uint16_t i = 0; i < seenCount; i++) { if (strcmp(seenHex[i], hex) == 0) return true; } return false; } void markSeen(const char* hex) { if (seenCount >= MAX_SEEN) return; strncpy(seenHex[seenCount], hex, sizeof(seenHex[seenCount]) - 1); seenHex[seenCount][sizeof(seenHex[seenCount]) - 1] = 0; seenCount++; } uint32_t countLinesFast(File& f) { constexpr size_t BUF_SIZE = 1024; static uint8_t buf[BUF_SIZE]; uint32_t lines = 0; uint32_t blocksRead = 0; while (f.available()) { size_t n = f.read(buf, BUF_SIZE); for (size_t i = 0; i < n; i++) { if (buf[i] == '\n') lines++; } blocksRead++; if (blocksRead % 8 == 0) yield(); } return lines; } void loadSeenFromTodayFile() { seenCount = 0; char filename[64]; logFilename(filename, sizeof(filename)); if (!SD.exists(filename)) return; File f = SD.open(filename, FILE_READ); if (!f) return; constexpr size_t BUF_SIZE = 1024; static uint8_t buf[BUF_SIZE]; char lineBuf[48]; size_t lineLen = 0; bool firstLine = true; uint32_t blocksRead = 0; auto processLine = [&]() { if (lineLen == 0) return; if (firstLine) { firstLine = false; return; } lineBuf[lineLen] = 0; char* firstComma = strchr(lineBuf, ','); if (!firstComma) return; char* secondComma = strchr(firstComma + 1, ','); size_t hexLen = secondComma ? (size_t)(secondComma - (firstComma + 1)) : strlen(firstComma + 1); if (hexLen > 0 && hexLen < sizeof(seenHex[0])) { char hexBuf[7] = {0}; memcpy(hexBuf, firstComma + 1, hexLen); markSeen(hexBuf); } }; while (f.available() && seenCount < MAX_SEEN) { size_t n = f.read(buf, BUF_SIZE); for (size_t i = 0; i < n && seenCount < MAX_SEEN; i++) { char c = (char)buf[i]; if (c == '\n' || c == '\r') { if (lineLen > 0) processLine(); lineLen = 0; } else if (lineLen < sizeof(lineBuf) - 1) { lineBuf[lineLen++] = c; } } blocksRead++; if (blocksRead % 8 == 0) yield(); } f.close(); } void ensureCurrentDate() { char today[11]; if (!computeDateStr(today, sizeof(today))) return; if (strcmp(today, currentDateStr) != 0) { strncpy(currentDateStr, today, sizeof(currentDateStr) - 1); loadSeenFromTodayFile(); } } void writeLogLine(File& f, const Aircraft& a) { time_t now = time(nullptr); struct tm tmNow; localtime_r(&now, &tmNow); char timestamp[20]; snprintf(timestamp, sizeof(timestamp), "%04d-%02d-%02d %02d:%02d:%02d", tmNow.tm_year + 1900, tmNow.tm_mon + 1, tmNow.tm_mday, tmNow.tm_hour, tmNow.tm_min, tmNow.tm_sec); f.printf("%s,%s,%s,%s,%s,%.1f,%d\n", timestamp, a.hex, a.callsign[0] ? a.callsign : "", a.reg[0] ? a.reg : "", a.typeCode[0] ? a.typeCode : "", a.distanceKm, (int)a.altBaroFt); } } void init() { SdMutex::Guard guard; ensureCurrentDate(); } void update() { if (!SettingsStore::flightLogbookEnabled()) return; SdMutex::Guard guard; ensureCurrentDate(); if (currentDateStr[0] == 0) return; static Aircraft snapshot[Config::MAX_TRACKED_AIRCRAFT]; uint8_t count = 0; AircraftTable::lock(); Aircraft* table = AircraftTable::raw(); for (uint8_t i = 0; i < AircraftTable::capacity(); i++) { if (table[i].valid) snapshot[count++] = table[i]; } AircraftTable::unlock(); bool anyNew = false; for (uint8_t i = 0; i < count; i++) { if (snapshot[i].hex[0] && !alreadySeen(snapshot[i].hex)) { anyNew = true; break; } } if (!anyNew) return; char filename[64]; logFilename(filename, sizeof(filename)); bool needsHeader = !SD.exists(filename); yield(); File f = SD.open(filename, FILE_APPEND); if (!f) return; if (needsHeader) { f.println("timestamp,hex,callsign,reg,type,distance_km,altitude_ft"); } for (uint8_t i = 0; i < count; i++) { if (!snapshot[i].hex[0]) continue; if (alreadySeen(snapshot[i].hex)) continue; markSeen(snapshot[i].hex); writeLogLine(f, snapshot[i]); yield(); } f.close(); } uint16_t todayCount() { return seenCount; } void computeAllTimeStats(uint32_t& totalAircraft, uint16_t& totalDays) { totalAircraft = 0; totalDays = 0; SdMutex::Guard guard; File dir = SD.open(Config::SD_LOG_DIR); if (!dir || !dir.isDirectory()) return; File entry = dir.openNextFile(); while (entry) { if (!entry.isDirectory()) { String name = String(entry.name()); if (name.endsWith(".csv")) { totalDays++; uint32_t lines = countLinesFast(entry); if (lines > 0) totalAircraft += (lines - 1); } } entry.close(); entry = dir.openNextFile(); } dir.close(); } uint8_t listDays(DayEntry* out, uint8_t maxEntries) { uint8_t filled = 0; SdMutex::Guard guard; File dir = SD.open(Config::SD_LOG_DIR); if (!dir || !dir.isDirectory()) return 0; File entry = dir.openNextFile(); while (entry && filled < maxEntries) { if (!entry.isDirectory()) { String name = String(entry.name()); if (name.endsWith(".csv")) { String dateOnly = name.substring(0, name.length() - 4); int slashIdx = dateOnly.lastIndexOf('/'); if (slashIdx >= 0) dateOnly = dateOnly.substring(slashIdx + 1); uint32_t lines = countLinesFast(entry); strncpy(out[filled].date, dateOnly.c_str(), sizeof(out[filled].date) - 1); out[filled].count = (lines > 0) ? (lines - 1) : 0; filled++; } } entry.close(); entry = dir.openNextFile(); } dir.close(); return filled; } void resetAllData() { SdMutex::Guard guard; File dir = SD.open(Config::SD_LOG_DIR); if (dir && dir.isDirectory()) { File entry = dir.openNextFile(); while (entry) { bool isDir = entry.isDirectory(); String name = String(entry.name()); entry.close(); if (!isDir) { String fullPath = name.startsWith("/") ? name : String(Config::SD_LOG_DIR) + "/" + name; SD.remove(fullPath); } entry = dir.openNextFile(); } dir.close(); } seenCount = 0; currentDateStr[0] = 0; ensureCurrentDate(); } }