From 1963d8d4233e543df49a9c73cf73555bdefdfab5 Mon Sep 17 00:00:00 2001 From: byun Date: Fri, 2 Jan 2026 21:37:48 +0000 Subject: [PATCH] =?UTF-8?q?=EC=9E=90=EB=8F=99=EB=A1=9C=EA=B9=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ESP32_CAN_Logger-a.ino | 474 ++++++++++++++++++++++++++++++++++++++++- index.h | 466 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 929 insertions(+), 11 deletions(-) diff --git a/ESP32_CAN_Logger-a.ino b/ESP32_CAN_Logger-a.ino index 4334721..97b86d9 100644 --- a/ESP32_CAN_Logger-a.ino +++ b/ESP32_CAN_Logger-a.ino @@ -244,6 +244,34 @@ TaskHandle_t serial2RxTaskHandle = NULL; // ⭐ Serial2 추가 // 로깅 변수 volatile bool loggingEnabled = false; + + +// ============================================ +// 🎯 Auto Trigger 전역 변수 +// ============================================ + +// Auto Trigger 조건 구조체 +struct TriggerCondition { + uint32_t canId; // CAN ID + uint8_t startBit; // 시작 비트 (0-63) + uint8_t bitLength; // 비트 길이 (1-64) + char op[3]; // 연산자: "==", "!=", ">", "<", ">=", "<=" + int64_t value; // 비교 값 + bool enabled; // 조건 활성화 +}; + +#define MAX_TRIGGERS 8 + +TriggerCondition startTriggers[MAX_TRIGGERS]; +TriggerCondition stopTriggers[MAX_TRIGGERS]; +int startTriggerCount = 0; +int stopTriggerCount = 0; + +bool autoTriggerEnabled = false; +char startLogicOp[4] = "OR"; +char stopLogicOp[4] = "OR"; +bool autoTriggerActive = false; +bool autoTriggerLogCSV = false; // 🆕 Auto Trigger용 CSV 형식 설정 volatile bool serialLoggingEnabled = false; volatile bool serial2LoggingEnabled = false; // ⭐ Serial2 추가 volatile bool sdCardReady = false; @@ -616,6 +644,295 @@ void loadSettings() { preferences.end(); } +// ============================================ +// 🎯 Auto Trigger 함수 +// ============================================ + +// 비트 추출 (Motorola/Big-endian) +int64_t extractBits(uint8_t *data, uint8_t startBit, uint8_t bitLength) { + if (bitLength == 0 || bitLength > 64 || startBit >= 64) return 0; + + int64_t result = 0; + for (int i = 0; i < bitLength; i++) { + uint8_t bitPos = startBit + i; + uint8_t byteIdx = bitPos / 8; + uint8_t bitIdx = 7 - (bitPos % 8); + + if (byteIdx < 8) { + if (data[byteIdx] & (1 << bitIdx)) { + result |= (1LL << (bitLength - 1 - i)); + } + } + } + return result; +} + +// 조건 체크 +bool checkCondition(TriggerCondition &trigger, uint8_t *data) { + if (!trigger.enabled) return false; + + int64_t extractedValue = extractBits(data, trigger.startBit, trigger.bitLength); + + if (strcmp(trigger.op, "==") == 0) return extractedValue == trigger.value; + else if (strcmp(trigger.op, "!=") == 0) return extractedValue != trigger.value; + else if (strcmp(trigger.op, ">") == 0) return extractedValue > trigger.value; + else if (strcmp(trigger.op, "<") == 0) return extractedValue < trigger.value; + else if (strcmp(trigger.op, ">=") == 0) return extractedValue >= trigger.value; + else if (strcmp(trigger.op, "<=") == 0) return extractedValue <= trigger.value; + + return false; +} + +// Auto Trigger 체크 +void checkAutoTriggers(struct can_frame &frame) { + if (!autoTriggerEnabled || !sdCardReady) return; + + // 시작 조건 체크 + if (!loggingEnabled && startTriggerCount > 0) { + bool result = (strcmp(startLogicOp, "AND") == 0); + bool anyMatch = false; + + for (int i = 0; i < startTriggerCount; i++) { + if (startTriggers[i].canId == frame.can_id && startTriggers[i].enabled) { + bool match = checkCondition(startTriggers[i], frame.data); + anyMatch = true; + + if (strcmp(startLogicOp, "AND") == 0) { + result = result && match; + if (!match) break; + } else { + if (match) { + result = true; + break; + } + } + } + } + + if (anyMatch && result) { + // 🎯 파일 생성 로직 추가 + if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) { + time_t now; + struct tm timeinfo; + time(&now); + localtime_r(&now, &timeinfo); + + struct timeval tv; + gettimeofday(&tv, NULL); + canLogStartTime = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec; + + // 🆕 Auto Trigger 전용 형식 설정 적용 + canLogFormatCSV = autoTriggerLogCSV; + + const char* ext = canLogFormatCSV ? "csv" : "bin"; + snprintf(currentFilename, sizeof(currentFilename), + "/CAN_%04d%02d%02d_%02d%02d%02d.%s", + timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday, + timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec, ext); + + // 파일 생성 (헤더 쓰기) + logFile = SD_MMC.open(currentFilename, FILE_WRITE); + + if (logFile) { + if (canLogFormatCSV) { + logFile.println("Time_us,CAN_ID,DLC,Data"); + logFile.flush(); + } + logFile.close(); + + // APPEND 모드로 다시 열기 + logFile = SD_MMC.open(currentFilename, FILE_APPEND); + + if (logFile) { + loggingEnabled = true; + autoTriggerActive = true; + bufferIndex = 0; + currentFileSize = logFile.size(); + + Serial.println("🎯 Auto Trigger: 로깅 시작!"); + Serial.printf(" 조건: ID 0x%03X 만족\n", frame.can_id); + Serial.printf(" 파일: %s\n", currentFilename); + Serial.printf(" 형식: %s\n", canLogFormatCSV ? "CSV" : "BIN"); + } else { + Serial.println("✗ Auto Trigger: APPEND 모드 파일 열기 실패"); + } + } else { + Serial.println("✗ Auto Trigger: 파일 생성 실패"); + } + xSemaphoreGive(sdMutex); + } else { + Serial.println("✗ Auto Trigger: sdMutex 획득 실패"); + } + } + } + + // 중지 조건 체크 + if (loggingEnabled && stopTriggerCount > 0) { + bool result = (strcmp(stopLogicOp, "AND") == 0); + bool anyMatch = false; + + for (int i = 0; i < stopTriggerCount; i++) { + if (stopTriggers[i].canId == frame.can_id && stopTriggers[i].enabled) { + bool match = checkCondition(stopTriggers[i], frame.data); + anyMatch = true; + + if (strcmp(stopLogicOp, "AND") == 0) { + result = result && match; + if (!match) break; + } else { + if (match) { + result = true; + break; + } + } + } + } + + if (anyMatch && result) { + // 🎯 파일 닫기 로직 추가 + if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(2000)) == pdTRUE) { + // BIN 형식: 버퍼에 남은 데이터 강제 플러시 + if (bufferIndex > 0 && logFile) { + size_t written = logFile.write(fileBuffer, bufferIndex); + logFile.flush(); + Serial.printf("✓ Auto Trigger 최종 플러시: %d bytes\n", written); + bufferIndex = 0; + } + + // CSV 형식: 최종 플러시 + if (canLogFormatCSV && logFile) { + logFile.flush(); + } + + if (logFile) { + size_t finalSize = logFile.size(); + logFile.close(); + Serial.printf("✓ Auto Trigger 파일 닫힘: %s (%lu bytes)\n", currentFilename, finalSize); + } + + loggingEnabled = false; + autoTriggerActive = false; + currentFilename[0] = '\0'; + bufferIndex = 0; + + Serial.println("🎯 Auto Trigger: 로깅 중지!"); + Serial.printf(" 조건: ID 0x%03X 만족\n", frame.can_id); + + xSemaphoreGive(sdMutex); + } + } + } +} + +// Auto Trigger 설정 저장 +void saveAutoTriggerSettings() { + preferences.begin("autotrigger", false); + + preferences.putBool("enabled", autoTriggerEnabled); + preferences.putBool("logCSV", autoTriggerLogCSV); // 🆕 로그 형식 저장 + preferences.putString("start_logic", startLogicOp); + preferences.putString("stop_logic", stopLogicOp); + + preferences.putInt("start_count", startTriggerCount); + for (int i = 0; i < startTriggerCount; i++) { + char key[32]; + sprintf(key, "s%d_id", i); + preferences.putUInt(key, startTriggers[i].canId); + sprintf(key, "s%d_bit", i); + preferences.putUChar(key, startTriggers[i].startBit); + sprintf(key, "s%d_len", i); + preferences.putUChar(key, startTriggers[i].bitLength); + sprintf(key, "s%d_op", i); + preferences.putString(key, startTriggers[i].op); + sprintf(key, "s%d_val", i); + preferences.putLong(key, startTriggers[i].value); + sprintf(key, "s%d_en", i); + preferences.putBool(key, startTriggers[i].enabled); + } + + preferences.putInt("stop_count", stopTriggerCount); + for (int i = 0; i < stopTriggerCount; i++) { + char key[32]; + sprintf(key, "p%d_id", i); + preferences.putUInt(key, stopTriggers[i].canId); + sprintf(key, "p%d_bit", i); + preferences.putUChar(key, stopTriggers[i].startBit); + sprintf(key, "p%d_len", i); + preferences.putUChar(key, stopTriggers[i].bitLength); + sprintf(key, "p%d_op", i); + preferences.putString(key, stopTriggers[i].op); + sprintf(key, "p%d_val", i); + preferences.putLong(key, stopTriggers[i].value); + sprintf(key, "p%d_en", i); + preferences.putBool(key, stopTriggers[i].enabled); + } + + preferences.end(); + Serial.println("💾 Auto Trigger 설정 저장 완료"); +} + +// Auto Trigger 설정 로드 +void loadAutoTriggerSettings() { + preferences.begin("autotrigger", true); + + autoTriggerEnabled = preferences.getBool("enabled", false); + autoTriggerLogCSV = preferences.getBool("logCSV", false); // 🆕 로그 형식 로드 + preferences.getString("start_logic", startLogicOp, sizeof(startLogicOp)); + preferences.getString("stop_logic", stopLogicOp, sizeof(stopLogicOp)); + + if (strlen(startLogicOp) == 0) strcpy(startLogicOp, "OR"); + if (strlen(stopLogicOp) == 0) strcpy(stopLogicOp, "OR"); + + startTriggerCount = preferences.getInt("start_count", 0); + if (startTriggerCount > MAX_TRIGGERS) startTriggerCount = MAX_TRIGGERS; + + for (int i = 0; i < startTriggerCount; i++) { + char key[32]; + sprintf(key, "s%d_id", i); + startTriggers[i].canId = preferences.getUInt(key, 0); + sprintf(key, "s%d_bit", i); + startTriggers[i].startBit = preferences.getUChar(key, 0); + sprintf(key, "s%d_len", i); + startTriggers[i].bitLength = preferences.getUChar(key, 8); + sprintf(key, "s%d_op", i); + preferences.getString(key, startTriggers[i].op, sizeof(startTriggers[i].op)); + if (strlen(startTriggers[i].op) == 0) strcpy(startTriggers[i].op, "=="); + sprintf(key, "s%d_val", i); + startTriggers[i].value = preferences.getLong(key, 0); + sprintf(key, "s%d_en", i); + startTriggers[i].enabled = preferences.getBool(key, true); + } + + stopTriggerCount = preferences.getInt("stop_count", 0); + if (stopTriggerCount > MAX_TRIGGERS) stopTriggerCount = MAX_TRIGGERS; + + for (int i = 0; i < stopTriggerCount; i++) { + char key[32]; + sprintf(key, "p%d_id", i); + stopTriggers[i].canId = preferences.getUInt(key, 0); + sprintf(key, "p%d_bit", i); + stopTriggers[i].startBit = preferences.getUChar(key, 0); + sprintf(key, "p%d_len", i); + stopTriggers[i].bitLength = preferences.getUChar(key, 8); + sprintf(key, "p%d_op", i); + preferences.getString(key, stopTriggers[i].op, sizeof(stopTriggers[i].op)); + if (strlen(stopTriggers[i].op) == 0) strcpy(stopTriggers[i].op, "=="); + sprintf(key, "p%d_val", i); + stopTriggers[i].value = preferences.getLong(key, 0); + sprintf(key, "p%d_en", i); + stopTriggers[i].enabled = preferences.getBool(key, true); + } + + preferences.end(); + + if (autoTriggerEnabled) { + Serial.println("✓ Auto Trigger 설정 로드 완료"); + Serial.printf(" 시작 조건: %d개 (%s)\n", startTriggerCount, startLogicOp); + Serial.printf(" 중지 조건: %d개 (%s)\n", stopTriggerCount, stopLogicOp); + } +} + + void saveSettings() { preferences.begin("can-logger", false); preferences.putString("wifi_ssid", wifiSSID); @@ -928,6 +1245,9 @@ void canRxTask(void *parameter) { while (mcp2515.readMessage(&frame) == MCP2515::ERROR_OK) { struct timeval tv; + // 🎯 Auto Trigger 체크 + checkAutoTriggers(frame); + gettimeofday(&tv, NULL); msg.timestamp_us = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec; msg.id = frame.can_id & 0x1FFFFFFF; @@ -1873,6 +2193,138 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) addFileComment(filename, comment); } } + // 🎯 Auto Trigger 명령 추가 + else if (strcmp(cmd, "setAutoTrigger") == 0) { + autoTriggerEnabled = doc["enabled"] | false; + + // 🆕 로그 형식 설정 + const char* logFormat = doc["logFormat"]; + if (logFormat) { + autoTriggerLogCSV = (strcmp(logFormat, "csv") == 0); + } + + saveAutoTriggerSettings(); + + DynamicJsonDocument response(256); + response["type"] = "autoTriggerSet"; + response["enabled"] = autoTriggerEnabled; + response["logFormat"] = autoTriggerLogCSV ? "csv" : "bin"; + + String json; + serializeJson(response, json); + webSocket.sendTXT(num, json); + } + // 🆕 Auto Trigger 형식 설정 명령 + else if (strcmp(cmd, "setAutoTriggerFormat") == 0) { + const char* logFormat = doc["logFormat"]; + if (logFormat) { + autoTriggerLogCSV = (strcmp(logFormat, "csv") == 0); + saveAutoTriggerSettings(); + Serial.printf("🎯 Auto Trigger 형식 설정: %s\n", autoTriggerLogCSV ? "CSV" : "BIN"); + } + + DynamicJsonDocument response(128); + response["type"] = "autoTriggerFormatSet"; + response["logFormat"] = autoTriggerLogCSV ? "csv" : "bin"; + + String json; + serializeJson(response, json); + webSocket.sendTXT(num, json); + } + else if (strcmp(cmd, "setStartTriggers") == 0) { + JsonArray triggers = doc["triggers"]; + strcpy(startLogicOp, doc["logic"] | "OR"); + startTriggerCount = 0; + + for (JsonObject t : triggers) { + if (startTriggerCount >= MAX_TRIGGERS) break; + + String idStr = t["canId"] | "0x0"; + startTriggers[startTriggerCount].canId = strtoul(idStr.c_str(), NULL, 16); + startTriggers[startTriggerCount].startBit = t["startBit"] | 0; + startTriggers[startTriggerCount].bitLength = t["bitLength"] | 8; + strcpy(startTriggers[startTriggerCount].op, t["op"] | "=="); + startTriggers[startTriggerCount].value = t["value"] | 0; + startTriggers[startTriggerCount].enabled = t["enabled"] | true; + startTriggerCount++; + } + + saveAutoTriggerSettings(); + + DynamicJsonDocument response(256); + response["type"] = "startTriggersSet"; + response["count"] = startTriggerCount; + + String json; + serializeJson(response, json); + webSocket.sendTXT(num, json); + } + else if (strcmp(cmd, "setStopTriggers") == 0) { + JsonArray triggers = doc["triggers"]; + strcpy(stopLogicOp, doc["logic"] | "OR"); + stopTriggerCount = 0; + + for (JsonObject t : triggers) { + if (stopTriggerCount >= MAX_TRIGGERS) break; + + String idStr = t["canId"] | "0x0"; + stopTriggers[stopTriggerCount].canId = strtoul(idStr.c_str(), NULL, 16); + stopTriggers[stopTriggerCount].startBit = t["startBit"] | 0; + stopTriggers[stopTriggerCount].bitLength = t["bitLength"] | 8; + strcpy(stopTriggers[stopTriggerCount].op, t["op"] | "=="); + stopTriggers[stopTriggerCount].value = t["value"] | 0; + stopTriggers[stopTriggerCount].enabled = t["enabled"] | true; + stopTriggerCount++; + } + + saveAutoTriggerSettings(); + + DynamicJsonDocument response(256); + response["type"] = "stopTriggersSet"; + response["count"] = stopTriggerCount; + + String json; + serializeJson(response, json); + webSocket.sendTXT(num, json); + } + else if (strcmp(cmd, "getAutoTriggers") == 0) { + DynamicJsonDocument response(2048); + response["type"] = "autoTriggers"; + response["enabled"] = autoTriggerEnabled; + response["logFormat"] = autoTriggerLogCSV ? "csv" : "bin"; // 🆕 로그 형식 전송 + response["startLogic"] = startLogicOp; + response["stopLogic"] = stopLogicOp; + + JsonArray startArray = response.createNestedArray("startTriggers"); + for (int i = 0; i < startTriggerCount; i++) { + JsonObject t = startArray.createNestedObject(); + char idStr[10]; + sprintf(idStr, "0x%03X", startTriggers[i].canId); + t["canId"] = idStr; + t["startBit"] = startTriggers[i].startBit; + t["bitLength"] = startTriggers[i].bitLength; + t["op"] = startTriggers[i].op; + t["value"] = (long)startTriggers[i].value; + t["enabled"] = startTriggers[i].enabled; + } + + JsonArray stopArray = response.createNestedArray("stopTriggers"); + for (int i = 0; i < stopTriggerCount; i++) { + JsonObject t = stopArray.createNestedObject(); + char idStr[10]; + sprintf(idStr, "0x%03X", stopTriggers[i].canId); + t["canId"] = idStr; + t["startBit"] = stopTriggers[i].startBit; + t["bitLength"] = stopTriggers[i].bitLength; + t["op"] = stopTriggers[i].op; + t["value"] = (long)stopTriggers[i].value; + t["enabled"] = stopTriggers[i].enabled; + } + + String json; + serializeJson(response, json); + webSocket.sendTXT(num, json); + } else if (strcmp(cmd, "sendOnce") == 0) { if (currentMcpMode == MCP_MODE_TRANSMIT) { mcp2515.setNormalMode(); @@ -2160,13 +2612,28 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) void webUpdateTask(void *parameter) { const TickType_t xDelay = pdMS_TO_TICKS(200); // ⭐ 100ms → 200ms (WiFi 안정성 향상) + // 🆕 초기화 대기 (부팅 직후 안정화) + vTaskDelay(pdMS_TO_TICKS(2000)); + while (1) { webSocket.loop(); if (webSocket.connectedClients() > 0) { + // 🆕 큐가 초기화되었는지 확인 + if (!canQueue || !serialQueue) { + vTaskDelay(xDelay); + continue; + } + DynamicJsonDocument doc(16384); // ⭐ 4096 → 8192로 증가 doc["type"] = "update"; doc["logging"] = loggingEnabled; + + // 🎯 Auto Trigger 상태 + doc["autoTriggerEnabled"] = autoTriggerEnabled; + doc["autoTriggerActive"] = autoTriggerActive; + doc["startTriggerCount"] = startTriggerCount; + doc["stopTriggerCount"] = stopTriggerCount; doc["serialLogging"] = serialLoggingEnabled; doc["serial2Logging"] = serial2LoggingEnabled; doc["totalSerial2Rx"] = totalSerial2RxCount; @@ -2189,9 +2656,9 @@ void webUpdateTask(void *parameter) { doc["totalSerialTx"] = totalSerialTxCount; doc["fileSize"] = currentFileSize; doc["serialFileSize"] = currentSerialFileSize; - doc["queueUsed"] = uxQueueMessagesWaiting(canQueue); + doc["queueUsed"] = canQueue ? uxQueueMessagesWaiting(canQueue) : 0; // 🆕 NULL 체크 doc["queueSize"] = CAN_QUEUE_SIZE; - doc["serialQueueUsed"] = uxQueueMessagesWaiting(serialQueue); + doc["serialQueueUsed"] = serialQueue ? uxQueueMessagesWaiting(serialQueue) : 0; // 🆕 NULL 체크 doc["serialQueueSize"] = SERIAL_QUEUE_SIZE; doc["timeSync"] = timeSyncStatus.synchronized; doc["rtcAvail"] = timeSyncStatus.rtcAvailable; @@ -2399,6 +2866,9 @@ void setup() { } loadSettings(); + + // 🎯 Auto Trigger 설정 로드 + loadAutoTriggerSettings(); analogSetPinAttenuation(MONITORING_VOLT, ADC_11db); pinMode(CAN_INT_PIN, INPUT_PULLUP); diff --git a/index.h b/index.h index 871c1a4..16e978d 100644 --- a/index.h +++ b/index.h @@ -675,6 +675,158 @@ const char index_html[] PROGMEM = R"rawliteral( padding: 30px; } } + + /* 🎯 Auto Trigger 스타일 */ + .auto-trigger-section { + background: white; + border-radius: 12px; + padding: 20px; + margin-bottom: 20px; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); + } + + .auto-trigger-section h3 { + margin-top: 0; + margin-bottom: 15px; + color: #667eea; + font-size: 1.3em; + } + + .trigger-control { + margin-bottom: 20px; + padding: 10px; + background: #f8f9fa; + border-radius: 8px; + } + + .trigger-control label { + font-size: 1.1em; + font-weight: 500; + cursor: pointer; + } + + .trigger-group { + margin-bottom: 25px; + padding: 15px; + background: #f8f9fa; + border-radius: 8px; + border-left: 4px solid #667eea; + } + + .trigger-group h4 { + margin-top: 0; + margin-bottom: 10px; + color: #333; + font-size: 1.1em; + } + + .trigger-group select { + margin-bottom: 15px; + padding: 8px; + border-radius: 6px; + border: 1px solid #ddd; + } + + .trigger-card { + background: white; + padding: 12px; + margin-bottom: 10px; + border-radius: 8px; + border: 1px solid #e0e0e0; + display: flex; + gap: 8px; + align-items: center; + flex-wrap: wrap; + } + + .trigger-card input[type="text"], + .trigger-card input[type="number"] { + padding: 6px 10px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 0.9em; + } + + .trigger-card input[type="text"] { + width: 100px; + } + + .trigger-card input[type="number"] { + width: 80px; + } + + .trigger-card select { + padding: 6px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 0.9em; + } + + .trigger-card .btn-delete { + background: #dc3545; + color: white; + border: none; + padding: 6px 12px; + border-radius: 4px; + cursor: pointer; + font-size: 0.9em; + } + + .trigger-card .btn-delete:hover { + background: #c82333; + } + + .btn-add-trigger { + background: #28a745; + color: white; + border: none; + padding: 10px 20px; + border-radius: 6px; + cursor: pointer; + font-size: 1em; + margin-top: 10px; + } + + .btn-add-trigger:hover { + background: #218838; + } + + .btn-save-triggers { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border: none; + padding: 12px 30px; + border-radius: 8px; + cursor: pointer; + font-size: 1.1em; + width: 100%; + margin-top: 15px; + } + + .btn-save-triggers:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4); + } + + .trigger-status { + display: inline-block; + padding: 4px 12px; + border-radius: 12px; + font-size: 0.85em; + font-weight: 500; + margin-left: 10px; + } + + .trigger-status.active { + background: #d4edda; + color: #155724; + } + + .trigger-status.inactive { + background: #f8d7da; + color: #721c24; + } + @@ -829,6 +981,66 @@ const char index_html[] PROGMEM = R"rawliteral( + +
+

