#include "sdcard_task.h" #include "serial_task.h" #include static SPIClass vspi(VSPI); volatile bool sdLoggingActive = false; char currentLogFileName[64] = ""; static File logFile; static bool sdReady = false; static SemaphoreHandle_t sdMutex = NULL; 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); Serial.printf("[SD] Card mounted. Type: %d, Size: %lluMB\n", cardType, cardSize); vspi.setFrequency(20000000); if (!SD.exists(LOG_DIR)) { SD.mkdir(LOG_DIR); Serial.println("[SD] Created /logs directory"); } // Don't auto-start logging - user starts manually via web UI sdLoggingActive = false; Serial.println("[SD] Ready (logging OFF - start via web UI)"); } } xTaskCreatePinnedToCore(sdLoggingTask, "SDLog", TASK_STACK_SD_LOG, NULL, TASK_PRIORITY_SD_LOG, NULL, 0); } 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(); Serial.printf("[SD] New log file: %s\n", currentLogFileName); xSemaphoreGive(sdMutex); return true; } void sdLoggingTask(void *param) { Serial.println("[Task] SDLog started on core " + String(xPortGetCoreID())); TickType_t lastFlush = 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); int len = snprintf(csvLine, sizeof(csvLine), "\"%s\",\"%c\",\"", entry->timestamp, entry->direction == 'T' ? 'T' : 'R'); // Write data as plain string (no hex encoding) 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; } // Skip control chars (0x00~0x1F) silently } len += snprintf(csvLine + len, sizeof(csvLine) - len, "\"\r\n"); logFile.write((uint8_t*)csvLine, len); pendingWrites++; xSemaphoreGive(sdMutex); } vPortFree(entry); } 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(); } if (pendingWrites >= 50) { if (logFile) { xSemaphoreTake(sdMutex, portMAX_DELAY); logFile.flush(); xSemaphoreGive(sdMutex); } pendingWrites = 0; lastFlush = xTaskGetTickCount(); } } } void sdStopLogging() { sdLoggingActive = false; if (logFile) { xSemaphoreTake(sdMutex, portMAX_DELAY); logFile.flush(); logFile.close(); xSemaphoreGive(sdMutex); } 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"); } String sdGetFileList() { if (!sdReady) return "{\"files\":[]}"; xSemaphoreTake(sdMutex, portMAX_DELAY); 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()) { if (!first) json += ","; first = false; json += "{\"name\":\""; json += file.name(); 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 += "\"}"; } file = dir.openNextFile(); } dir.close(); } json += "]}"; xSemaphoreGive(sdMutex); return json; } 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); Serial.printf("[SD] Delete %s: %s\n", path, result ? "OK" : "FAIL"); xSemaphoreGive(sdMutex); return result; } bool sdCardPresent() { return sdReady; }