diff --git a/ESP32_SerialLogger.ino b/ESP32_SerialLogger.ino index cdaa97f..93fcb01 100644 --- a/ESP32_SerialLogger.ino +++ b/ESP32_SerialLogger.ino @@ -229,6 +229,8 @@ void setup() { Serial.printf(" RTC: %s | Time: %s\n", rtcStatus.available ? "OK" : "NO", rtcStatus.timeSynced ? "Valid" : "Pending"); + Serial.printf(" AutoStart: %s\n", + sdAutoStart ? "ON (logging started)" : "OFF"); Serial.println("============================================\n"); } @@ -244,12 +246,13 @@ void loop() { lastReport = millis(); char timeBuf[24]; getTimestamp(timeBuf, sizeof(timeBuf)); - Serial.printf("[SYS] %s | Heap:%d | SD:%s | WS:%d | RTC:%s | STA:%s\n", + Serial.printf("[SYS] %s | Heap:%d | SD:%s | WS:%d | RTC:%s | STA:%s | Auto:%s\n", timeBuf, ESP.getFreeHeap(), sdCardPresent() ? "OK" : "FAIL", webSocket.connectedClients(), rtcStatus.available ? "OK" : "NO", - staConnected ? "ON" : "OFF"); + staConnected ? "ON" : "OFF", + sdAutoStart ? "ON" : "OFF"); } } diff --git a/sdcard_task.cpp b/sdcard_task.cpp index d0b36ea..df51aea 100644 --- a/sdcard_task.cpp +++ b/sdcard_task.cpp @@ -4,11 +4,15 @@ 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 @@ -70,8 +74,19 @@ void sdTaskInit() { SD.mkdir(LOG_DIR); Serial.println("[SD] Created /logs directory"); } - sdLoggingActive = false; - Serial.println("[SD] Ready (logging OFF - start via web UI)"); + + // 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))); @@ -80,6 +95,13 @@ void sdTaskInit() { 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); + } } // ============================================================ @@ -403,3 +425,36 @@ bool sdDeleteFile(const char *filename) { } 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); +} diff --git a/sdcard_task.h b/sdcard_task.h index 5b305f9..69c6fd6 100644 --- a/sdcard_task.h +++ b/sdcard_task.h @@ -9,6 +9,7 @@ #include "config.h" extern volatile bool sdLoggingActive; +extern volatile bool sdAutoStart; extern char currentLogFileName[64]; void sdTaskInit(); @@ -21,5 +22,7 @@ bool sdDeleteFile(const char *filename); bool sdCardPresent(); String sdGetComment(const char *filename); bool sdSetComment(const char *filename, const char *comment); +bool sdGetAutoStart(); +void sdSetAutoStart(bool enable); #endif diff --git a/serial_task.cpp b/serial_task.cpp index 3a7be8f..494aec4 100644 --- a/serial_task.cpp +++ b/serial_task.cpp @@ -10,6 +10,10 @@ volatile uint32_t serialBaud = DEFAULT_BAUD_RATE; volatile uint8_t serialDataBits = 8; volatile char serialParity = 'N'; volatile uint8_t serialStopBits = 1; +volatile uint8_t serialPort = 0; // 0=UART2, 1=UART0(USB) + +// Pointer to the active monitoring serial port +HardwareSerial* monSerial = &Serial2; static SemaphoreHandle_t serialMutex = NULL; @@ -25,7 +29,6 @@ void getTimestamp(char *buf, size_t len) { } static uint32_t getSerialConfig(uint8_t dataBits, char parity, uint8_t stopBits) { - // Use standard Arduino serial config constants if (dataBits == 8 && parity == 'N' && stopBits == 1) return SERIAL_8N1; if (dataBits == 8 && parity == 'E' && stopBits == 1) return SERIAL_8E1; if (dataBits == 8 && parity == 'O' && stopBits == 1) return SERIAL_8O1; @@ -41,30 +44,80 @@ static uint32_t getSerialConfig(uint8_t dataBits, char parity, uint8_t stopBits) if (dataBits == 5 && parity == 'N' && stopBits == 1) return SERIAL_5N1; if (dataBits == 5 && parity == 'E' && stopBits == 1) return SERIAL_5E1; if (dataBits == 5 && parity == 'O' && stopBits == 1) return SERIAL_5O1; - return SERIAL_8N1; // default + return SERIAL_8N1; } +// ============================================================ +// Switch serial port: 0=UART2(GPIO16/17), 1=UART0(USB) +// ============================================================ +void switchSerialPort(uint8_t port) { + if (serialMutex) xSemaphoreTake(serialMutex, portMAX_DELAY); + + if (port == 1) { + // Switch to UART0 (USB) for testing + // Note: UART0 is already running (Serial.begin in setup) + // Just point monSerial to it. Debug output still goes to Serial TX, + // but RX task only reads what PC sends (RX buffer), so no conflict. + Serial2.flush(); + Serial2.end(); + monSerial = &Serial; + serialPort = 1; + Serial.println("[Serial] Switched to UART0 (USB) - test mode"); + Serial.println("[Serial] Send data from PC terminal to test"); + } else { + // Switch to UART2 (GPIO16/17) for field use + uint32_t config = getSerialConfig(serialDataBits, serialParity, serialStopBits); + Serial2.begin(serialBaud, config, SERIAL2_RX_PIN, SERIAL2_TX_PIN); + Serial2.setRxBufferSize(DEFAULT_RX_BUFFER); + monSerial = &Serial2; + serialPort = 0; + Serial.printf("[Serial] Switched to UART2 (TX=%d, RX=%d)\n", + SERIAL2_TX_PIN, SERIAL2_RX_PIN); + } + + // Drain any old data from new port's RX buffer + while (monSerial->available()) monSerial->read(); + + if (serialMutex) xSemaphoreGive(serialMutex); +} + +// ============================================================ +// Reconfigure baud/parity/etc (applies to current port) +// ============================================================ void reconfigureSerial(uint32_t baud, uint8_t dataBits, char parity, uint8_t stopBits) { if (serialMutex) xSemaphoreTake(serialMutex, portMAX_DELAY); - Serial2.flush(); - Serial2.end(); - delay(50); + monSerial->flush(); - uint32_t config = getSerialConfig(dataBits, parity, stopBits); - Serial2.begin(baud, config, SERIAL2_RX_PIN, SERIAL2_TX_PIN); - Serial2.setRxBufferSize(DEFAULT_RX_BUFFER); + if (serialPort == 0) { + // UART2 - full reconfigure + Serial2.end(); + delay(50); + uint32_t config = getSerialConfig(dataBits, parity, stopBits); + Serial2.begin(baud, config, SERIAL2_RX_PIN, SERIAL2_TX_PIN); + Serial2.setRxBufferSize(DEFAULT_RX_BUFFER); + } else { + // UART0 - reconfigure USB serial baud + Serial.end(); + delay(50); + Serial.begin(baud); + } serialBaud = baud; serialDataBits = dataBits; serialParity = parity; serialStopBits = stopBits; - Serial.printf("[Serial] Reconfigured: %lu %d%c%d\n", baud, dataBits, parity, stopBits); + Serial.printf("[Serial] Reconfigured: %lu %d%c%d (port=%s)\n", + baud, dataBits, parity, stopBits, + serialPort == 0 ? "UART2" : "USB"); if (serialMutex) xSemaphoreGive(serialMutex); } +// ============================================================ +// Init +// ============================================================ void serialTaskInit() { queueSD = xQueueCreate(QUEUE_SD_SIZE, sizeof(LogEntry*)); queueWeb = xQueueCreate(QUEUE_WEB_SIZE, sizeof(LogEntry*)); @@ -72,13 +125,17 @@ void serialTaskInit() { serialMutex = xSemaphoreCreateMutex(); + // Default: UART2 uint32_t config = getSerialConfig(serialDataBits, serialParity, serialStopBits); Serial2.begin(serialBaud, config, SERIAL2_RX_PIN, SERIAL2_TX_PIN); Serial2.setRxBufferSize(DEFAULT_RX_BUFFER); + monSerial = &Serial2; + serialPort = 0; - Serial.printf("[Serial] Init: %lu %d%c%d on TX=%d RX=%d\n", + Serial.printf("[Serial] Init: %lu %d%c%d on UART2 (TX=%d, RX=%d)\n", serialBaud, serialDataBits, (char)serialParity, serialStopBits, SERIAL2_TX_PIN, SERIAL2_RX_PIN); + Serial.println("[Serial] Port switchable via Settings (UART2/USB)"); xTaskCreatePinnedToCore(serialRxTask, "SerialRX", TASK_STACK_SERIAL, NULL, TASK_PRIORITY_SERIAL, NULL, 1); @@ -87,7 +144,9 @@ void serialTaskInit() { NULL, TASK_PRIORITY_SERIAL - 1, NULL, 1); } +// ============================================================ // Flush assembled line to SD and Web queues +// ============================================================ static void flushLineToQueues(char *lineBuf, int &linePos) { if (linePos <= 0) return; lineBuf[linePos] = '\0'; @@ -114,6 +173,9 @@ static void flushLineToQueues(char *lineBuf, int &linePos) { linePos = 0; } +// ============================================================ +// RX Task - reads from monSerial (UART2 or UART0) +// ============================================================ void serialRxTask(void *param) { static char lineBuf[LOG_LINE_MAX_LEN]; int linePos = 0; @@ -122,7 +184,8 @@ void serialRxTask(void *param) { Serial.println("[Task] SerialRX started on core " + String(xPortGetCoreID())); while (true) { - int available = Serial2.available(); + HardwareSerial* port = monSerial; // Local copy for thread safety + int available = port->available(); if (available > 0) { int space = (int)(LOG_LINE_MAX_LEN - linePos - 1); @@ -130,13 +193,12 @@ void serialRxTask(void *param) { int toRead = (available < space) ? available : space; for (int i = 0; i < toRead; i++) { - char c = Serial2.read(); + char c = port->read(); if (c == '\n' || c == '\r') { if (linePos > 0) flushLineToQueues(lineBuf, linePos); continue; } // Only accept printable ASCII (0x20~0x7E) and TAB (0x09) - // Skip null bytes and noise (0x00, 0xFF, etc.) if ((c >= 0x20 && c <= 0x7E) || c == '\t') { lineBuf[linePos++] = c; } @@ -151,15 +213,20 @@ void serialRxTask(void *param) { } } +// ============================================================ +// TX Task - writes to monSerial (UART2 or UART0) +// ============================================================ void serialTxTask(void *param) { Serial.println("[Task] SerialTX started on core " + String(xPortGetCoreID())); while (true) { LogEntry *entry; if (xQueueReceive(queueTX, &entry, pdMS_TO_TICKS(50)) == pdTRUE) { + HardwareSerial* port = monSerial; + if (serialMutex) xSemaphoreTake(serialMutex, portMAX_DELAY); - Serial2.write((uint8_t*)entry->data, entry->dataLen); - Serial2.flush(); + port->write((uint8_t*)entry->data, entry->dataLen); + port->flush(); if (serialMutex) xSemaphoreGive(serialMutex); LogEntry *sdEntry = (LogEntry*)pvPortMalloc(sizeof(LogEntry)); diff --git a/serial_task.h b/serial_task.h index e35da4e..2c797ec 100644 --- a/serial_task.h +++ b/serial_task.h @@ -15,11 +15,15 @@ extern volatile uint32_t serialBaud; extern volatile uint8_t serialDataBits; extern volatile char serialParity; extern volatile uint8_t serialStopBits; +extern volatile uint8_t serialPort; // 0=UART2(GPIO16/17), 1=UART0(USB) + +extern HardwareSerial* monSerial; // Active monitoring serial port void serialTaskInit(); void serialRxTask(void *param); void serialTxTask(void *param); void reconfigureSerial(uint32_t baud, uint8_t dataBits, char parity, uint8_t stopBits); +void switchSerialPort(uint8_t port); void getTimestamp(char *buf, size_t len); #endif diff --git a/web_html.h b/web_html.h index 63ccb78..618c859 100644 --- a/web_html.h +++ b/web_html.h @@ -186,6 +186,15 @@ tr:active{background:rgba(233,69,96,0.1);}

