From f1c989f635adcb2b8302d5cda9f55659244cc8d1 Mon Sep 17 00:00:00 2001 From: byun Date: Sun, 12 Oct 2025 16:55:46 +0000 Subject: [PATCH] =?UTF-8?q?RTC=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 주요 변경사항 하드웨어 연결 RTC DS3231: I²C (SDA: GPIO25, SCL: GPIO26) 추가된 기능 1. RTC 초기화 및 관리 DS3231 RTC 자동 감지 전원 손실 감지 및 경고 RTC에서 시스템 시간 자동 동기화 2. 웹 인터페이스 (모니터 페이지) RTC 읽기 버튼: RTC의 현재 시간 확인 RTC 설정 버튼: 웹 브라우저 시간을 RTC에 설정 RTC→시스템 버튼: RTC 시간을 ESP32 시스템 시간에 동기화 RTC 사용 가능 여부 실시간 표시 RTC 시간 실시간 표시 3. 시간 동기화 옵션 웹 시간 동기화: 브라우저 시간으로 ESP32 동기화 (기존) RTC 시간 설정: 브라우저 시간을 RTC에 저장 RTC→시스템: RTC 시간으로 ESP32 동기화 필요한 라이브러리 Arduino IDE에서 다음 라이브러리를 설치하세요: RTClib by Adafruit 동작 방식 ESP32 부팅 시 RTC 자동 감지 RTC가 있으면 자동으로 시스템 시간 동기화 웹페이지에서 RTC 상태 확인 가능 로깅 파일명에 정확한 시간 반영 이제 인터넷 없이도 RTC를 통해 정확한 시간 관리가 가능합니다! 🕰️ --- test_i2c_reset/ESP32_CAN_Logger.ino | 157 +++++++++++++++++++-- test_i2c_reset/index.h | 203 ++++++++++++++++++++-------- 2 files changed, 294 insertions(+), 66 deletions(-) diff --git a/test_i2c_reset/ESP32_CAN_Logger.ino b/test_i2c_reset/ESP32_CAN_Logger.ino index ee6c226..0676699 100644 --- a/test_i2c_reset/ESP32_CAN_Logger.ino +++ b/test_i2c_reset/ESP32_CAN_Logger.ino @@ -1,6 +1,6 @@ /* - * Byun CAN Logger with Web Interface + Time Synchronization - * Version: 1.2 + * Byun CAN Logger with Web Interface + RTC DS3231 + * Version: 1.3 */ #include @@ -16,6 +16,8 @@ #include #include #include +#include +#include #include "index.h" #include "transmit.h" #include "graph.h" @@ -37,6 +39,10 @@ #define VSPI_SCLK 18 #define VSPI_CS 5 +// I2C 핀 (RTC DS3231) +#define I2C_SDA 25 +#define I2C_SCL 26 + // 버퍼 설정 #define CAN_QUEUE_SIZE 1000 #define FILE_BUFFER_SIZE 8192 @@ -75,7 +81,8 @@ struct TimeSyncStatus { uint64_t lastSyncTime; int32_t offsetUs; uint32_t syncCount; -} timeSyncStatus = {false, 0, 0, 0}; + bool rtcAvailable; +} timeSyncStatus = {false, 0, 0, 0, false}; // WiFi AP 설정 const char* ssid = "Byun_CAN_Logger"; @@ -85,6 +92,7 @@ const char* password = "12345678"; SPIClass hspi(HSPI); SPIClass vspi(VSPI); MCP2515 mcp2515(HSPI_CS, 10000000, &hspi); +RTC_DS3231 rtc; WebServer server(80); WebSocketsServer webSocket = WebSocketsServer(81); @@ -125,7 +133,61 @@ uint64_t getMicrosecondTimestamp() { return (uint64_t)tv.tv_sec * 1000000ULL + (uint64_t)tv.tv_usec; } -// 시간 동기화 설정 +// RTC에서 시간 읽기 +String getRTCTime() { + if (!timeSyncStatus.rtcAvailable) { + return "RTC not available"; + } + + DateTime now = rtc.now(); + char timeStr[32]; + snprintf(timeStr, sizeof(timeStr), "%04d-%02d-%02d %02d:%02d:%02d", + now.year(), now.month(), now.day(), + now.hour(), now.minute(), now.second()); + return String(timeStr); +} + +// RTC에 시간 설정 +bool setRTCTime(uint64_t timestampMs) { + if (!timeSyncStatus.rtcAvailable) { + Serial.println("❌ RTC not available"); + return false; + } + + time_t timestamp = timestampMs / 1000; + DateTime dt(timestamp); + rtc.adjust(dt); + + Serial.printf("⏰ RTC 시간 설정: %s\n", getRTCTime().c_str()); + return true; +} + +// RTC에서 시스템 시간 동기화 +bool syncTimeFromRTC() { + if (!timeSyncStatus.rtcAvailable) { + Serial.println("❌ RTC not available"); + return false; + } + + DateTime now = rtc.now(); + struct timeval tv; + tv.tv_sec = now.unixtime(); + tv.tv_usec = 0; + settimeofday(&tv, NULL); + + timeSyncStatus.synchronized = true; + timeSyncStatus.lastSyncTime = getMicrosecondTimestamp(); + timeSyncStatus.syncCount++; + + char timeStr[64]; + strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", localtime(&tv.tv_sec)); + + Serial.printf("⏰ RTC로부터 시간 동기화: %s (동기화 횟수: %u)\n", + timeStr, timeSyncStatus.syncCount); + return true; +} + +// 시간 동기화 설정 (웹에서) void setSystemTime(uint64_t timestampMs) { struct timeval tv; tv.tv_sec = timestampMs / 1000; @@ -143,7 +205,7 @@ void setSystemTime(uint64_t timestampMs) { char timeStr[64]; strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", &timeinfo); - Serial.printf("⏰ 시간 동기화 완료: %s.%03d (동기화 횟수: %u)\n", + Serial.printf("⏰ 웹에서 시간 동기화: %s.%03d (동기화 횟수: %u)\n", timeStr, (int)(tv.tv_usec / 1000), timeSyncStatus.syncCount); } @@ -162,6 +224,8 @@ void handleCanTransmit(String msg); void handleStartMessage(String msg); void handleStopMessage(String msg); void handleTimeSync(String msg); +void handleRTCGet(uint8_t clientNum); +void handleRTCSet(String msg); void webUpdateTask(void *pvParameters); // CAN 인터럽트 핸들러 @@ -219,7 +283,7 @@ bool createNewLogFile() { // 시간 동기화 경고 if (!timeSyncStatus.synchronized) { - Serial.println("⚠️ 경고: 시간이 동기화되지 않았습니다! 웹페이지에서 시간 동기화를 실행하세요."); + Serial.println("⚠️ 경고: 시간이 동기화되지 않았습니다! 웹페이지에서 시간 동기화를 실행하세요."); } return true; @@ -530,7 +594,7 @@ void handleStopMessage(String msg) { } } -// 시간 동기화 처리 +// 시간 동기화 처리 (웹에서) void handleTimeSync(String msg) { int timestampIdx = msg.indexOf("\"timestamp\":") + 12; String timestampStr = msg.substring(timestampIdx); @@ -543,6 +607,32 @@ void handleTimeSync(String msg) { } } +// RTC 시간 읽기 +void handleRTCGet(uint8_t clientNum) { + String response = "{\"type\":\"rtcTime\",\"time\":\""; + response += getRTCTime(); + response += "\",\"available\":"; + response += timeSyncStatus.rtcAvailable ? "true" : "false"; + response += "}"; + + webSocket.sendTXT(clientNum, response); +} + +// RTC 시간 설정 +void handleRTCSet(String msg) { + int timestampIdx = msg.indexOf("\"timestamp\":") + 12; + String timestampStr = msg.substring(timestampIdx); + timestampStr = timestampStr.substring(0, timestampStr.indexOf("}")); + + uint64_t timestamp = strtoull(timestampStr.c_str(), NULL, 10); + + if (timestamp > 0) { + setRTCTime(timestamp); + // RTC 설정 후 시스템 시간도 동기화 + syncTimeFromRTC(); + } +} + // 웹소켓 이벤트 핸들러 void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length) { switch(type) { @@ -560,8 +650,14 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length // 시간 동기화 상태 전송 String syncStatus = "{\"type\":\"timeSyncStatus\",\"synchronized\":"; syncStatus += timeSyncStatus.synchronized ? "true" : "false"; - syncStatus += ",\"syncCount\":" + String(timeSyncStatus.syncCount) + "}"; + syncStatus += ",\"syncCount\":" + String(timeSyncStatus.syncCount); + syncStatus += ",\"rtcAvailable\":"; + syncStatus += timeSyncStatus.rtcAvailable ? "true" : "false"; + syncStatus += "}"; webSocket.sendTXT(num, syncStatus); + + // RTC 시간 전송 + handleRTCGet(num); } break; @@ -603,6 +699,15 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length else if (msg.indexOf("\"cmd\":\"syncTime\"") >= 0) { handleTimeSync(msg); } + else if (msg.indexOf("\"cmd\":\"getRTC\"") >= 0) { + handleRTCGet(num); + } + else if (msg.indexOf("\"cmd\":\"setRTC\"") >= 0) { + handleRTCSet(msg); + } + else if (msg.indexOf("\"cmd\":\"syncFromRTC\"") >= 0) { + syncTimeFromRTC(); + } } break; } @@ -660,6 +765,7 @@ void webUpdateTask(void *pvParameters) { status += "\"msgSpeed\":" + String(msgPerSecond) + ","; status += "\"timeSync\":" + String(timeSyncStatus.synchronized ? "true" : "false") + ","; status += "\"syncCount\":" + String(timeSyncStatus.syncCount) + ","; + status += "\"rtcAvailable\":" + String(timeSyncStatus.rtcAvailable ? "true" : "false") + ","; if (loggingEnabled && logFile) { status += "\"currentFile\":\"" + String(currentFilename) + "\""; @@ -722,12 +828,33 @@ void setup() { Serial.begin(115200); delay(1000); Serial.println("\n========================================"); - Serial.println(" ESP32 CAN Logger with Time Sync "); + Serial.println(" ESP32 CAN Logger with RTC DS3231 "); Serial.println("========================================"); memset(recentData, 0, sizeof(recentData)); memset(txMessages, 0, sizeof(txMessages)); + // I2C 초기화 (RTC) + Wire.begin(I2C_SDA, I2C_SCL); + + // RTC 초기화 + if (rtc.begin(&Wire)) { + Serial.println("✓ RTC DS3231 초기화 완료"); + timeSyncStatus.rtcAvailable = true; + + // RTC 손실 확인 + if (rtc.lostPower()) { + Serial.println("⚠️ RTC 전원 손실 감지 - 시간 설정 필요"); + } else { + Serial.printf("✓ RTC 시간: %s\n", getRTCTime().c_str()); + // RTC에서 시스템 시간 동기화 + syncTimeFromRTC(); + } + } else { + Serial.println("✗ RTC DS3231 초기화 실패"); + timeSyncStatus.rtcAvailable = false; + } + pinMode(CAN_INT_PIN, INPUT_PULLUP); hspi.begin(HSPI_SCLK, HSPI_MISO, HSPI_MOSI, HSPI_CS); @@ -822,7 +949,12 @@ void setup() { Serial.println(" - Transmit: /transmit"); Serial.println(" - Graph: /graph"); Serial.println("========================================\n"); - Serial.println("⚠️ 시간 동기화를 위해 웹페이지에서 '⏰ 시간 동기화' 버튼을 클릭하세요"); + + if (timeSyncStatus.rtcAvailable) { + Serial.println("✓ RTC 사용 가능 - 웹페이지에서 RTC 시간 관리 가능"); + } else { + Serial.println("⚠️ RTC 사용 불가 - 웹페이지에서 시간 동기화 필요"); + } } void loop() { @@ -831,12 +963,13 @@ void loop() { static uint32_t lastPrint = 0; if (millis() - lastPrint > 10000) { - Serial.printf("[상태] 큐: %d/%d | 로깅: %s | SD: %s | RX: %lu | TX: %lu | 시간동기: %s\n", + Serial.printf("[상태] 큐: %d/%d | 로깅: %s | SD: %s | RX: %lu | TX: %lu | 시간동기: %s | RTC: %s\n", uxQueueMessagesWaiting(canQueue), CAN_QUEUE_SIZE, loggingEnabled ? "ON " : "OFF", sdCardReady ? "OK" : "NO", totalMsgCount, totalTxCount, - timeSyncStatus.synchronized ? "OK" : "NO"); + timeSyncStatus.synchronized ? "OK" : "NO", + timeSyncStatus.rtcAvailable ? "OK" : "NO"); lastPrint = millis(); } } \ No newline at end of file diff --git a/test_i2c_reset/index.h b/test_i2c_reset/index.h index 33c42e6..c0fca79 100644 --- a/test_i2c_reset/index.h +++ b/test_i2c_reset/index.h @@ -59,11 +59,6 @@ const char index_html[] PROGMEM = R"rawliteral( padding: 15px 20px; border-radius: 10px; margin-bottom: 20px; - display: flex; - justify-content: space-between; - align-items: center; - flex-wrap: wrap; - gap: 15px; box-shadow: 0 4px 15px rgba(240, 147, 251, 0.4); } .time-sync-info { @@ -71,6 +66,7 @@ const char index_html[] PROGMEM = R"rawliteral( gap: 20px; align-items: center; flex-wrap: wrap; + margin-bottom: 15px; } .time-info-item { display: flex; @@ -87,25 +83,66 @@ const char index_html[] PROGMEM = R"rawliteral( font-size: 1.1em; font-weight: 700; } - .btn-time-sync { + + .rtc-panel { + background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%); + padding: 15px; + border-radius: 10px; + margin-top: 15px; + border-left: 4px solid #667eea; + } + .rtc-panel h3 { + color: #333; + margin-bottom: 10px; + font-size: 1em; + } + .rtc-controls { + display: flex; + gap: 10px; + align-items: center; + flex-wrap: wrap; + } + .rtc-time { + flex: 1; + min-width: 200px; + padding: 10px 15px; + background: white; + border-radius: 8px; + font-family: 'Courier New', monospace; + font-size: 1.1em; + font-weight: 600; + color: #667eea; + text-align: center; + } + .rtc-status { + padding: 6px 12px; + background: white; + border-radius: 5px; + font-size: 0.85em; + font-weight: 600; + } + .rtc-status.available { color: #11998e; } + .rtc-status.unavailable { color: #e74c3c; } + + .btn-time-sync, .btn-rtc { background: white; color: #f5576c; - padding: 12px 24px; + padding: 10px 20px; border: none; border-radius: 8px; - font-size: 1em; + font-size: 0.9em; font-weight: 700; cursor: pointer; transition: all 0.3s; box-shadow: 0 3px 10px rgba(0,0,0,0.2); } - .btn-time-sync:hover { + .btn-rtc { + color: #667eea; + } + .btn-time-sync:hover, .btn-rtc:hover { transform: translateY(-3px); box-shadow: 0 6px 20px rgba(0,0,0,0.3); } - .btn-time-sync:active { - transform: translateY(-1px); - } .status-grid { display: grid; @@ -243,8 +280,6 @@ const char index_html[] PROGMEM = R"rawliteral( table { min-width: 400px; } th, td { padding: 6px 4px; font-size: 0.75em; } .time-sync-banner { - flex-direction: column; - align-items: stretch; padding: 12px 15px; } .time-sync-info { @@ -253,6 +288,13 @@ const char index_html[] PROGMEM = R"rawliteral( .time-value { font-size: 1em; } + .rtc-controls { + flex-direction: column; + align-items: stretch; + } + .rtc-time { + min-width: 100%; + } } @@ -260,7 +302,7 @@ const char index_html[] PROGMEM = R"rawliteral(

