diff --git a/ESP32_CAN_Logger-a.ino b/ESP32_CAN_Logger-a.ino index a623672..516c59a 100644 --- a/ESP32_CAN_Logger-a.ino +++ b/ESP32_CAN_Logger-a.ino @@ -3368,6 +3368,7 @@ void setup() { // WebSocket 시작 webSocket.begin(); webSocket.onEvent(webSocketEvent); + webSocket.enableHeartbeat(15000, 3000, 2); // ping 15초, pong 3초, 재시도 2회 // ★★★ 웹 서버 라우팅 (중요!) ★★★ server.on("/", HTTP_GET, []() { @@ -3399,27 +3400,236 @@ void setup() { }); server.on("/download", HTTP_GET, []() { - if (server.hasArg("file")) { - String filename = "/" + server.arg("file"); - - if (SD_MMC.exists(filename)) { - File file = SD_MMC.open(filename, FILE_READ); - if (file) { - String displayName = server.arg("file"); - server.sendHeader("Content-Disposition", - "attachment; filename=\"" + displayName + "\""); - server.sendHeader("Content-Type", "application/octet-stream"); - server.streamFile(file, "application/octet-stream"); - file.close(); - } else { - server.send(500, "text/plain", "Failed to open file"); - } - } else { - server.send(404, "text/plain", "File not found"); - } - } else { + if (!server.hasArg("file")) { server.send(400, "text/plain", "Bad request"); + return; } + + String filename = "/" + server.arg("file"); + + Serial.println("\n========================================"); + Serial.println("📥 다운로드 준비"); + Serial.println("========================================"); + + // ⭐ 1단계: 모든 로깅 완전 중지 + bool wasLogging = loggingEnabled; + if (wasLogging) { + loggingEnabled = false; + delay(200); + Serial.println("⏸ 모든 로깅 중지"); + } + + // ⭐ 2단계: 모든 SD 관련 Task 중단 + if (sdWriteTaskHandle != NULL) { + vTaskSuspend(sdWriteTaskHandle); + delay(100); + Serial.println("⏸ SD 쓰기 Task 중단"); + } + if (webTaskHandle != NULL) { + vTaskSuspend(webTaskHandle); + delay(50); + Serial.println("⏸ 웹 업데이트 Task 중단"); + } + + // ⭐ 3단계: SD Mutex 획득 + if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(10000)) != pdTRUE) { + if (webTaskHandle != NULL) vTaskResume(webTaskHandle); + if (sdWriteTaskHandle != NULL) vTaskResume(sdWriteTaskHandle); + if (wasLogging) loggingEnabled = true; + server.send(503, "text/plain", "SD card busy"); + Serial.println("✗ SD Mutex 획득 실패"); + return; + } + + // ⭐ 4단계: SD 카드 재마운트 (1-bit 모드로) + Serial.println("🔄 SD 카드 재마운트 중..."); + SD_MMC.end(); + delay(200); + + // 1-bit 모드로 재시작 (더 안정적) + if (!SD_MMC.setPins(SDIO_CLK, SDIO_CMD, SDIO_D0)) { + Serial.println("✗ SD_MMC.setPins() 실패"); + xSemaphoreGive(sdMutex); + if (webTaskHandle != NULL) vTaskResume(webTaskHandle); + if (sdWriteTaskHandle != NULL) vTaskResume(sdWriteTaskHandle); + if (wasLogging) loggingEnabled = true; + server.send(500, "text/plain", "SD remount failed"); + return; + } + + if (!SD_MMC.begin("/sdcard", true)) { // true = 1-bit mode + Serial.println("✗ SD 카드 1-bit 재마운트 실패"); + xSemaphoreGive(sdMutex); + if (webTaskHandle != NULL) vTaskResume(webTaskHandle); + if (sdWriteTaskHandle != NULL) vTaskResume(sdWriteTaskHandle); + if (wasLogging) loggingEnabled = true; + server.send(500, "text/plain", "SD 1-bit mode failed"); + return; + } + + Serial.println("✓ SD 카드 1-bit 모드 활성화"); + delay(100); + + // 파일 존재 확인 + if (!SD_MMC.exists(filename)) { + Serial.println("✗ 파일 없음"); + + // 4-bit 모드로 복구 + SD_MMC.end(); + delay(100); + SD_MMC.setPins(SDIO_CLK, SDIO_CMD, SDIO_D0, SDIO_D1, SDIO_D2, SDIO_D3); + SD_MMC.begin("/sdcard", false); + + xSemaphoreGive(sdMutex); + if (webTaskHandle != NULL) vTaskResume(webTaskHandle); + if (sdWriteTaskHandle != NULL) vTaskResume(sdWriteTaskHandle); + if (wasLogging) loggingEnabled = true; + server.send(404, "text/plain", "File not found"); + return; + } + + File file = SD_MMC.open(filename, FILE_READ); + if (!file) { + Serial.println("✗ 파일 열기 실패"); + + // 4-bit 모드로 복구 + SD_MMC.end(); + delay(100); + SD_MMC.setPins(SDIO_CLK, SDIO_CMD, SDIO_D0, SDIO_D1, SDIO_D2, SDIO_D3); + SD_MMC.begin("/sdcard", false); + + xSemaphoreGive(sdMutex); + if (webTaskHandle != NULL) vTaskResume(webTaskHandle); + if (sdWriteTaskHandle != NULL) vTaskResume(sdWriteTaskHandle); + if (wasLogging) loggingEnabled = true; + server.send(500, "text/plain", "Failed to open file"); + return; + } + + size_t fileSize = file.size(); + String displayName = server.arg("file"); + + Serial.printf("📥 다운로드 시작: %s (%u bytes)\n", displayName.c_str(), fileSize); + + // 헤더 전송 + server.setContentLength(fileSize); + server.sendHeader("Content-Disposition", "attachment; filename=\"" + displayName + "\""); + server.sendHeader("Content-Type", "application/octet-stream"); + server.sendHeader("Connection", "close"); + server.send(200, "application/octet-stream", ""); + + // 512바이트 섹터 단위 전송 + const size_t CHUNK_SIZE = 512; + uint8_t *buffer = (uint8_t*)heap_caps_aligned_alloc(32, CHUNK_SIZE, MALLOC_CAP_DMA); + + if (!buffer) { + buffer = (uint8_t*)malloc(CHUNK_SIZE); + } + + if (!buffer) { + Serial.println("✗ 버퍼 할당 실패"); + file.close(); + + // 4-bit 모드로 복구 + SD_MMC.end(); + delay(100); + SD_MMC.setPins(SDIO_CLK, SDIO_CMD, SDIO_D0, SDIO_D1, SDIO_D2, SDIO_D3); + SD_MMC.begin("/sdcard", false); + + xSemaphoreGive(sdMutex); + if (webTaskHandle != NULL) vTaskResume(webTaskHandle); + if (sdWriteTaskHandle != NULL) vTaskResume(sdWriteTaskHandle); + if (wasLogging) loggingEnabled = true; + return; + } + + size_t totalSent = 0; + bool downloadSuccess = true; + WiFiClient client = server.client(); + unsigned long lastPrint = millis(); + + while (file.available() && totalSent < fileSize && downloadSuccess) { + // SD 읽기 + size_t bytesRead = file.read(buffer, CHUNK_SIZE); + + if (bytesRead == 0) { + Serial.printf("✗ 읽기 실패 (위치: %u)\n", totalSent); + downloadSuccess = false; + break; + } + + // WiFi 전송 + if (!client.connected()) { + Serial.println("✗ 클라이언트 연결 끊김"); + downloadSuccess = false; + break; + } + + size_t totalWritten = 0; + while (totalWritten < bytesRead && client.connected()) { + size_t written = client.write(buffer + totalWritten, bytesRead - totalWritten); + totalWritten += written; + if (written == 0) delay(5); + } + + totalSent += bytesRead; + + // 진행상황 (1초마다) + if (millis() - lastPrint > 1000) { + float percent = (totalSent * 100.0) / fileSize; + Serial.printf("📤 %.1f%% (%u/%u)\n", percent, totalSent, fileSize); + lastPrint = millis(); + } + + yield(); + } + + free(buffer); + file.close(); + + Serial.println("========================================"); + if (downloadSuccess && totalSent == fileSize) { + Serial.printf("✓ 완료: %u bytes (100.0%%)\n", totalSent); + } else { + Serial.printf("⚠ 불완전: %u/%u bytes (%.1f%%)\n", + totalSent, fileSize, (totalSent * 100.0) / fileSize); + } + Serial.println("========================================"); + + // ⭐ SD 카드를 4-bit 모드로 복구 + Serial.println("🔄 SD 카드 4-bit 모드로 복구..."); + SD_MMC.end(); + delay(200); + + if (SD_MMC.setPins(SDIO_CLK, SDIO_CMD, SDIO_D0, SDIO_D1, SDIO_D2, SDIO_D3)) { + if (SD_MMC.begin("/sdcard", false)) { // false = 4-bit mode + Serial.println("✓ SD 카드 4-bit 모드 복구 완료"); + } else { + Serial.println("⚠ 4-bit 모드 복구 실패, 1-bit 유지"); + SD_MMC.setPins(SDIO_CLK, SDIO_CMD, SDIO_D0); + SD_MMC.begin("/sdcard", true); + } + } + + xSemaphoreGive(sdMutex); + + // Task 재개 + if (webTaskHandle != NULL) { + vTaskResume(webTaskHandle); + Serial.println("▶ 웹 업데이트 Task 재개"); + } + if (sdWriteTaskHandle != NULL) { + vTaskResume(sdWriteTaskHandle); + Serial.println("▶ SD 쓰기 Task 재개"); + } + + // 로깅 재개 + if (wasLogging) { + loggingEnabled = true; + Serial.println("▶ 로깅 재개"); + } + + Serial.println("\n"); }); server.begin(); @@ -3441,7 +3651,7 @@ void setup() { xTaskCreatePinnedToCore(serialRxTask, "SERIAL_RX", 6144, NULL, 5, &serialRxTaskHandle, 0); xTaskCreatePinnedToCore(serial2RxTask, "SERIAL2_RX", 6144, NULL, 5, &serial2RxTaskHandle, 0); // ⭐ Serial2 xTaskCreatePinnedToCore(txTask, "TX", 4096, NULL, 3, NULL, 1); - xTaskCreatePinnedToCore(webUpdateTask, "WEB_UPDATE", 10384, NULL, 2, &webTaskHandle, 0); // ⭐ 10240 → 16384 + xTaskCreatePinnedToCore(webUpdateTask, "WEB_UPDATE", 18384, NULL, 2, &webTaskHandle, 1); // ⭐ 10240 → 16384 xTaskCreatePinnedToCore(sdMonitorTask, "SD_MONITOR", 4096, NULL, 1, NULL, 1); if (timeSyncStatus.rtcAvailable) {