Serial Port

+
+ +
+
+ UART2: TX=GPIO17, RX=GPIO16 (외부 장비 연결) +
@@ -240,6 +249,20 @@ tr:active{background:rgba(233,69,96,0.1);}
+
+

상시로깅 (Auto-Start)

+

전원 OFF→ON 시 자동으로 로깅 시작. 현장 장기 설치용.

+
+ + OFF +
+ + +
@@ -266,7 +289,7 @@ tr:active{background:rgba(233,69,96,0.1);}
Log OFF - 115200 8N1 + UART2 115200 8N1 RX:0 TX:0 -- @@ -304,6 +327,7 @@ function wsConn(){ sendC('sysinfo'); sendC('get_serial_config'); sendC('get_wifi'); + sendC('get_autostart'); }; ws.onmessage=function(e){ try{ @@ -313,6 +337,7 @@ function wsConn(){ else if(m.type==='serial_config') updSer(m); else if(m.type==='log_status') updLog(m); else if(m.type==='wifi_status') updWifi(m); + else if(m.type==='autostart_status') updAuto(m); }catch(x){} }; ws.onclose=function(){ @@ -432,7 +457,37 @@ function updSer(m){ if(m.dataBits) document.getElementById('dBits').value=m.dataBits; if(m.parity) document.getElementById('par').value=m.parity; if(m.stopBits) document.getElementById('sBits').value=m.stopBits; - document.getElementById('serInfo').textContent=m.baud+' '+m.dataBits+m.parity+m.stopBits; + // Port selector + if(m.port!==undefined){ + document.getElementById('sPort').value=m.port; + updPortInfo(m.port); + } + // Status bar: show port + config + let portLabel=((m.port||0)==1)?'USB':'UART2'; + document.getElementById('serInfo').textContent=portLabel+' '+m.baud+' '+m.dataBits+m.parity+m.stopBits; +} + +function updPortInfo(p){ + let info=document.getElementById('portInfo'); + if(p==1){ + info.textContent='USB: PC 시리얼 터미널에서 데이터 송수신 테스트'; + info.style.color='var(--ok)'; + }else{ + info.textContent='UART2: TX=GPIO17, RX=GPIO16 (외부 장비 연결)'; + info.style.color='#888'; + } +} + +function switchPort(){ + let port=parseInt(document.getElementById('sPort').value); + if(port===1){ + if(!confirm('USB 테스트 모드로 전환하시겠습니까?\n\nPC의 시리얼 터미널(PuTTY 등)에서\nESP32 COM 포트로 데이터를 보내면\n웹 터미널에 표시됩니다.')) { + document.getElementById('sPort').value='0'; + return; + } + } + updPortInfo(port); + if(ws&&ws.readyState===1) ws.send(JSON.stringify({cmd:'switch_port',port:port})); } // ===== System Info ===== @@ -533,6 +588,35 @@ function updLog(m){ document.getElementById('logFile').value=m.file||''; } +// ===== Autostart (Persistent Logging) ===== +function updAuto(m){ + let st=document.getElementById('autoSt'); + let btn=document.getElementById('autoBtn'); + let grp=document.getElementById('autoGrp'); + let desc=document.getElementById('autoDesc'); + if(m.enabled){ + st.textContent='ON';st.style.color='var(--ok)'; + btn.textContent='상시로깅 비활성화'; + btn.style.background='var(--btn)';btn.style.color='#fff'; + grp.style.borderColor='var(--ok)'; + desc.style.display='block'; + }else{ + st.textContent='OFF';st.style.color='#888'; + btn.textContent='상시로깅 활성화'; + btn.style.background='var(--border)';btn.style.color='var(--text)'; + grp.style.borderColor='var(--border)'; + desc.style.display='none'; + } +} + +function toggleAutoStart(){ + let cur=document.getElementById('autoSt').textContent==='ON'; + if(!cur){ + if(!confirm('상시로깅을 활성화하시겠습니까?\n\n전원이 꺼졌다 켜져도 자동으로 로깅이 시작됩니다.\n(현장 장기 설치용)')) return; + } + sendC('toggle_autostart'); +} + // ===== File Manager ===== function loadFiles(){ fetch('/api/files').then(r=>r.json()).then(d=>{ diff --git a/web_task.cpp b/web_task.cpp index 46836b5..da665aa 100644 --- a/web_task.cpp +++ b/web_task.cpp @@ -172,11 +172,29 @@ void handleWsMessage(uint8_t num, const char *message) { resp["dataBits"] = serialDataBits; resp["parity"] = String((char)serialParity); resp["stopBits"] = serialStopBits; + resp["port"] = serialPort; // 0=UART2, 1=USB String json; serializeJson(resp, json); webSocket.sendTXT(num, json); } + // --- Switch Serial Port --- + else if (strcmp(cmd, "switch_port") == 0) { + uint8_t port = doc["port"] | 0; + switchSerialPort(port); + + StaticJsonDocument<256> resp; + resp["type"] = "serial_config"; + resp["baud"] = (uint32_t)serialBaud; + resp["dataBits"] = serialDataBits; + resp["parity"] = String((char)serialParity); + resp["stopBits"] = serialStopBits; + resp["port"] = serialPort; + String json; + serializeJson(resp, json); + webSocket.broadcastTXT(json); + } + // --- System Info (includes RTC + WiFi status) --- else if (strcmp(cmd, "sysinfo") == 0) { StaticJsonDocument<768> resp; @@ -291,6 +309,42 @@ void handleWsMessage(uint8_t num, const char *message) { serializeJson(resp, json); webSocket.sendTXT(num, json); } + + // --- Toggle Autostart (persistent logging) --- + else if (strcmp(cmd, "toggle_autostart") == 0) { + bool newState = !sdAutoStart; + sdSetAutoStart(newState); + + // If just enabled and not already logging, start now + if (newState && !sdLoggingActive) { + sdStartLogging(); + // Also send log_status update + StaticJsonDocument<256> logResp; + logResp["type"] = "log_status"; + logResp["active"] = sdLoggingActive; + logResp["file"] = currentLogFileName; + String logJson; + serializeJson(logResp, logJson); + webSocket.broadcastTXT(logJson); + } + + StaticJsonDocument<128> resp; + resp["type"] = "autostart_status"; + resp["enabled"] = (bool)sdAutoStart; + String json; + serializeJson(resp, json); + webSocket.broadcastTXT(json); + } + + // --- Get Autostart Status --- + else if (strcmp(cmd, "get_autostart") == 0) { + StaticJsonDocument<128> resp; + resp["type"] = "autostart_status"; + resp["enabled"] = (bool)sdAutoStart; + String json; + serializeJson(resp, json); + webSocket.sendTXT(num, json); + } } // ============================================================