정상 로깅 확인 soft reset 추가
This commit is contained in:
@@ -287,7 +287,64 @@ int commentCount = 0;
|
|||||||
// Forward declarations
|
// Forward declarations
|
||||||
void IRAM_ATTR canISR();
|
void IRAM_ATTR canISR();
|
||||||
void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length);
|
void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length);
|
||||||
|
void resetMCP2515();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
void resetMCP2515() {
|
||||||
|
Serial.println("🔄 MCP2515 리셋 시작...");
|
||||||
|
|
||||||
|
// 1. 로깅 중지 (진행 중이면)
|
||||||
|
if (loggingEnabled) {
|
||||||
|
// 버퍼 플러시 및 파일 닫기
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. CAN 큐 비우기
|
||||||
|
CANMessage tempMsg;
|
||||||
|
while (xQueueReceive(canQueue, &tempMsg, 0) == pdTRUE) {
|
||||||
|
// 큐에서 모든 메시지 제거
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. MCP2515 하드 리셋
|
||||||
|
digitalWrite(HSPI_CS, LOW);
|
||||||
|
delay(10);
|
||||||
|
digitalWrite(HSPI_CS, HIGH);
|
||||||
|
delay(50);
|
||||||
|
|
||||||
|
// 4. MCP2515 재초기화
|
||||||
|
mcp2515.reset();
|
||||||
|
delay(100);
|
||||||
|
mcp2515.setBitrate(currentCanSpeed, MCP_8MHZ);
|
||||||
|
delay(10);
|
||||||
|
|
||||||
|
// 5. 모드 설정 (Normal/Loopback/Listen Only)
|
||||||
|
if (currentMcpMode == MCP_MODE_NORMAL) {
|
||||||
|
mcp2515.setNormalMode();
|
||||||
|
} else if (currentMcpMode == MCP_MODE_LOOPBACK) {
|
||||||
|
mcp2515.setLoopbackMode();
|
||||||
|
} else {
|
||||||
|
mcp2515.setListenOnlyMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 버퍼 클리어
|
||||||
|
struct can_frame dummyFrame;
|
||||||
|
while (mcp2515.readMessage(&dummyFrame) == MCP2515::ERROR_OK) {
|
||||||
|
// MCP2515 수신 버퍼 비우기
|
||||||
|
}
|
||||||
|
mcp2515.clearRXnOVRFlags();
|
||||||
|
|
||||||
|
// 7. 통계 리셋
|
||||||
|
totalMsgCount = 0;
|
||||||
|
lastMsgCount = 0;
|
||||||
|
msgPerSecond = 0;
|
||||||
|
|
||||||
|
// 8. 최근 메시지 테이블 클리어
|
||||||
|
for (int i = 0; i < RECENT_MSG_COUNT; i++) {
|
||||||
|
recentData[i].count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.println("✅ MCP2515 리셋 완료!");
|
||||||
|
}
|
||||||
// ========================================
|
// ========================================
|
||||||
// PSRAM 초기화 함수
|
// PSRAM 초기화 함수
|
||||||
// ========================================
|
// ========================================
|
||||||
@@ -914,12 +971,15 @@ void sdWriteTask(void *parameter) {
|
|||||||
|
|
||||||
// CAN 로깅
|
// CAN 로깅
|
||||||
if (loggingEnabled && sdCardReady) {
|
if (loggingEnabled && sdCardReady) {
|
||||||
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(50)) == pdTRUE) {
|
// ⭐⭐⭐ 뮤텍스 타임아웃 1ms로 감소 (블로킹 방지)
|
||||||
|
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1)) == pdTRUE) {
|
||||||
if (canLogFormatCSV) {
|
if (canLogFormatCSV) {
|
||||||
char csvLine[128];
|
char csvLine[128];
|
||||||
uint64_t relativeTime = canMsg.timestamp_us - canLogStartTime;
|
uint64_t relativeTime = canMsg.timestamp_us - canLogStartTime;
|
||||||
char dataStr[32];
|
char dataStr[32];
|
||||||
int dataLen = 0;
|
int dataLen = 0;
|
||||||
|
static uint32_t csvReopenCounter = 0;
|
||||||
|
|
||||||
for (int i = 0; i < canMsg.dlc; i++) {
|
for (int i = 0; i < canMsg.dlc; i++) {
|
||||||
dataLen += sprintf(&dataStr[dataLen], "%02X", canMsg.data[i]);
|
dataLen += sprintf(&dataStr[dataLen], "%02X", canMsg.data[i]);
|
||||||
if (i < canMsg.dlc - 1) dataStr[dataLen++] = ' ';
|
if (i < canMsg.dlc - 1) dataStr[dataLen++] = ' ';
|
||||||
@@ -931,23 +991,40 @@ void sdWriteTask(void *parameter) {
|
|||||||
relativeTime, canMsg.id, canMsg.dlc, dataStr);
|
relativeTime, canMsg.id, canMsg.dlc, dataStr);
|
||||||
|
|
||||||
if (logFile) {
|
if (logFile) {
|
||||||
logFile.write((uint8_t*)csvLine, lineLen);
|
size_t written = logFile.write((uint8_t*)csvLine, lineLen);
|
||||||
currentFileSize += lineLen;
|
currentFileSize += lineLen;
|
||||||
|
|
||||||
static int csvFlushCounter = 0;
|
static int csvFlushCounter = 0;
|
||||||
if (++csvFlushCounter >= 20) { // 50 → 20으로 더 자주 플러시
|
if (++csvFlushCounter >= 50) { // ⭐ 20 → 50 (너무 자주 플러시하면 느림)
|
||||||
logFile.flush();
|
logFile.flush();
|
||||||
csvFlushCounter = 0;
|
csvFlushCounter = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ⭐⭐⭐ 500개마다 파일 재오픈 (핵심!)
|
||||||
|
if (++csvReopenCounter >= 500) {
|
||||||
|
logFile.close();
|
||||||
|
logFile = SD.open(currentFilename, FILE_APPEND);
|
||||||
|
if (logFile) {
|
||||||
|
Serial.printf("✓ CSV 파일 재오픈: %s (%lu bytes)\n", currentFilename, currentFileSize);
|
||||||
|
} else {
|
||||||
|
Serial.println("✗ CSV 파일 재오픈 실패!");
|
||||||
|
loggingEnabled = false;
|
||||||
|
}
|
||||||
|
csvReopenCounter = 0;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// BIN 형식
|
// BIN 형식
|
||||||
|
static uint32_t binMsgCounter = 0;
|
||||||
|
static uint32_t binReopenCounter = 0;
|
||||||
|
|
||||||
// ⭐⭐⭐ 1단계: 버퍼 가득 찼으면 먼저 플러시
|
// ⭐⭐⭐ 1단계: 버퍼 가득 찼으면 먼저 플러시
|
||||||
if (bufferIndex + sizeof(CANMessage) > FILE_BUFFER_SIZE) {
|
if (bufferIndex + sizeof(CANMessage) > FILE_BUFFER_SIZE) {
|
||||||
if (logFile) {
|
if (logFile) {
|
||||||
size_t written = logFile.write(fileBuffer, bufferIndex);
|
size_t written = logFile.write(fileBuffer, bufferIndex);
|
||||||
logFile.flush();
|
logFile.flush();
|
||||||
Serial.printf("✓ BIN 버퍼 플러시: %d bytes written\n", written);
|
Serial.printf("✓ BIN 버퍼 플러시 (FULL): %d bytes\n", written);
|
||||||
bufferIndex = 0;
|
bufferIndex = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -956,6 +1033,39 @@ void sdWriteTask(void *parameter) {
|
|||||||
memcpy(&fileBuffer[bufferIndex], &canMsg, sizeof(CANMessage));
|
memcpy(&fileBuffer[bufferIndex], &canMsg, sizeof(CANMessage));
|
||||||
bufferIndex += sizeof(CANMessage);
|
bufferIndex += sizeof(CANMessage);
|
||||||
currentFileSize += sizeof(CANMessage);
|
currentFileSize += sizeof(CANMessage);
|
||||||
|
binMsgCounter++;
|
||||||
|
binReopenCounter++;
|
||||||
|
|
||||||
|
// ⭐⭐⭐ 3단계: 100개 메시지마다 주기적 플러시
|
||||||
|
if (binMsgCounter % 100 == 0) {
|
||||||
|
if (logFile && bufferIndex > 0) {
|
||||||
|
size_t written = logFile.write(fileBuffer, bufferIndex);
|
||||||
|
logFile.flush();
|
||||||
|
Serial.printf("✓ BIN 주기 플러시: %d bytes (메시지: %d)\n", written, binMsgCounter);
|
||||||
|
bufferIndex = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ⭐⭐⭐ 4단계: 500개마다 파일 재오픈 (핵심!)
|
||||||
|
if (binReopenCounter >= 500) {
|
||||||
|
// 버퍼 먼저 플러시
|
||||||
|
if (logFile && bufferIndex > 0) {
|
||||||
|
logFile.write(fileBuffer, bufferIndex);
|
||||||
|
logFile.flush();
|
||||||
|
bufferIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 파일 닫고 다시 열기
|
||||||
|
logFile.close();
|
||||||
|
logFile = SD.open(currentFilename, FILE_APPEND);
|
||||||
|
if (logFile) {
|
||||||
|
Serial.printf("✓ BIN 파일 재오픈: %s (%lu bytes)\n", currentFilename, currentFileSize);
|
||||||
|
} else {
|
||||||
|
Serial.println("✗ BIN 파일 재오픈 실패!");
|
||||||
|
loggingEnabled = false;
|
||||||
|
}
|
||||||
|
binReopenCounter = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
xSemaphoreGive(sdMutex);
|
xSemaphoreGive(sdMutex);
|
||||||
}
|
}
|
||||||
@@ -1308,9 +1418,14 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length)
|
|||||||
loggingEnabled = true;
|
loggingEnabled = true;
|
||||||
bufferIndex = 0;
|
bufferIndex = 0;
|
||||||
currentFileSize = logFile.size();
|
currentFileSize = logFile.size();
|
||||||
Serial.printf("✓ 로깅 파일 열림 (APPEND): %s\n", currentFilename);
|
Serial.printf("✅ 로깅 시작!\n");
|
||||||
|
Serial.printf(" 파일: %s\n", currentFilename);
|
||||||
|
Serial.printf(" 형식: %s\n", canLogFormatCSV ? "CSV" : "BIN");
|
||||||
|
Serial.printf(" 초기 크기: %lu bytes\n", currentFileSize);
|
||||||
|
Serial.printf(" sdCardReady: %d\n", sdCardReady);
|
||||||
} else {
|
} else {
|
||||||
Serial.println("✗ APPEND 모드로 파일 열기 실패");
|
Serial.println("✗ APPEND 모드로 파일 열기 실패");
|
||||||
|
Serial.printf(" 파일명: %s\n", currentFilename);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Serial.println("✗ 파일 생성 실패");
|
Serial.println("✗ 파일 생성 실패");
|
||||||
@@ -1322,18 +1437,35 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length)
|
|||||||
}
|
}
|
||||||
else if (strcmp(cmd, "stopLogging") == 0) {
|
else if (strcmp(cmd, "stopLogging") == 0) {
|
||||||
if (loggingEnabled) {
|
if (loggingEnabled) {
|
||||||
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
|
Serial.println("🛑 로깅 중지 요청...");
|
||||||
|
|
||||||
|
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(2000)) == pdTRUE) {
|
||||||
|
// ⭐⭐⭐ BIN 형식: 버퍼에 남은 데이터 강제 플러시
|
||||||
if (bufferIndex > 0 && logFile) {
|
if (bufferIndex > 0 && logFile) {
|
||||||
logFile.write(fileBuffer, bufferIndex);
|
size_t written = logFile.write(fileBuffer, bufferIndex);
|
||||||
|
logFile.flush();
|
||||||
|
Serial.printf("✓ 최종 플러시: %d bytes\n", written);
|
||||||
bufferIndex = 0;
|
bufferIndex = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ⭐⭐⭐ CSV 형식: 최종 플러시
|
||||||
|
if (canLogFormatCSV && logFile) {
|
||||||
|
logFile.flush();
|
||||||
|
Serial.println("✓ CSV 최종 플러시");
|
||||||
|
}
|
||||||
|
|
||||||
if (logFile) {
|
if (logFile) {
|
||||||
|
size_t finalSize = logFile.size();
|
||||||
logFile.close();
|
logFile.close();
|
||||||
|
Serial.printf("✓ 파일 닫힘: %s (%lu bytes)\n", currentFilename, finalSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
loggingEnabled = false;
|
loggingEnabled = false;
|
||||||
|
currentFilename[0] = '\0';
|
||||||
|
bufferIndex = 0;
|
||||||
xSemaphoreGive(sdMutex);
|
xSemaphoreGive(sdMutex);
|
||||||
|
} else {
|
||||||
|
Serial.println("✗ sdMutex 획득 실패!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1761,6 +1893,97 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length)
|
|||||||
mcp2515.setListenOnlyMode();
|
mcp2515.setListenOnlyMode();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (strcmp(cmd, "addSequence") == 0) {
|
||||||
|
// ⭐⭐⭐ Sequence 저장 기능
|
||||||
|
if (sequenceCount >= MAX_SEQUENCES) {
|
||||||
|
Serial.println("✗ Sequence 저장 실패: 최대 개수 초과");
|
||||||
|
DynamicJsonDocument response(256);
|
||||||
|
response["type"] = "error";
|
||||||
|
response["message"] = "Maximum sequences reached";
|
||||||
|
String json;
|
||||||
|
serializeJson(response, json);
|
||||||
|
webSocket.sendTXT(num, json);
|
||||||
|
} else {
|
||||||
|
const char* name = doc["name"];
|
||||||
|
int repeatMode = doc["repeatMode"] | 0;
|
||||||
|
int repeatCount = doc["repeatCount"] | 1;
|
||||||
|
JsonArray stepsArray = doc["steps"];
|
||||||
|
|
||||||
|
if (name && stepsArray.size() > 0) {
|
||||||
|
CANSequence* seq = &sequences[sequenceCount];
|
||||||
|
|
||||||
|
// 이름 복사
|
||||||
|
strncpy(seq->name, name, sizeof(seq->name) - 1);
|
||||||
|
seq->name[sizeof(seq->name) - 1] = '\0';
|
||||||
|
|
||||||
|
// Repeat 설정
|
||||||
|
seq->repeatMode = repeatMode;
|
||||||
|
seq->repeatCount = repeatCount;
|
||||||
|
|
||||||
|
// Steps 복사
|
||||||
|
seq->stepCount = 0;
|
||||||
|
for (JsonObject stepObj : stepsArray) {
|
||||||
|
if (seq->stepCount >= 20) break; // 최대 20개
|
||||||
|
|
||||||
|
SequenceStep* step = &seq->steps[seq->stepCount];
|
||||||
|
|
||||||
|
// ID 파싱 (0x 제거)
|
||||||
|
const char* idStr = stepObj["id"];
|
||||||
|
if (idStr) {
|
||||||
|
if (strncmp(idStr, "0x", 2) == 0 || strncmp(idStr, "0X", 2) == 0) {
|
||||||
|
step->canId = strtoul(idStr + 2, NULL, 16);
|
||||||
|
} else {
|
||||||
|
step->canId = strtoul(idStr, NULL, 16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
step->extended = stepObj["ext"] | false;
|
||||||
|
step->dlc = stepObj["dlc"] | 8;
|
||||||
|
|
||||||
|
// Data 배열 복사
|
||||||
|
JsonArray dataArray = stepObj["data"];
|
||||||
|
for (int i = 0; i < 8 && i < dataArray.size(); i++) {
|
||||||
|
step->data[i] = dataArray[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
step->delayMs = stepObj["delay"] | 0;
|
||||||
|
|
||||||
|
seq->stepCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
sequenceCount++;
|
||||||
|
|
||||||
|
// SD 카드에 저장
|
||||||
|
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
|
||||||
|
File seqFile = SD.open("/sequences.dat", FILE_WRITE);
|
||||||
|
if (seqFile) {
|
||||||
|
seqFile.write((uint8_t*)&sequenceCount, sizeof(sequenceCount));
|
||||||
|
seqFile.write((uint8_t*)sequences, sizeof(CANSequence) * sequenceCount);
|
||||||
|
seqFile.close();
|
||||||
|
Serial.printf("✓ Sequence 저장 완료: %s (Steps: %d)\n", name, seq->stepCount);
|
||||||
|
} else {
|
||||||
|
Serial.println("✗ sequences.dat 열기 실패");
|
||||||
|
}
|
||||||
|
xSemaphoreGive(sdMutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 성공 응답
|
||||||
|
DynamicJsonDocument response(256);
|
||||||
|
response["type"] = "sequenceSaved";
|
||||||
|
response["name"] = name;
|
||||||
|
response["steps"] = seq->stepCount;
|
||||||
|
String json;
|
||||||
|
serializeJson(response, json);
|
||||||
|
webSocket.sendTXT(num, json);
|
||||||
|
|
||||||
|
// Sequence 목록 다시 전송
|
||||||
|
delay(100);
|
||||||
|
webSocket.sendTXT(num, "{\"cmd\":\"getSequences\"}");
|
||||||
|
} else {
|
||||||
|
Serial.println("✗ Sequence 저장 실패: 잘못된 데이터");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
else if (strcmp(cmd, "getSequences") == 0) {
|
else if (strcmp(cmd, "getSequences") == 0) {
|
||||||
DynamicJsonDocument response(3072);
|
DynamicJsonDocument response(3072);
|
||||||
response["type"] = "sequences";
|
response["type"] = "sequences";
|
||||||
@@ -1778,6 +2001,149 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length)
|
|||||||
serializeJson(response, json);
|
serializeJson(response, json);
|
||||||
webSocket.sendTXT(num, json);
|
webSocket.sendTXT(num, json);
|
||||||
}
|
}
|
||||||
|
else if (strcmp(cmd, "startSequence") == 0) {
|
||||||
|
// ⭐⭐⭐ Sequence 실행
|
||||||
|
int index = doc["index"] | -1;
|
||||||
|
|
||||||
|
if (index >= 0 && index < sequenceCount) {
|
||||||
|
seqRuntime.running = true;
|
||||||
|
seqRuntime.activeSequenceIndex = index;
|
||||||
|
seqRuntime.currentStep = 0;
|
||||||
|
seqRuntime.currentRepeat = 0;
|
||||||
|
seqRuntime.lastStepTime = millis();
|
||||||
|
|
||||||
|
Serial.printf("✓ Sequence 시작: %s (index: %d)\n", sequences[index].name, index);
|
||||||
|
|
||||||
|
// 성공 응답
|
||||||
|
DynamicJsonDocument response(256);
|
||||||
|
response["type"] = "sequenceStarted";
|
||||||
|
response["index"] = index;
|
||||||
|
response["name"] = sequences[index].name;
|
||||||
|
String json;
|
||||||
|
serializeJson(response, json);
|
||||||
|
webSocket.sendTXT(num, json);
|
||||||
|
} else {
|
||||||
|
Serial.printf("✗ Sequence 시작 실패: 잘못된 index %d\n", index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (strcmp(cmd, "stopSequence") == 0) {
|
||||||
|
// ⭐⭐⭐ Sequence 중지
|
||||||
|
if (seqRuntime.running) {
|
||||||
|
Serial.println("✓ Sequence 중지됨");
|
||||||
|
seqRuntime.running = false;
|
||||||
|
seqRuntime.activeSequenceIndex = -1;
|
||||||
|
|
||||||
|
// 성공 응답
|
||||||
|
DynamicJsonDocument response(256);
|
||||||
|
response["type"] = "sequenceStopped";
|
||||||
|
String json;
|
||||||
|
serializeJson(response, json);
|
||||||
|
webSocket.sendTXT(num, json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (strcmp(cmd, "removeSequence") == 0) {
|
||||||
|
// ⭐⭐⭐ Sequence 삭제
|
||||||
|
int index = doc["index"] | -1;
|
||||||
|
|
||||||
|
if (index >= 0 && index < sequenceCount) {
|
||||||
|
Serial.printf("✓ Sequence 삭제: %s (index: %d)\n", sequences[index].name, index);
|
||||||
|
|
||||||
|
// 배열에서 제거 (뒤의 항목들을 앞으로 이동)
|
||||||
|
for (int i = index; i < sequenceCount - 1; i++) {
|
||||||
|
memcpy(&sequences[i], &sequences[i + 1], sizeof(CANSequence));
|
||||||
|
}
|
||||||
|
|
||||||
|
sequenceCount--;
|
||||||
|
|
||||||
|
// SD 카드에 저장
|
||||||
|
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
|
||||||
|
File seqFile = SD.open("/sequences.dat", FILE_WRITE);
|
||||||
|
if (seqFile) {
|
||||||
|
seqFile.write((uint8_t*)&sequenceCount, sizeof(sequenceCount));
|
||||||
|
seqFile.write((uint8_t*)sequences, sizeof(CANSequence) * sequenceCount);
|
||||||
|
seqFile.close();
|
||||||
|
Serial.printf("✓ SD 카드 업데이트: %d개 sequence\n", sequenceCount);
|
||||||
|
}
|
||||||
|
xSemaphoreGive(sdMutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 성공 응답
|
||||||
|
DynamicJsonDocument response(256);
|
||||||
|
response["type"] = "sequenceDeleted";
|
||||||
|
response["index"] = index;
|
||||||
|
String json;
|
||||||
|
serializeJson(response, json);
|
||||||
|
webSocket.sendTXT(num, json);
|
||||||
|
|
||||||
|
// Sequence 목록 업데이트
|
||||||
|
delay(100);
|
||||||
|
webSocket.sendTXT(num, "{\"cmd\":\"getSequences\"}");
|
||||||
|
} else {
|
||||||
|
Serial.printf("✗ Sequence 삭제 실패: 잘못된 index %d\n", index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (strcmp(cmd, "getSequenceDetail") == 0) {
|
||||||
|
// ⭐⭐⭐ Sequence 상세 정보 (Edit용)
|
||||||
|
int index = doc["index"] | -1;
|
||||||
|
|
||||||
|
if (index >= 0 && index < sequenceCount) {
|
||||||
|
DynamicJsonDocument response(4096);
|
||||||
|
response["type"] = "sequenceDetail";
|
||||||
|
response["index"] = index;
|
||||||
|
response["name"] = sequences[index].name;
|
||||||
|
response["repeatMode"] = sequences[index].repeatMode;
|
||||||
|
response["repeatCount"] = sequences[index].repeatCount;
|
||||||
|
|
||||||
|
JsonArray stepsArray = response.createNestedArray("steps");
|
||||||
|
for (int i = 0; i < sequences[index].stepCount; i++) {
|
||||||
|
SequenceStep* step = &sequences[index].steps[i];
|
||||||
|
JsonObject stepObj = stepsArray.createNestedObject();
|
||||||
|
|
||||||
|
char idStr[12];
|
||||||
|
sprintf(idStr, "0x%X", step->canId);
|
||||||
|
stepObj["id"] = idStr;
|
||||||
|
stepObj["ext"] = step->extended;
|
||||||
|
stepObj["dlc"] = step->dlc;
|
||||||
|
|
||||||
|
JsonArray dataArray = stepObj.createNestedArray("data");
|
||||||
|
for (int j = 0; j < 8; j++) {
|
||||||
|
dataArray.add(step->data[j]);
|
||||||
|
}
|
||||||
|
|
||||||
|
stepObj["delay"] = step->delayMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
String json;
|
||||||
|
serializeJson(response, json);
|
||||||
|
webSocket.sendTXT(num, json);
|
||||||
|
|
||||||
|
Serial.printf("✓ Sequence 상세 전송: %s (index: %d)\n", sequences[index].name, index);
|
||||||
|
} else {
|
||||||
|
Serial.printf("✗ Sequence 상세 조회 실패: 잘못된 index %d\n", index);
|
||||||
|
}
|
||||||
|
}else if (strcmp(cmd, "hwReset") == 0) {
|
||||||
|
// ⭐⭐⭐ ESP32 하드웨어 리셋 (재부팅)
|
||||||
|
Serial.println("📨 하드웨어 리셋 요청 수신");
|
||||||
|
Serial.println("🔄 ESP32 재부팅 중...");
|
||||||
|
Serial.println("");
|
||||||
|
Serial.println("========================================");
|
||||||
|
Serial.println(" ESP32 REBOOTING...");
|
||||||
|
Serial.println("========================================");
|
||||||
|
|
||||||
|
// 응답 전송
|
||||||
|
DynamicJsonDocument response(256);
|
||||||
|
response["type"] = "hwReset";
|
||||||
|
response["success"] = true;
|
||||||
|
String json;
|
||||||
|
serializeJson(response, json);
|
||||||
|
webSocket.sendTXT(num, json);
|
||||||
|
|
||||||
|
// 약간의 지연 후 재부팅 (응답 전송 시간 확보)
|
||||||
|
delay(100);
|
||||||
|
|
||||||
|
// ESP32 재부팅
|
||||||
|
ESP.restart();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2212,13 +2578,13 @@ void setup() {
|
|||||||
|
|
||||||
// Task 생성
|
// Task 생성
|
||||||
xTaskCreatePinnedToCore(canRxTask, "CAN_RX", 8192, NULL, configMAX_PRIORITIES - 1, &canRxTaskHandle, 1); // Core 0, Pri 24
|
xTaskCreatePinnedToCore(canRxTask, "CAN_RX", 8192, NULL, configMAX_PRIORITIES - 1, &canRxTaskHandle, 1); // Core 0, Pri 24
|
||||||
xTaskCreatePinnedToCore(sdWriteTask, "SD_WRITE", 12576, NULL, 6, &sdWriteTaskHandle, 0);
|
xTaskCreatePinnedToCore(sdWriteTask, "SD_WRITE", 18576, NULL, 6, &sdWriteTaskHandle, 0);
|
||||||
xTaskCreatePinnedToCore(sequenceTask, "SEQ", 4096, NULL, 2, NULL, 1);
|
xTaskCreatePinnedToCore(sequenceTask, "SEQ", 4096, NULL, 2, NULL, 1);
|
||||||
xTaskCreatePinnedToCore(serialRxTask, "SERIAL_RX", 6144, NULL, 5, &serialRxTaskHandle, 0);
|
xTaskCreatePinnedToCore(serialRxTask, "SERIAL_RX", 6144, NULL, 5, &serialRxTaskHandle, 0);
|
||||||
xTaskCreatePinnedToCore(serial2RxTask, "SERIAL2_RX", 6144, NULL, 5, &serial2RxTaskHandle, 0); // ⭐ Serial2
|
xTaskCreatePinnedToCore(serial2RxTask, "SERIAL2_RX", 6144, NULL, 5, &serial2RxTaskHandle, 0); // ⭐ Serial2
|
||||||
xTaskCreatePinnedToCore(txTask, "TX", 4096, NULL, 3, NULL, 1);
|
xTaskCreatePinnedToCore(txTask, "TX", 4096, NULL, 3, NULL, 1);
|
||||||
xTaskCreatePinnedToCore(webUpdateTask, "WEB_UPDATE", 16384, NULL, 5, &webTaskHandle, 0); // ⭐ 10240 → 16384
|
xTaskCreatePinnedToCore(webUpdateTask, "WEB_UPDATE", 10384, NULL, 2, &webTaskHandle, 0); // ⭐ 10240 → 16384
|
||||||
xTaskCreatePinnedToCore(sdMonitorTask, "SD_MONITOR", 4096, NULL, 1, NULL, 0);
|
xTaskCreatePinnedToCore(sdMonitorTask, "SD_MONITOR", 4096, NULL, 1, NULL, 1);
|
||||||
|
|
||||||
if (timeSyncStatus.rtcAvailable) {
|
if (timeSyncStatus.rtcAvailable) {
|
||||||
xTaskCreatePinnedToCore(rtcSyncTask, "RTC_SYNC", 3072, NULL, 0, &rtcTaskHandle, 0);
|
xTaskCreatePinnedToCore(rtcSyncTask, "RTC_SYNC", 3072, NULL, 0, &rtcTaskHandle, 0);
|
||||||
|
|||||||
64
index.h
64
index.h
@@ -75,7 +75,7 @@ const char index_html[] PROGMEM = R"rawliteral(
|
|||||||
/* 전력 경고 배너 */
|
/* 전력 경고 배너 */
|
||||||
.power-warning {
|
.power-warning {
|
||||||
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
|
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
|
||||||
color: #666;
|
color: white; /* ⭐ #666 → white */
|
||||||
padding: 12px 20px;
|
padding: 12px 20px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
@@ -339,7 +339,7 @@ const char index_html[] PROGMEM = R"rawliteral(
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
color: #666;
|
color: white; /* ⭐ #666 → white */
|
||||||
}
|
}
|
||||||
.control-row button:hover {
|
.control-row button:hover {
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
@@ -360,7 +360,7 @@ const char index_html[] PROGMEM = R"rawliteral(
|
|||||||
}
|
}
|
||||||
thead {
|
thead {
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
color: #666;
|
color: white; /* ⭐ #666 → white */
|
||||||
}
|
}
|
||||||
th {
|
th {
|
||||||
padding: 12px 8px;
|
padding: 12px 8px;
|
||||||
@@ -479,21 +479,21 @@ const char index_html[] PROGMEM = R"rawliteral(
|
|||||||
}
|
}
|
||||||
.download-btn {
|
.download-btn {
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
color: #666;
|
color: white; /* ⭐ #666 → white */
|
||||||
}
|
}
|
||||||
.download-btn:hover {
|
.download-btn:hover {
|
||||||
background: linear-gradient(135deg, #5568d3 0%, #66409e 100%);
|
background: linear-gradient(135deg, #5568d3 0%, #66409e 100%);
|
||||||
}
|
}
|
||||||
.comment-btn {
|
.comment-btn {
|
||||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||||
color: #666;
|
color: white; /* ⭐ #666 → white */
|
||||||
}
|
}
|
||||||
.comment-btn:hover {
|
.comment-btn:hover {
|
||||||
background: linear-gradient(135deg, #e77fe8 0%, #e44459 100%);
|
background: linear-gradient(135deg, #e77fe8 0%, #e44459 100%);
|
||||||
}
|
}
|
||||||
.delete-btn {
|
.delete-btn {
|
||||||
background: linear-gradient(135deg, #eb3349 0%, #f45c43 100%);
|
background: linear-gradient(135deg, #eb3349 0%, #f45c43 100%);
|
||||||
color: #666;
|
color: white; /* ⭐ #666 → white */
|
||||||
}
|
}
|
||||||
.delete-btn:hover {
|
.delete-btn:hover {
|
||||||
background: linear-gradient(135deg, #d32f3f 0%, #e53935 100%);
|
background: linear-gradient(135deg, #d32f3f 0%, #e53935 100%);
|
||||||
@@ -823,6 +823,7 @@ const char index_html[] PROGMEM = R"rawliteral(
|
|||||||
<div class="control-row">
|
<div class="control-row">
|
||||||
<button onclick="startLogging()" style="background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);">Start Logging</button>
|
<button onclick="startLogging()" style="background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);">Start Logging</button>
|
||||||
<button onclick="stopLogging()" style="background: linear-gradient(135deg, #eb3349 0%, #f45c43 100%);">Stop Logging</button>
|
<button onclick="stopLogging()" style="background: linear-gradient(135deg, #eb3349 0%, #f45c43 100%);">Stop Logging</button>
|
||||||
|
<button onclick="hardwareReset()" style="background: linear-gradient(135deg, #fa709a 0%, #fee140 100%); color: white;">🔄 Hardware Reset</button>
|
||||||
<button onclick="refreshFiles()">Refresh Files</button>
|
<button onclick="refreshFiles()">Refresh Files</button>
|
||||||
<button onclick="clearMessages()">Clear Display</button>
|
<button onclick="clearMessages()">Clear Display</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -943,19 +944,11 @@ const char index_html[] PROGMEM = R"rawliteral(
|
|||||||
document.getElementById('sync-status').textContent = '연결됨';
|
document.getElementById('sync-status').textContent = '연결됨';
|
||||||
document.getElementById('sync-status').style.color = '#38ef7d';
|
document.getElementById('sync-status').style.color = '#38ef7d';
|
||||||
|
|
||||||
// ⭐⭐⭐ 자동 시간 동기화 (1분에 1회로 제한)
|
// ⭐⭐⭐ 자동 시간 동기화 (페이지 로드 시 항상 실행)
|
||||||
const now = Date.now();
|
setTimeout(function() {
|
||||||
const lastSync = parseInt(localStorage.getItem('lastTimeSync') || '0');
|
syncTimeFromPhone();
|
||||||
|
console.log('✅ Auto time sync on page load');
|
||||||
if (now - lastSync > 60000) { // 1분 이상 경과
|
}, 500); // WebSocket 안정화 대기
|
||||||
setTimeout(function() {
|
|
||||||
syncTimeFromPhone();
|
|
||||||
localStorage.setItem('lastTimeSync', now.toString());
|
|
||||||
console.log('Auto time sync on connect');
|
|
||||||
}, 100);
|
|
||||||
} else {
|
|
||||||
console.log('Skipping time sync (last sync was recent)');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ⭐ WebSocket 연결되면 즉시 파일 목록 요청
|
// ⭐ WebSocket 연결되면 즉시 파일 목록 요청
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
@@ -1004,6 +997,9 @@ const char index_html[] PROGMEM = R"rawliteral(
|
|||||||
if (data.success) {
|
if (data.success) {
|
||||||
console.log('Comment saved successfully');
|
console.log('Comment saved successfully');
|
||||||
}
|
}
|
||||||
|
} else if (data.type === 'hwReset') {
|
||||||
|
console.log('✅ 하드웨어 리셋 명령 전송됨 - ESP32 재부팅 중...');
|
||||||
|
// ESP32가 재부팅되므로 WebSocket 연결 끊김
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Parse error:', e);
|
console.error('Parse error:', e);
|
||||||
@@ -1393,12 +1389,42 @@ const char index_html[] PROGMEM = R"rawliteral(
|
|||||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
ws.send(JSON.stringify({cmd: 'stopLogging'}));
|
ws.send(JSON.stringify({cmd: 'stopLogging'}));
|
||||||
console.log('Stop logging command sent');
|
console.log('Stop logging command sent');
|
||||||
|
|
||||||
|
// ⭐⭐⭐ 로깅 종료 시 Messages 카운트 리셋
|
||||||
|
document.getElementById('msg-count').textContent = '0';
|
||||||
|
document.getElementById('msg-speed').textContent = '0/s';
|
||||||
|
console.log('✅ Messages count reset to 0');
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
refreshFiles();
|
refreshFiles();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hardwareReset() {
|
||||||
|
if (!confirm('ESP32를 재부팅하시겠습니까?\n\n⚠️ 모든 로깅이 중지되고 WebSocket 연결이 끊어집니다.\n재부팅 후 웹페이지를 새로고침하세요.')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
|
console.log('🔄 하드웨어 리셋 요청...');
|
||||||
|
ws.send(JSON.stringify({cmd: 'hwReset'}));
|
||||||
|
|
||||||
|
// 메시지 카운터 리셋
|
||||||
|
document.getElementById('msg-count').textContent = '0';
|
||||||
|
document.getElementById('msg-speed').textContent = '0/s';
|
||||||
|
console.log('✅ 하드웨어 리셋 요청 전송됨');
|
||||||
|
|
||||||
|
// 3초 후 알림
|
||||||
|
setTimeout(() => {
|
||||||
|
alert('ESP32가 재부팅 중입니다...\n\n10초 후 웹페이지를 새로고침하세요!');
|
||||||
|
}, 1000);
|
||||||
|
} else {
|
||||||
|
alert('WebSocket이 연결되지 않았습니다!');
|
||||||
|
console.error('WebSocket not connected');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function refreshFiles() {
|
function refreshFiles() {
|
||||||
console.log('Requesting file list...'); // ⭐ 디버그
|
console.log('Requesting file list...'); // ⭐ 디버그
|
||||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
|
|||||||
Reference in New Issue
Block a user