자동로깅
This commit is contained in:
@@ -244,6 +244,34 @@ TaskHandle_t serial2RxTaskHandle = NULL; // ⭐ Serial2 추가
|
|||||||
|
|
||||||
// 로깅 변수
|
// 로깅 변수
|
||||||
volatile bool loggingEnabled = false;
|
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 serialLoggingEnabled = false;
|
||||||
volatile bool serial2LoggingEnabled = false; // ⭐ Serial2 추가
|
volatile bool serial2LoggingEnabled = false; // ⭐ Serial2 추가
|
||||||
volatile bool sdCardReady = false;
|
volatile bool sdCardReady = false;
|
||||||
@@ -616,6 +644,295 @@ void loadSettings() {
|
|||||||
preferences.end();
|
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() {
|
void saveSettings() {
|
||||||
preferences.begin("can-logger", false);
|
preferences.begin("can-logger", false);
|
||||||
preferences.putString("wifi_ssid", wifiSSID);
|
preferences.putString("wifi_ssid", wifiSSID);
|
||||||
@@ -928,6 +1245,9 @@ void canRxTask(void *parameter) {
|
|||||||
|
|
||||||
while (mcp2515.readMessage(&frame) == MCP2515::ERROR_OK) {
|
while (mcp2515.readMessage(&frame) == MCP2515::ERROR_OK) {
|
||||||
struct timeval tv;
|
struct timeval tv;
|
||||||
|
// 🎯 Auto Trigger 체크
|
||||||
|
checkAutoTriggers(frame);
|
||||||
|
|
||||||
gettimeofday(&tv, NULL);
|
gettimeofday(&tv, NULL);
|
||||||
msg.timestamp_us = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec;
|
msg.timestamp_us = (uint64_t)tv.tv_sec * 1000000ULL + tv.tv_usec;
|
||||||
msg.id = frame.can_id & 0x1FFFFFFF;
|
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);
|
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) {
|
else if (strcmp(cmd, "sendOnce") == 0) {
|
||||||
if (currentMcpMode == MCP_MODE_TRANSMIT) {
|
if (currentMcpMode == MCP_MODE_TRANSMIT) {
|
||||||
mcp2515.setNormalMode();
|
mcp2515.setNormalMode();
|
||||||
@@ -2160,13 +2612,28 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length)
|
|||||||
void webUpdateTask(void *parameter) {
|
void webUpdateTask(void *parameter) {
|
||||||
const TickType_t xDelay = pdMS_TO_TICKS(200); // ⭐ 100ms → 200ms (WiFi 안정성 향상)
|
const TickType_t xDelay = pdMS_TO_TICKS(200); // ⭐ 100ms → 200ms (WiFi 안정성 향상)
|
||||||
|
|
||||||
|
// 🆕 초기화 대기 (부팅 직후 안정화)
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(2000));
|
||||||
|
|
||||||
while (1) {
|
while (1) {
|
||||||
webSocket.loop();
|
webSocket.loop();
|
||||||
|
|
||||||
if (webSocket.connectedClients() > 0) {
|
if (webSocket.connectedClients() > 0) {
|
||||||
|
// 🆕 큐가 초기화되었는지 확인
|
||||||
|
if (!canQueue || !serialQueue) {
|
||||||
|
vTaskDelay(xDelay);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
DynamicJsonDocument doc(16384); // ⭐ 4096 → 8192로 증가
|
DynamicJsonDocument doc(16384); // ⭐ 4096 → 8192로 증가
|
||||||
doc["type"] = "update";
|
doc["type"] = "update";
|
||||||
doc["logging"] = loggingEnabled;
|
doc["logging"] = loggingEnabled;
|
||||||
|
|
||||||
|
// 🎯 Auto Trigger 상태
|
||||||
|
doc["autoTriggerEnabled"] = autoTriggerEnabled;
|
||||||
|
doc["autoTriggerActive"] = autoTriggerActive;
|
||||||
|
doc["startTriggerCount"] = startTriggerCount;
|
||||||
|
doc["stopTriggerCount"] = stopTriggerCount;
|
||||||
doc["serialLogging"] = serialLoggingEnabled;
|
doc["serialLogging"] = serialLoggingEnabled;
|
||||||
doc["serial2Logging"] = serial2LoggingEnabled;
|
doc["serial2Logging"] = serial2LoggingEnabled;
|
||||||
doc["totalSerial2Rx"] = totalSerial2RxCount;
|
doc["totalSerial2Rx"] = totalSerial2RxCount;
|
||||||
@@ -2189,9 +2656,9 @@ void webUpdateTask(void *parameter) {
|
|||||||
doc["totalSerialTx"] = totalSerialTxCount;
|
doc["totalSerialTx"] = totalSerialTxCount;
|
||||||
doc["fileSize"] = currentFileSize;
|
doc["fileSize"] = currentFileSize;
|
||||||
doc["serialFileSize"] = currentSerialFileSize;
|
doc["serialFileSize"] = currentSerialFileSize;
|
||||||
doc["queueUsed"] = uxQueueMessagesWaiting(canQueue);
|
doc["queueUsed"] = canQueue ? uxQueueMessagesWaiting(canQueue) : 0; // 🆕 NULL 체크
|
||||||
doc["queueSize"] = CAN_QUEUE_SIZE;
|
doc["queueSize"] = CAN_QUEUE_SIZE;
|
||||||
doc["serialQueueUsed"] = uxQueueMessagesWaiting(serialQueue);
|
doc["serialQueueUsed"] = serialQueue ? uxQueueMessagesWaiting(serialQueue) : 0; // 🆕 NULL 체크
|
||||||
doc["serialQueueSize"] = SERIAL_QUEUE_SIZE;
|
doc["serialQueueSize"] = SERIAL_QUEUE_SIZE;
|
||||||
doc["timeSync"] = timeSyncStatus.synchronized;
|
doc["timeSync"] = timeSyncStatus.synchronized;
|
||||||
doc["rtcAvail"] = timeSyncStatus.rtcAvailable;
|
doc["rtcAvail"] = timeSyncStatus.rtcAvailable;
|
||||||
@@ -2399,6 +2866,9 @@ void setup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
loadSettings();
|
loadSettings();
|
||||||
|
|
||||||
|
// 🎯 Auto Trigger 설정 로드
|
||||||
|
loadAutoTriggerSettings();
|
||||||
analogSetPinAttenuation(MONITORING_VOLT, ADC_11db);
|
analogSetPinAttenuation(MONITORING_VOLT, ADC_11db);
|
||||||
|
|
||||||
pinMode(CAN_INT_PIN, INPUT_PULLUP);
|
pinMode(CAN_INT_PIN, INPUT_PULLUP);
|
||||||
|
|||||||
466
index.h
466
index.h
@@ -675,6 +675,158 @@ const char index_html[] PROGMEM = R"rawliteral(
|
|||||||
padding: 30px;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -829,6 +981,66 @@ const char index_html[] PROGMEM = R"rawliteral(
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 🎯 Auto Trigger 섹션 -->
|
||||||
|
<div class="auto-trigger-section">
|
||||||
|
<h3>
|
||||||
|
🎯 Auto Trigger
|
||||||
|
<span id="autoTriggerStatus" class="trigger-status inactive">Disabled</span>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<!-- Enable/Disable -->
|
||||||
|
<div class="trigger-control">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" id="autoTriggerEnabled" onchange="toggleAutoTrigger()">
|
||||||
|
Enable Auto Trigger
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 🆕 Auto Trigger 로그 형식 선택 -->
|
||||||
|
<div class="trigger-group" id="autoTriggerFormatGroup" style="display: none;">
|
||||||
|
<h4>📁 Log Format</h4>
|
||||||
|
<select id="autoTriggerFormat" style="width: 100%; padding: 8px; border-radius: 5px; border: 1px solid #444; background: #2a2a2a; color: white;">
|
||||||
|
<option value="bin">📦 Binary (BIN) - High Speed</option>
|
||||||
|
<option value="csv">📄 CSV - Human Readable</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 시작 조건 -->
|
||||||
|
<div class="trigger-group" id="startTriggerGroup" style="display: none;">
|
||||||
|
<h4>▶️ Start Logging Conditions</h4>
|
||||||
|
<select id="startLogic">
|
||||||
|
<option value="OR">OR (Any condition matches)</option>
|
||||||
|
<option value="AND">AND (All conditions match)</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<div id="startTriggersList">
|
||||||
|
<!-- 동적 추가 -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn-add-trigger" onclick="addStartTrigger()">➕ Add Start Condition</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 중지 조건 -->
|
||||||
|
<div class="trigger-group" id="stopTriggerGroup" style="display: none;">
|
||||||
|
<h4>⏹️ Stop Logging Conditions</h4>
|
||||||
|
<select id="stopLogic">
|
||||||
|
<option value="OR">OR (Any condition matches)</option>
|
||||||
|
<option value="AND">AND (All conditions match)</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<div id="stopTriggersList">
|
||||||
|
<!-- 동적 추가 -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn-add-trigger" onclick="addStopTrigger()">➕ Add Stop Condition</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn-save-triggers" onclick="saveAutoTriggers()" style="display: none;" id="saveTriggerBtn">
|
||||||
|
💾 Save Auto Trigger Settings
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<h2>CAN Messages (by ID)</h2>
|
<h2>CAN Messages (by ID)</h2>
|
||||||
<div class="can-table-container">
|
<div class="can-table-container">
|
||||||
<table>
|
<table>
|
||||||
@@ -886,6 +1098,10 @@ const char index_html[] PROGMEM = R"rawliteral(
|
|||||||
let commentingFile = '';
|
let commentingFile = '';
|
||||||
// hasInitialSync 제거 - 매번 자동 동기화
|
// hasInitialSync 제거 - 매번 자동 동기화
|
||||||
|
|
||||||
|
// 🎯 Auto Trigger 전역 변수
|
||||||
|
let startTriggers = [];
|
||||||
|
let stopTriggers = [];
|
||||||
|
|
||||||
function updateCurrentTime() {
|
function updateCurrentTime() {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const timeStr = now.toLocaleTimeString('ko-KR', {hour12: false});
|
const timeStr = now.toLocaleTimeString('ko-KR', {hour12: false});
|
||||||
@@ -954,7 +1170,11 @@ const char index_html[] PROGMEM = R"rawliteral(
|
|||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
refreshFiles();
|
refreshFiles();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
};
|
|
||||||
|
|
||||||
|
// 🎯 Auto Trigger 설정 로드
|
||||||
|
setTimeout(loadAutoTriggers, 500);
|
||||||
|
};
|
||||||
|
|
||||||
ws.onclose = function() {
|
ws.onclose = function() {
|
||||||
console.log('WebSocket disconnected');
|
console.log('WebSocket disconnected');
|
||||||
@@ -968,25 +1188,39 @@ const char index_html[] PROGMEM = R"rawliteral(
|
|||||||
};
|
};
|
||||||
|
|
||||||
ws.onmessage = function(event) {
|
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 {
|
try {
|
||||||
const data = JSON.parse(event.data);
|
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);
|
console.log('WebSocket message received:', data.type);
|
||||||
|
|
||||||
// ★ 수정: 'update' 타입 추가 (서버에서 보내는 타입과 일치)
|
|
||||||
if (data.type === 'status' || data.type === 'update') {
|
if (data.type === 'status' || data.type === 'update') {
|
||||||
updateStatus(data);
|
updateStatus(data);
|
||||||
// CAN 메시지 배열이 있으면 처리
|
|
||||||
if (data.messages && data.messages.length > 0) {
|
if (data.messages && data.messages.length > 0) {
|
||||||
updateCanMessages(data.messages);
|
updateCanMessages(data.messages);
|
||||||
}
|
}
|
||||||
} else if (data.type === 'canBatch') {
|
} else if (data.type === 'canBatch') {
|
||||||
updateCanBatch(data.messages);
|
updateCanBatch(data.messages);
|
||||||
} else if (data.type === 'files') {
|
} else if (data.type === 'files') {
|
||||||
console.log('Files received:', data.files ? data.files.length : 0, 'files'); // ⭐ 디버그
|
console.log('Files received:', data.files ? data.files.length : 0, 'files');
|
||||||
console.log('Files data:', data.files); // ⭐ 디버그
|
console.log('Files data:', data.files);
|
||||||
updateFileList(data.files || data.list); // ★ 수정: 'list' 키도 확인
|
updateFileList(data.files || data.list);
|
||||||
} else if (data.type === 'deleteResult') {
|
} else if (data.type === 'deleteResult') {
|
||||||
handleDeleteResult(data);
|
handleDeleteResult(data);
|
||||||
} else if (data.type === 'timeSyncResult') {
|
} else if (data.type === 'timeSyncResult') {
|
||||||
@@ -999,14 +1233,62 @@ const char index_html[] PROGMEM = R"rawliteral(
|
|||||||
}
|
}
|
||||||
} else if (data.type === 'hwReset') {
|
} else if (data.type === 'hwReset') {
|
||||||
console.log('✅ 하드웨어 리셋 명령 전송됨 - ESP32 재부팅 중...');
|
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) {
|
} catch (e) {
|
||||||
console.error('Parse error:', e);
|
console.error('Parse error:', e);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function updateStatus(data) {
|
function updateStatus(data) {
|
||||||
// 로깅 상태
|
// 로깅 상태
|
||||||
const loggingCard = document.getElementById('logging-status');
|
const loggingCard = document.getElementById('logging-status');
|
||||||
@@ -1588,6 +1870,172 @@ const char index_html[] PROGMEM = R"rawliteral(
|
|||||||
});
|
});
|
||||||
|
|
||||||
initWebSocket();
|
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 = `
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" ${trigger.enabled ? 'checked' : ''}
|
||||||
|
onchange="startTriggers[${index}].enabled = this.checked">
|
||||||
|
Enable
|
||||||
|
</label>
|
||||||
|
<input type="text" placeholder="CAN ID (0x100)" value="${trigger.canId}"
|
||||||
|
onchange="startTriggers[${index}].canId = this.value">
|
||||||
|
<input type="number" placeholder="Start Bit" min="0" max="63" value="${trigger.startBit}"
|
||||||
|
onchange="startTriggers[${index}].startBit = parseInt(this.value)">
|
||||||
|
<input type="number" placeholder="Bit Length" min="1" max="64" value="${trigger.bitLength}"
|
||||||
|
onchange="startTriggers[${index}].bitLength = parseInt(this.value)">
|
||||||
|
<select onchange="startTriggers[${index}].op = this.value">
|
||||||
|
<option value="==" ${trigger.op === '==' ? 'selected' : ''}> == </option>
|
||||||
|
<option value="!=" ${trigger.op === '!=' ? 'selected' : ''}> != </option>
|
||||||
|
<option value=">" ${trigger.op === '>' ? 'selected' : ''}> > </option>
|
||||||
|
<option value="<" ${trigger.op === '<' ? 'selected' : ''}> < </option>
|
||||||
|
<option value=">=" ${trigger.op === '>=' ? 'selected' : ''}> >= </option>
|
||||||
|
<option value="<=" ${trigger.op === '<=' ? 'selected' : ''}> <= </option>
|
||||||
|
</select>
|
||||||
|
<input type="number" placeholder="Value" value="${trigger.value}"
|
||||||
|
onchange="startTriggers[${index}].value = parseInt(this.value)">
|
||||||
|
<button class="btn-delete" onclick="removeStartTrigger(${index})">🗑️</button>
|
||||||
|
`;
|
||||||
|
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 = `
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" ${trigger.enabled ? 'checked' : ''}
|
||||||
|
onchange="stopTriggers[${index}].enabled = this.checked">
|
||||||
|
Enable
|
||||||
|
</label>
|
||||||
|
<input type="text" placeholder="CAN ID (0x100)" value="${trigger.canId}"
|
||||||
|
onchange="stopTriggers[${index}].canId = this.value">
|
||||||
|
<input type="number" placeholder="Start Bit" min="0" max="63" value="${trigger.startBit}"
|
||||||
|
onchange="stopTriggers[${index}].startBit = parseInt(this.value)">
|
||||||
|
<input type="number" placeholder="Bit Length" min="1" max="64" value="${trigger.bitLength}"
|
||||||
|
onchange="stopTriggers[${index}].bitLength = parseInt(this.value)">
|
||||||
|
<select onchange="stopTriggers[${index}].op = this.value">
|
||||||
|
<option value="==" ${trigger.op === '==' ? 'selected' : ''}> == </option>
|
||||||
|
<option value="!=" ${trigger.op === '!=' ? 'selected' : ''}> != </option>
|
||||||
|
<option value=">" ${trigger.op === '>' ? 'selected' : ''}> > </option>
|
||||||
|
<option value="<" ${trigger.op === '<' ? 'selected' : ''}> < </option>
|
||||||
|
<option value=">=" ${trigger.op === '>=' ? 'selected' : ''}> >= </option>
|
||||||
|
<option value="<=" ${trigger.op === '<=' ? 'selected' : ''}> <= </option>
|
||||||
|
</select>
|
||||||
|
<input type="number" placeholder="Value" value="${trigger.value}"
|
||||||
|
onchange="stopTriggers[${index}].value = parseInt(this.value)">
|
||||||
|
<button class="btn-delete" onclick="removeStopTrigger(${index})">🗑️</button>
|
||||||
|
`;
|
||||||
|
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' }));
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user