정상 로깅 확인 soft reset 추가

This commit is contained in:
2025-12-13 17:18:55 +00:00
parent 2d7368679d
commit 455e066af7
2 changed files with 421 additions and 29 deletions

View File

@@ -287,7 +287,64 @@ int commentCount = 0;
// Forward declarations
void IRAM_ATTR canISR();
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 초기화 함수
// ========================================
@@ -914,12 +971,15 @@ void sdWriteTask(void *parameter) {
// CAN 로깅
if (loggingEnabled && sdCardReady) {
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(50)) == pdTRUE) {
// ⭐⭐⭐ 뮤텍스 타임아웃 1ms로 감소 (블로킹 방지)
if (xSemaphoreTake(sdMutex, pdMS_TO_TICKS(1)) == pdTRUE) {
if (canLogFormatCSV) {
char csvLine[128];
uint64_t relativeTime = canMsg.timestamp_us - canLogStartTime;
char dataStr[32];
int dataLen = 0;
static uint32_t csvReopenCounter = 0;
for (int i = 0; i < canMsg.dlc; i++) {
dataLen += sprintf(&dataStr[dataLen], "%02X", canMsg.data[i]);
if (i < canMsg.dlc - 1) dataStr[dataLen++] = ' ';
@@ -931,23 +991,40 @@ void sdWriteTask(void *parameter) {
relativeTime, canMsg.id, canMsg.dlc, dataStr);
if (logFile) {
logFile.write((uint8_t*)csvLine, lineLen);
size_t written = logFile.write((uint8_t*)csvLine, lineLen);
currentFileSize += lineLen;
static int csvFlushCounter = 0;
if (++csvFlushCounter >= 20) { // 50 → 20으로 더 자주 플러시
if (++csvFlushCounter >= 50) { // ⭐ 20 → 50 (너무 자주 플러시하면 느림)
logFile.flush();
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 {
// BIN 형식
static uint32_t binMsgCounter = 0;
static uint32_t binReopenCounter = 0;
// ⭐⭐⭐ 1단계: 버퍼 가득 찼으면 먼저 플러시
if (bufferIndex + sizeof(CANMessage) > FILE_BUFFER_SIZE) {
if (logFile) {
size_t written = logFile.write(fileBuffer, bufferIndex);
logFile.flush();
Serial.printf("✓ BIN 버퍼 플러시: %d bytes written\n", written);
Serial.printf("✓ BIN 버퍼 플러시 (FULL): %d bytes\n", written);
bufferIndex = 0;
}
}
@@ -956,6 +1033,39 @@ void sdWriteTask(void *parameter) {
memcpy(&fileBuffer[bufferIndex], &canMsg, sizeof(CANMessage));
bufferIndex += 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);
}
@@ -1308,9 +1418,14 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length)
loggingEnabled = true;
bufferIndex = 0;
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 {
Serial.println("✗ APPEND 모드로 파일 열기 실패");
Serial.printf(" 파일명: %s\n", currentFilename);
}
} else {
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) {
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) {
logFile.write(fileBuffer, bufferIndex);
size_t written = logFile.write(fileBuffer, bufferIndex);
logFile.flush();
Serial.printf("✓ 최종 플러시: %d bytes\n", written);
bufferIndex = 0;
}
// ⭐⭐⭐ CSV 형식: 최종 플러시
if (canLogFormatCSV && logFile) {
logFile.flush();
Serial.println("✓ CSV 최종 플러시");
}
if (logFile) {
size_t finalSize = logFile.size();
logFile.close();
Serial.printf("✓ 파일 닫힘: %s (%lu bytes)\n", currentFilename, finalSize);
}
loggingEnabled = false;
currentFilename[0] = '\0';
bufferIndex = 0;
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();
}
}
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) {
DynamicJsonDocument response(3072);
response["type"] = "sequences";
@@ -1778,6 +2001,149 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length)
serializeJson(response, 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 생성
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(serialRxTask, "SERIAL_RX", 6144, NULL, 5, &serialRxTaskHandle, 0);
xTaskCreatePinnedToCore(serial2RxTask, "SERIAL2_RX", 6144, NULL, 5, &serial2RxTaskHandle, 0); // ⭐ Serial2
xTaskCreatePinnedToCore(txTask, "TX", 4096, NULL, 3, NULL, 1);
xTaskCreatePinnedToCore(webUpdateTask, "WEB_UPDATE", 16384, NULL, 5, &webTaskHandle, 0); // ⭐ 10240 → 16384
xTaskCreatePinnedToCore(sdMonitorTask, "SD_MONITOR", 4096, NULL, 1, NULL, 0);
xTaskCreatePinnedToCore(webUpdateTask, "WEB_UPDATE", 10384, NULL, 2, &webTaskHandle, 0); // ⭐ 10240 → 16384
xTaskCreatePinnedToCore(sdMonitorTask, "SD_MONITOR", 4096, NULL, 1, NULL, 1);
if (timeSyncStatus.rtcAvailable) {
xTaskCreatePinnedToCore(rtcSyncTask, "RTC_SYNC", 3072, NULL, 0, &rtcTaskHandle, 0);

60
index.h
View File

@@ -75,7 +75,7 @@ const char index_html[] PROGMEM = R"rawliteral(
/* 전력 경고 배너 */
.power-warning {
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
color: #666;
color: white; /* ⭐ #666 → white */
padding: 12px 20px;
border-radius: 8px;
margin-bottom: 15px;
@@ -339,7 +339,7 @@ const char index_html[] PROGMEM = R"rawliteral(
cursor: pointer;
transition: all 0.3s;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #666;
color: white; /* ⭐ #666 → white */
}
.control-row button:hover {
transform: translateY(-2px);
@@ -360,7 +360,7 @@ const char index_html[] PROGMEM = R"rawliteral(
}
thead {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #666;
color: white; /* ⭐ #666 → white */
}
th {
padding: 12px 8px;
@@ -479,21 +479,21 @@ const char index_html[] PROGMEM = R"rawliteral(
}
.download-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #666;
color: white; /* ⭐ #666 → white */
}
.download-btn:hover {
background: linear-gradient(135deg, #5568d3 0%, #66409e 100%);
}
.comment-btn {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: #666;
color: white; /* ⭐ #666 → white */
}
.comment-btn:hover {
background: linear-gradient(135deg, #e77fe8 0%, #e44459 100%);
}
.delete-btn {
background: linear-gradient(135deg, #eb3349 0%, #f45c43 100%);
color: #666;
color: white; /* ⭐ #666 → white */
}
.delete-btn:hover {
background: linear-gradient(135deg, #d32f3f 0%, #e53935 100%);
@@ -823,6 +823,7 @@ const char index_html[] PROGMEM = R"rawliteral(
<div class="control-row">
<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="hardwareReset()" style="background: linear-gradient(135deg, #fa709a 0%, #fee140 100%); color: white;">🔄 Hardware Reset</button>
<button onclick="refreshFiles()">Refresh Files</button>
<button onclick="clearMessages()">Clear Display</button>
</div>
@@ -943,19 +944,11 @@ const char index_html[] PROGMEM = R"rawliteral(
document.getElementById('sync-status').textContent = '';
document.getElementById('sync-status').style.color = '#38ef7d';
// ⭐⭐⭐ 자동 시간 동기화 (1분에 1회로 제한)
const now = Date.now();
const lastSync = parseInt(localStorage.getItem('lastTimeSync') || '0');
if (now - lastSync > 60000) { // 1분 이상 경과
// ⭐⭐⭐ 자동 시간 동기화 (페이지 로드 시 항상 실행)
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)');
}
console.log(' Auto time sync on page load');
}, 500); // WebSocket 안정화 대기
// ⭐ WebSocket 연결되면 즉시 파일 목록 요청
setTimeout(function() {
@@ -1004,6 +997,9 @@ const char index_html[] PROGMEM = R"rawliteral(
if (data.success) {
console.log('Comment saved successfully');
}
} else if (data.type === 'hwReset') {
console.log(' - ESP32 ...');
// ESP32가 재부팅되므로 WebSocket 연결 끊김
}
} catch (e) {
console.error('Parse error:', e);
@@ -1393,12 +1389,42 @@ const char index_html[] PROGMEM = R"rawliteral(
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({cmd: 'stopLogging'}));
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(() => {
refreshFiles();
}, 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() {
console.log('Requesting file list...'); // ⭐ 디버그
if (ws && ws.readyState === WebSocket.OPEN) {