RTC추가

주요 변경사항
하드웨어 연결

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를 통해 정확한 시간 관리가 가능합니다! 🕰️
This commit is contained in:
2025-10-12 16:55:46 +00:00
parent 7dcae09fd9
commit f1c989f635
2 changed files with 294 additions and 66 deletions

View File

@@ -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 <Arduino.h>
@@ -16,6 +16,8 @@
#include <freertos/semphr.h>
#include <sys/time.h>
#include <time.h>
#include <Wire.h>
#include <RTClib.h>
#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();
}
}

View File

@@ -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%;
}
}
</style>
</head>
@@ -260,7 +302,7 @@ const char index_html[] PROGMEM = R"rawliteral(
<div class="container">
<div class="header">
<h1>Byun CAN Logger</h1>
<p>Real-time CAN Bus Monitor & Data Logger with Time Sync</p>
<p>Real-time CAN Bus Monitor & Data Logger with RTC</p>
</div>
<div class="nav">
@@ -281,7 +323,18 @@ const char index_html[] PROGMEM = R"rawliteral(
<span class="time-value" id="current-time">--:--:--</span>
</div>
</div>
<button class="btn-time-sync" onclick="syncTime()"> </button>
<button class="btn-time-sync" onclick="syncTime()"> </button>
<div class="rtc-panel">
<h3>🕰 RTC DS3231 </h3>
<div class="rtc-controls">
<div class="rtc-time" id="rtc-time">--:--:--</div>
<span class="rtc-status unavailable" id="rtc-status"> ...</span>
<button class="btn-rtc" onclick="getRTCTime()">📖 RTC </button>
<button class="btn-rtc" onclick="setRTCTime()">📝 RTC </button>
<button class="btn-rtc" onclick="syncFromRTC()">🔄 RTC</button>
</div>
</div>
</div>
<div class="status-grid">
@@ -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);
</script>