+ 🎯 Auto Trigger + Disabled +

+ + +
+ +
+ + + + + + + + + + + +
+ +

CAN Messages (by ID)

@@ -886,6 +1098,10 @@ const char index_html[] PROGMEM = R"rawliteral( let commentingFile = ''; // hasInitialSync 제거 - 매번 자동 동기화 + // 🎯 Auto Trigger 전역 변수 + let startTriggers = []; + let stopTriggers = []; + function updateCurrentTime() { const now = new Date(); const timeStr = now.toLocaleTimeString('ko-KR', {hour12: false}); @@ -954,7 +1170,11 @@ const char index_html[] PROGMEM = R"rawliteral( setTimeout(function() { refreshFiles(); }, 1000); - }; + + + // 🎯 Auto Trigger 설정 로드 + setTimeout(loadAutoTriggers, 500); + }; ws.onclose = function() { console.log('WebSocket disconnected'); @@ -968,25 +1188,39 @@ const char index_html[] PROGMEM = R"rawliteral( }; ws.onmessage = function(event) { + // 🆕 바이너리/비정상 데이터 필터링 + if (typeof event.data !== 'string') { + console.warn('Non-string WebSocket data received, ignoring'); + return; + } + + // 🆕 빈 데이터 필터링 + if (!event.data || event.data.trim() === '') { + return; + } + try { const data = JSON.parse(event.data); - // ⭐ 디버그: 받은 메시지 타입 출력 + // 🆕 유효한 JSON 객체인지 확인 + if (!data || typeof data !== 'object') { + console.warn('Invalid JSON object received'); + return; + } + console.log('WebSocket message received:', data.type); - // ★ 수정: 'update' 타입 추가 (서버에서 보내는 타입과 일치) if (data.type === 'status' || data.type === 'update') { updateStatus(data); - // CAN 메시지 배열이 있으면 처리 if (data.messages && data.messages.length > 0) { updateCanMessages(data.messages); } } else if (data.type === 'canBatch') { updateCanBatch(data.messages); } else if (data.type === 'files') { - console.log('Files received:', data.files ? data.files.length : 0, 'files'); // ⭐ 디버그 - console.log('Files data:', data.files); // ⭐ 디버그 - updateFileList(data.files || data.list); // ★ 수정: 'list' 키도 확인 + console.log('Files received:', data.files ? data.files.length : 0, 'files'); + console.log('Files data:', data.files); + updateFileList(data.files || data.list); } else if (data.type === 'deleteResult') { handleDeleteResult(data); } else if (data.type === 'timeSyncResult') { @@ -999,14 +1233,62 @@ const char index_html[] PROGMEM = R"rawliteral( } } else if (data.type === 'hwReset') { console.log('✅ 하드웨어 리셋 명령 전송됨 - ESP32 재부팅 중...'); - // ESP32가 재부팅되므로 WebSocket 연결 끊김 } + // 🎯 Auto Trigger 메시지 처리 - try 블록 내부로 이동! + else if (data.type === 'autoTriggers') { + document.getElementById('autoTriggerEnabled').checked = data.enabled; + document.getElementById('startLogic').value = data.startLogic; + document.getElementById('stopLogic').value = data.stopLogic; + + // 🆕 로그 형식 설정 + if (data.logFormat) { + document.getElementById('autoTriggerFormat').value = data.logFormat; + } + + startTriggers = data.startTriggers || []; + stopTriggers = data.stopTriggers || []; + + if (data.enabled) { + document.getElementById('autoTriggerFormatGroup').style.display = 'block'; + document.getElementById('startTriggerGroup').style.display = 'block'; + document.getElementById('stopTriggerGroup').style.display = 'block'; + document.getElementById('saveTriggerBtn').style.display = 'block'; + document.getElementById('autoTriggerStatus').textContent = 'Enabled'; + document.getElementById('autoTriggerStatus').className = 'trigger-status active'; + } + + renderStartTriggers(); + renderStopTriggers(); + } + else if (data.type === 'startTriggersSet') { + console.log('✅ Start triggers saved:', data.count); + } + else if (data.type === 'stopTriggersSet') { + console.log('✅ Stop triggers saved:', data.count); + } + + // Auto Trigger 상태 업데이트 (update 메시지에서) + if (data.autoTriggerEnabled !== undefined) { + const status = document.getElementById('autoTriggerStatus'); + if (data.autoTriggerActive) { + status.textContent = 'Active (Triggered)'; + status.className = 'trigger-status active'; + } else if (data.autoTriggerEnabled) { + status.textContent = 'Enabled (Waiting)'; + status.className = 'trigger-status active'; + } else { + status.textContent = 'Disabled'; + status.className = 'trigger-status inactive'; + } + } + } catch (e) { console.error('Parse error:', e); } - }; + } } + function updateStatus(data) { // 로깅 상태 const loggingCard = document.getElementById('logging-status'); @@ -1588,6 +1870,172 @@ const char index_html[] PROGMEM = R"rawliteral( }); initWebSocket(); + + // ============================================ + // 🎯 Auto Trigger 관련 함수 + // ============================================ + + + // Auto Trigger 토글 + function toggleAutoTrigger() { + const enabled = document.getElementById('autoTriggerEnabled').checked; + + const formatGroup = document.getElementById('autoTriggerFormatGroup'); + const startGroup = document.getElementById('startTriggerGroup'); + const stopGroup = document.getElementById('stopTriggerGroup'); + const saveBtn = document.getElementById('saveTriggerBtn'); + const status = document.getElementById('autoTriggerStatus'); + + if (enabled) { + formatGroup.style.display = 'block'; + startGroup.style.display = 'block'; + stopGroup.style.display = 'block'; + saveBtn.style.display = 'block'; + status.textContent = 'Enabled'; + status.className = 'trigger-status active'; + + if (startTriggers.length === 0) addStartTrigger(); + if (stopTriggers.length === 0) addStopTrigger(); + } else { + formatGroup.style.display = 'none'; + startGroup.style.display = 'none'; + stopGroup.style.display = 'none'; + saveBtn.style.display = 'none'; + status.textContent = 'Disabled'; + status.className = 'trigger-status inactive'; + } + + const logFormat = document.getElementById('autoTriggerFormat').value; + ws.send(JSON.stringify({ cmd: 'setAutoTrigger', enabled: enabled, logFormat: logFormat })); + } + + function addStartTrigger() { + const trigger = { + canId: '0x100', + startBit: 0, + bitLength: 8, + op: '==', + value: 0, + enabled: true + }; + startTriggers.push(trigger); + renderStartTriggers(); + } + + function addStopTrigger() { + const trigger = { + canId: '0x100', + startBit: 0, + bitLength: 8, + op: '==', + value: 0, + enabled: true + }; + stopTriggers.push(trigger); + renderStopTriggers(); + } + + function renderStartTriggers() { + const container = document.getElementById('startTriggersList'); + container.innerHTML = ''; + + startTriggers.forEach((trigger, index) => { + const card = document.createElement('div'); + card.className = 'trigger-card'; + card.innerHTML = ` + + + + + + + + `; + container.appendChild(card); + }); + } + + function renderStopTriggers() { + const container = document.getElementById('stopTriggersList'); + container.innerHTML = ''; + + stopTriggers.forEach((trigger, index) => { + const card = document.createElement('div'); + card.className = 'trigger-card'; + card.innerHTML = ` + + + + + + + + `; + container.appendChild(card); + }); + } + + function removeStartTrigger(index) { + if (confirm('이 조건을 삭제하시겠습니까?')) { + startTriggers.splice(index, 1); + renderStartTriggers(); + } + } + + function removeStopTrigger(index) { + if (confirm('이 조건을 삭제하시겠습니까?')) { + stopTriggers.splice(index, 1); + renderStopTriggers(); + } + } + + function saveAutoTriggers() { + const startLogic = document.getElementById('startLogic').value; + const stopLogic = document.getElementById('stopLogic').value; + const logFormat = document.getElementById('autoTriggerFormat').value; + + // 로그 형식 설정 먼저 전송 + ws.send(JSON.stringify({ cmd: 'setAutoTriggerFormat', logFormat: logFormat })); + ws.send(JSON.stringify({ cmd: 'setStartTriggers', logic: startLogic, triggers: startTriggers })); + ws.send(JSON.stringify({ cmd: 'setStopTriggers', logic: stopLogic, triggers: stopTriggers })); + + alert('✅ Auto Trigger 설정이 저장되었습니다!'); + } + + function loadAutoTriggers() { + ws.send(JSON.stringify({ cmd: 'getAutoTriggers' })); + } +