Byun CAN Logger

-

Real-time CAN Bus Monitor & Data Logger with Time Sync

+

Real-time CAN Bus Monitor & Data Logger with RTC

- + + +
+

🕰️ RTC DS3231 모듈

+
+
--:--:--
+ 확인 중... + + + +
+
@@ -361,6 +414,7 @@ const char index_html[] PROGMEM = R"rawliteral( let canMessages = {}; let messageOrder = []; let lastMessageData = {}; + let rtcAvailable = false; const speedNames = ['125 Kbps', '250 Kbps', '500 Kbps', '1 Mbps']; @@ -376,7 +430,7 @@ const char index_html[] PROGMEM = R"rawliteral( setInterval(updateCurrentTime, 1000); updateCurrentTime(); - // 시간 동기화 함수 + // 웹 시간 동기화 function syncTime() { if (ws && ws.readyState === WebSocket.OPEN) { const timestamp = Date.now(); @@ -398,39 +452,63 @@ const char index_html[] PROGMEM = R"rawliteral( document.getElementById('sync-status').textContent = '✓ ' + dateStr; }, 200); - console.log('시간 동기화 전송:', new Date(timestamp).toLocaleString()); + console.log('웹 시간 동기화 전송:', new Date(timestamp).toLocaleString()); } else { alert('WebSocket이 연결되지 않았습니다!'); } } - function saveCanSpeed() { - const speed = document.getElementById('can-speed').value; - try { - window.localStorage.setItem('canSpeed', speed); - console.log('Saved CAN speed:', speedNames[speed]); - } catch(e) { - console.error('Failed to save CAN speed:', e); + // RTC 시간 읽기 + function getRTCTime() { + if (ws && ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ cmd: 'getRTC' })); + console.log('RTC 시간 요청'); + } else { + alert('WebSocket이 연결되지 않았습니다!'); } } - function loadCanSpeed() { - try { - const savedSpeed = window.localStorage.getItem('canSpeed'); - if (savedSpeed !== null) { - document.getElementById('can-speed').value = savedSpeed; - console.log('Restored CAN speed:', speedNames[savedSpeed]); - - const statusSpan = document.getElementById('speed-status'); - if (statusSpan) { - statusSpan.textContent = '(Restored: ' + speedNames[savedSpeed] + ')'; - setTimeout(() => { - statusSpan.textContent = ''; - }, 3000); - } - } - } catch(e) { - console.error('Failed to load CAN speed:', e); + // RTC에 현재 웹 시간 설정 + function setRTCTime() { + if (!rtcAvailable) { + alert('RTC를 사용할 수 없습니다!'); + return; + } + + if (!confirm('현재 웹 브라우저 시간을 RTC에 설정하시겠습니까?')) { + return; + } + + if (ws && ws.readyState === WebSocket.OPEN) { + const timestamp = Date.now(); + ws.send(JSON.stringify({ + cmd: 'setRTC', + timestamp: timestamp + })); + + console.log('RTC 시간 설정:', new Date(timestamp).toLocaleString()); + + // 설정 후 읽기 + setTimeout(() => { getRTCTime(); }, 500); + } else { + alert('WebSocket이 연결되지 않았습니다!'); + } + } + + // RTC에서 시스템 시간 동기화 + function syncFromRTC() { + if (!rtcAvailable) { + alert('RTC를 사용할 수 없습니다!'); + return; + } + + if (ws && ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ cmd: 'syncFromRTC' })); + console.log('RTC→시스템 동기화 요청'); + + setTimeout(() => { getRTCTime(); }, 500); + } else { + alert('WebSocket이 연결되지 않았습니다!'); } } @@ -441,12 +519,7 @@ const char index_html[] PROGMEM = R"rawliteral( console.log('WebSocket connected'); clearInterval(reconnectInterval); setTimeout(() => { refreshFiles(); }, 500); - - // 연결 직후 자동 시간 동기화 - setTimeout(() => { - syncTime(); - console.log('자동 시간 동기화 실행'); - }, 1000); + setTimeout(() => { getRTCTime(); }, 1000); }; ws.onclose = function() { @@ -478,10 +551,33 @@ const char index_html[] PROGMEM = R"rawliteral( card.classList.add('status-on'); card.classList.remove('status-off'); } + + if (data.rtcAvailable !== undefined) { + rtcAvailable = data.rtcAvailable; + updateRTCStatus(rtcAvailable); + } + } else if (data.type === 'rtcTime') { + document.getElementById('rtc-time').textContent = data.time; + + if (data.available !== undefined) { + rtcAvailable = data.available; + updateRTCStatus(rtcAvailable); + } } }; } + function updateRTCStatus(available) { + const statusEl = document.getElementById('rtc-status'); + if (available) { + statusEl.textContent = '✓ 사용 가능'; + statusEl.className = 'rtc-status available'; + } else { + statusEl.textContent = '✗ 사용 불가'; + statusEl.className = 'rtc-status unavailable'; + } + } + function updateStatus(data) { const loggingCard = document.getElementById('logging-status'); const sdCard = document.getElementById('sd-status'); @@ -520,6 +616,11 @@ const char index_html[] PROGMEM = R"rawliteral( document.getElementById('sync-count').textContent = data.syncCount; } + if (data.rtcAvailable !== undefined) { + rtcAvailable = data.rtcAvailable; + updateRTCStatus(rtcAvailable); + } + if (data.currentFile && data.currentFile !== '') { fileCard.classList.add('status-on'); fileCard.classList.remove('status-off'); @@ -649,8 +750,6 @@ const char index_html[] PROGMEM = R"rawliteral( ws.send(JSON.stringify({cmd: 'setSpeed', speed: parseInt(speed)})); - saveCanSpeed(); - const statusSpan = document.getElementById('speed-status'); if (statusSpan) { statusSpan.textContent = '✓ Applied: ' + speedName; @@ -695,10 +794,6 @@ const char index_html[] PROGMEM = R"rawliteral( window.location.href = '/download?file=' + encodeURIComponent(filename); } - window.addEventListener('load', function() { - loadCanSpeed(); - }); - initWebSocket(); setTimeout(() => { refreshFiles(); }, 2000);