461 lines
13 KiB
C++
461 lines
13 KiB
C++
#include "sdcard_task.h"
|
|
#include "serial_task.h"
|
|
#include <time.h>
|
|
|
|
static SPIClass vspi(VSPI);
|
|
volatile bool sdLoggingActive = false;
|
|
volatile bool sdAutoStart = false;
|
|
char currentLogFileName[64] = "";
|
|
static File logFile;
|
|
static bool sdReady = false;
|
|
static SemaphoreHandle_t sdMutex = NULL;
|
|
|
|
// --- Persistent autostart flag ---
|
|
#define AUTOSTART_FLAG "/logs/.autostart"
|
|
|
|
// --- File rotation tracking ---
|
|
static int logStartDay = -1; // Day-of-year when current log started
|
|
static size_t logFileSize = 0; // Running byte count of current file
|
|
|
|
// Get current day-of-year (1~366)
|
|
static int getDayOfYear() {
|
|
time_t now;
|
|
time(&now);
|
|
struct tm tm;
|
|
localtime_r(&now, &tm);
|
|
return tm.tm_yday;
|
|
}
|
|
|
|
// Check if file rotation is needed
|
|
static bool needsRotation() {
|
|
// 1. Midnight check: day changed since file was created
|
|
if (LOG_ROTATE_MIDNIGHT && logStartDay >= 0) {
|
|
int today = getDayOfYear();
|
|
if (today != logStartDay) {
|
|
Serial.printf("[SD] Midnight rotation (day %d -> %d)\n", logStartDay, today);
|
|
return true;
|
|
}
|
|
}
|
|
// 2. Size check: file exceeds max size
|
|
if (logFileSize >= LOG_MAX_FILE_SIZE) {
|
|
Serial.printf("[SD] Size rotation (%.1fMB >= %.0fMB)\n",
|
|
logFileSize / (1024.0 * 1024.0),
|
|
LOG_MAX_FILE_SIZE / (1024.0 * 1024.0));
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// ============================================================
|
|
// Init
|
|
// ============================================================
|
|
void sdTaskInit() {
|
|
sdMutex = xSemaphoreCreateMutex();
|
|
|
|
vspi.begin(SD_VSPI_SCLK, SD_VSPI_MISO, SD_VSPI_MOSI, SD_VSPI_CS);
|
|
|
|
if (!SD.begin(SD_VSPI_CS, vspi, 4000000)) {
|
|
Serial.println("[SD] Card mount FAILED!");
|
|
sdReady = false;
|
|
} else {
|
|
uint8_t cardType = SD.cardType();
|
|
if (cardType == CARD_NONE) {
|
|
Serial.println("[SD] No card detected!");
|
|
sdReady = false;
|
|
} else {
|
|
sdReady = true;
|
|
uint64_t cardSize = SD.cardSize() / (1024 * 1024);
|
|
uint64_t usedSize = SD.usedBytes() / (1024 * 1024);
|
|
Serial.printf("[SD] Card: %lluMB total, %lluMB used, %lluMB free\n",
|
|
cardSize, usedSize, cardSize - usedSize);
|
|
vspi.setFrequency(20000000);
|
|
|
|
if (!SD.exists(LOG_DIR)) {
|
|
SD.mkdir(LOG_DIR);
|
|
Serial.println("[SD] Created /logs directory");
|
|
}
|
|
|
|
// Check persistent autostart flag
|
|
sdAutoStart = SD.exists(AUTOSTART_FLAG);
|
|
|
|
if (sdAutoStart) {
|
|
// Auto-start logging immediately (field deployment mode)
|
|
Serial.println("[SD] *** AUTOSTART MODE - starting logging immediately ***");
|
|
// sdCreateNewLogFile takes mutex, so don't hold it here
|
|
sdLoggingActive = false; // Will be set true after file creation
|
|
} else {
|
|
sdLoggingActive = false;
|
|
Serial.println("[SD] Ready (logging OFF - start via web UI)");
|
|
}
|
|
Serial.printf("[SD] Auto-rotate: midnight=%s, maxSize=%luMB\n",
|
|
LOG_ROTATE_MIDNIGHT ? "ON" : "OFF",
|
|
(unsigned long)(LOG_MAX_FILE_SIZE / (1024 * 1024)));
|
|
}
|
|
}
|
|
|
|
xTaskCreatePinnedToCore(sdLoggingTask, "SDLog", TASK_STACK_SD_LOG,
|
|
NULL, TASK_PRIORITY_SD_LOG, NULL, 0);
|
|
|
|
// Deferred autostart (after task is running)
|
|
if (sdReady && sdAutoStart) {
|
|
sdCreateNewLogFile();
|
|
sdLoggingActive = true;
|
|
Serial.printf("[SD] Autostart logging: %s\n", currentLogFileName);
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// Create new log file
|
|
// ============================================================
|
|
bool sdCreateNewLogFile() {
|
|
if (!sdReady) return false;
|
|
xSemaphoreTake(sdMutex, portMAX_DELAY);
|
|
|
|
if (logFile) { logFile.flush(); logFile.close(); }
|
|
|
|
struct tm timeinfo;
|
|
time_t now;
|
|
time(&now);
|
|
localtime_r(&now, &timeinfo);
|
|
|
|
snprintf(currentLogFileName, sizeof(currentLogFileName),
|
|
"%s/LOG_%04d%02d%02d_%02d%02d%02d.csv",
|
|
LOG_DIR,
|
|
timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday,
|
|
timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
|
|
|
|
logFile = SD.open(currentLogFileName, FILE_WRITE);
|
|
if (!logFile) {
|
|
Serial.printf("[SD] Failed to create: %s\n", currentLogFileName);
|
|
xSemaphoreGive(sdMutex);
|
|
return false;
|
|
}
|
|
logFile.print(CSV_HEADER);
|
|
logFile.flush();
|
|
|
|
// Reset rotation tracking
|
|
logStartDay = timeinfo.tm_yday;
|
|
logFileSize = strlen(CSV_HEADER);
|
|
|
|
Serial.printf("[SD] New log: %s (day=%d)\n", currentLogFileName, logStartDay);
|
|
xSemaphoreGive(sdMutex);
|
|
return true;
|
|
}
|
|
|
|
// ============================================================
|
|
// Auto-rotate: close current, open new (called inside mutex)
|
|
// ============================================================
|
|
static void rotateLogFile() {
|
|
// Close current
|
|
if (logFile) {
|
|
logFile.flush();
|
|
logFile.close();
|
|
Serial.printf("[SD] Rotated: %s (%.1fMB)\n",
|
|
currentLogFileName, logFileSize / (1024.0 * 1024.0));
|
|
}
|
|
|
|
// Create new file (releases and re-takes mutex internally)
|
|
// We need to release mutex first since sdCreateNewLogFile takes it
|
|
xSemaphoreGive(sdMutex);
|
|
sdCreateNewLogFile();
|
|
xSemaphoreTake(sdMutex, portMAX_DELAY);
|
|
}
|
|
|
|
// ============================================================
|
|
// SD Logging Task
|
|
// ============================================================
|
|
void sdLoggingTask(void *param) {
|
|
Serial.println("[Task] SDLog started on core " + String(xPortGetCoreID()));
|
|
|
|
TickType_t lastFlush = xTaskGetTickCount();
|
|
TickType_t lastRotateCheck = xTaskGetTickCount();
|
|
int pendingWrites = 0;
|
|
char csvLine[LOG_LINE_MAX_LEN + 64];
|
|
|
|
while (true) {
|
|
LogEntry *entry;
|
|
if (xQueueReceive(queueSD, &entry, pdMS_TO_TICKS(100)) == pdTRUE) {
|
|
if (sdLoggingActive && sdReady && logFile) {
|
|
xSemaphoreTake(sdMutex, portMAX_DELAY);
|
|
|
|
// Check rotation before writing
|
|
if (needsRotation()) {
|
|
rotateLogFile();
|
|
}
|
|
|
|
int len = snprintf(csvLine, sizeof(csvLine),
|
|
"\"%s\",\"%c\",\"", entry->timestamp,
|
|
entry->direction == 'T' ? 'T' : 'R');
|
|
|
|
for (int i = 0; i < entry->dataLen && len < (int)sizeof(csvLine) - 4; i++) {
|
|
char c = entry->data[i];
|
|
if (c == '"') { csvLine[len++] = '"'; csvLine[len++] = '"'; }
|
|
else if (c >= 0x20) { csvLine[len++] = c; }
|
|
}
|
|
len += snprintf(csvLine + len, sizeof(csvLine) - len, "\"\r\n");
|
|
|
|
if (logFile) {
|
|
logFile.write((uint8_t*)csvLine, len);
|
|
logFileSize += len;
|
|
pendingWrites++;
|
|
}
|
|
|
|
xSemaphoreGive(sdMutex);
|
|
}
|
|
vPortFree(entry);
|
|
}
|
|
|
|
// Periodic flush
|
|
if (pendingWrites > 0 &&
|
|
(xTaskGetTickCount() - lastFlush) > pdMS_TO_TICKS(SD_WRITE_INTERVAL)) {
|
|
if (logFile) {
|
|
xSemaphoreTake(sdMutex, portMAX_DELAY);
|
|
logFile.flush();
|
|
xSemaphoreGive(sdMutex);
|
|
}
|
|
pendingWrites = 0;
|
|
lastFlush = xTaskGetTickCount();
|
|
}
|
|
|
|
// Batch flush
|
|
if (pendingWrites >= 50) {
|
|
if (logFile) {
|
|
xSemaphoreTake(sdMutex, portMAX_DELAY);
|
|
logFile.flush();
|
|
xSemaphoreGive(sdMutex);
|
|
}
|
|
pendingWrites = 0;
|
|
lastFlush = xTaskGetTickCount();
|
|
}
|
|
|
|
// Periodic midnight check even when idle (every 30s)
|
|
if (sdLoggingActive && logFile &&
|
|
(xTaskGetTickCount() - lastRotateCheck) > pdMS_TO_TICKS(30000)) {
|
|
lastRotateCheck = xTaskGetTickCount();
|
|
if (needsRotation()) {
|
|
xSemaphoreTake(sdMutex, portMAX_DELAY);
|
|
rotateLogFile();
|
|
xSemaphoreGive(sdMutex);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// Stop / Start Logging
|
|
// ============================================================
|
|
void sdStopLogging() {
|
|
sdLoggingActive = false;
|
|
if (logFile) {
|
|
xSemaphoreTake(sdMutex, portMAX_DELAY);
|
|
logFile.flush();
|
|
logFile.close();
|
|
xSemaphoreGive(sdMutex);
|
|
}
|
|
logStartDay = -1;
|
|
logFileSize = 0;
|
|
Serial.println("[SD] Logging stopped");
|
|
}
|
|
|
|
void sdStartLogging() {
|
|
if (!sdReady) { Serial.println("[SD] Card not ready!"); return; }
|
|
if (!logFile || !sdLoggingActive) sdCreateNewLogFile();
|
|
sdLoggingActive = true;
|
|
Serial.println("[SD] Logging started");
|
|
}
|
|
|
|
// ============================================================
|
|
// Comments - stored in /logs/.comments (filename|comment per line)
|
|
// ============================================================
|
|
#define COMMENTS_FILE "/logs/.comments"
|
|
|
|
// Read all comments into a temp map, find one for given filename
|
|
String sdGetComment(const char *filename) {
|
|
if (!sdReady) return "";
|
|
// Note: caller should NOT hold sdMutex (or use recursive mutex)
|
|
File f = SD.open(COMMENTS_FILE, FILE_READ);
|
|
if (!f) return "";
|
|
|
|
String target = String(filename) + "|";
|
|
String line;
|
|
String result = "";
|
|
while (f.available()) {
|
|
line = f.readStringUntil('\n');
|
|
line.trim();
|
|
if (line.startsWith(target)) {
|
|
result = line.substring(target.length());
|
|
break;
|
|
}
|
|
}
|
|
f.close();
|
|
return result;
|
|
}
|
|
|
|
bool sdSetComment(const char *filename, const char *comment) {
|
|
if (!sdReady) return false;
|
|
|
|
// Read existing comments (skip target file)
|
|
String allLines = "";
|
|
File f = SD.open(COMMENTS_FILE, FILE_READ);
|
|
String target = String(filename) + "|";
|
|
if (f) {
|
|
while (f.available()) {
|
|
String line = f.readStringUntil('\n');
|
|
line.trim();
|
|
if (line.length() == 0) continue;
|
|
if (line.startsWith(target)) continue; // Skip old comment for this file
|
|
allLines += line + "\n";
|
|
}
|
|
f.close();
|
|
}
|
|
|
|
// Append new comment (if not empty)
|
|
if (comment && strlen(comment) > 0) {
|
|
allLines += String(filename) + "|" + String(comment) + "\n";
|
|
}
|
|
|
|
// Rewrite file
|
|
f = SD.open(COMMENTS_FILE, FILE_WRITE);
|
|
if (!f) return false;
|
|
f.print(allLines);
|
|
f.close();
|
|
return true;
|
|
}
|
|
|
|
// Remove comment when file is deleted
|
|
static void sdRemoveComment(const char *filename) {
|
|
sdSetComment(filename, ""); // Empty = remove
|
|
}
|
|
|
|
// ============================================================
|
|
// File List (includes comments)
|
|
// ============================================================
|
|
String sdGetFileList() {
|
|
if (!sdReady) return "{\"files\":[]}";
|
|
xSemaphoreTake(sdMutex, portMAX_DELAY);
|
|
|
|
// Pre-load all comments
|
|
String commentsRaw = "";
|
|
File cf = SD.open(COMMENTS_FILE, FILE_READ);
|
|
if (cf) {
|
|
commentsRaw = cf.readString();
|
|
cf.close();
|
|
}
|
|
|
|
String json = "{\"files\":[";
|
|
File dir = SD.open(LOG_DIR);
|
|
bool first = true;
|
|
|
|
if (dir && dir.isDirectory()) {
|
|
File file = dir.openNextFile();
|
|
while (file) {
|
|
if (!file.isDirectory()) {
|
|
String fname = file.name();
|
|
// Skip the .comments metadata file
|
|
if (fname == ".comments") { file = dir.openNextFile(); continue; }
|
|
|
|
if (!first) json += ",";
|
|
first = false;
|
|
json += "{\"name\":\"";
|
|
json += fname;
|
|
json += "\",\"size\":";
|
|
json += String(file.size());
|
|
time_t t = file.getLastWrite();
|
|
struct tm *tm = localtime(&t);
|
|
char timeBuf[32];
|
|
strftime(timeBuf, sizeof(timeBuf), "%Y-%m-%d %H:%M:%S", tm);
|
|
json += ",\"modified\":\"";
|
|
json += timeBuf;
|
|
json += "\"";
|
|
|
|
// Find comment for this file
|
|
String key = fname + "|";
|
|
int idx = commentsRaw.indexOf(key);
|
|
if (idx >= 0) {
|
|
int start = idx + key.length();
|
|
int end = commentsRaw.indexOf('\n', start);
|
|
if (end < 0) end = commentsRaw.length();
|
|
String cmt = commentsRaw.substring(start, end);
|
|
cmt.trim();
|
|
// Escape quotes in comment for JSON
|
|
cmt.replace("\\", "\\\\");
|
|
cmt.replace("\"", "\\\"");
|
|
json += ",\"comment\":\"";
|
|
json += cmt;
|
|
json += "\"";
|
|
}
|
|
|
|
json += "}";
|
|
}
|
|
file = dir.openNextFile();
|
|
}
|
|
dir.close();
|
|
}
|
|
|
|
uint64_t totalMB = SD.totalBytes() / (1024 * 1024);
|
|
uint64_t usedMB = SD.usedBytes() / (1024 * 1024);
|
|
json += "],\"totalMB\":";
|
|
json += String((uint32_t)totalMB);
|
|
json += ",\"usedMB\":";
|
|
json += String((uint32_t)usedMB);
|
|
json += "}";
|
|
|
|
xSemaphoreGive(sdMutex);
|
|
return json;
|
|
}
|
|
|
|
// ============================================================
|
|
// Delete File (also removes comment)
|
|
// ============================================================
|
|
bool sdDeleteFile(const char *filename) {
|
|
if (!sdReady) return false;
|
|
xSemaphoreTake(sdMutex, portMAX_DELAY);
|
|
|
|
char path[128];
|
|
snprintf(path, sizeof(path), "%s/%s", LOG_DIR, filename);
|
|
if (sdLoggingActive && strcmp(path, currentLogFileName) == 0) {
|
|
xSemaphoreGive(sdMutex);
|
|
return false;
|
|
}
|
|
bool result = SD.remove(path);
|
|
if (result) sdRemoveComment(filename);
|
|
Serial.printf("[SD] Delete %s: %s\n", path, result ? "OK" : "FAIL");
|
|
xSemaphoreGive(sdMutex);
|
|
return result;
|
|
}
|
|
|
|
bool sdCardPresent() { return sdReady; }
|
|
|
|
// ============================================================
|
|
// Autostart - persistent flag on SD card
|
|
// ============================================================
|
|
bool sdGetAutoStart() {
|
|
if (!sdReady) return false;
|
|
return SD.exists(AUTOSTART_FLAG);
|
|
}
|
|
|
|
void sdSetAutoStart(bool enable) {
|
|
if (!sdReady) return;
|
|
xSemaphoreTake(sdMutex, portMAX_DELAY);
|
|
|
|
if (enable) {
|
|
// Create flag file
|
|
File f = SD.open(AUTOSTART_FLAG, FILE_WRITE);
|
|
if (f) {
|
|
f.println("autostart=1");
|
|
f.close();
|
|
}
|
|
sdAutoStart = true;
|
|
Serial.println("[SD] Autostart ENABLED (will log on next boot)");
|
|
} else {
|
|
// Remove flag file
|
|
if (SD.exists(AUTOSTART_FLAG)) {
|
|
SD.remove(AUTOSTART_FLAG);
|
|
}
|
|
sdAutoStart = false;
|
|
Serial.println("[SD] Autostart DISABLED");
|
|
}
|
|
|
|
xSemaphoreGive(sdMutex);
|
|
}
|