결과 확인해보니 웹페이지의 Real-time CAN messages(by ID) 의 count 작동이 데이터 보내지 않음에도 카운트 되는데 이부분 잘못 된것 같아 can신호가 수집된 갯수를 count 하게 수정해줘, 그리고 logging status, sd card, messages, speed 추가로 현재 로깅되고 있는 파일 명도 추가 해줘
462 lines
17 KiB
C
462 lines
17 KiB
C
#ifndef INDEX_H
|
||
#define INDEX_H
|
||
|
||
const char index_html[] PROGMEM = R"rawliteral(
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>ESP32 CAN Logger</title>
|
||
<style>
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
body {
|
||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
min-height: 100vh;
|
||
padding: 20px;
|
||
}
|
||
.container {
|
||
max-width: 1400px;
|
||
margin: 0 auto;
|
||
background: white;
|
||
border-radius: 15px;
|
||
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||
overflow: hidden;
|
||
}
|
||
.header {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
padding: 30px;
|
||
text-align: center;
|
||
}
|
||
.header h1 { font-size: 2.5em; margin-bottom: 10px; }
|
||
.header p { opacity: 0.9; font-size: 1.1em; }
|
||
.content { padding: 30px; }
|
||
.status-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||
gap: 20px;
|
||
margin-bottom: 30px;
|
||
}
|
||
.status-card {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
padding: 20px;
|
||
border-radius: 10px;
|
||
text-align: center;
|
||
}
|
||
.status-card h3 { font-size: 0.9em; opacity: 0.9; margin-bottom: 10px; }
|
||
.status-card .value { font-size: 2em; font-weight: bold; }
|
||
.status-on { background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%) !important; }
|
||
.status-off { background: linear-gradient(135deg, #eb3349 0%, #f45c43 100%) !important; }
|
||
.control-panel {
|
||
background: #f8f9fa;
|
||
padding: 20px;
|
||
border-radius: 10px;
|
||
margin-bottom: 30px;
|
||
}
|
||
.control-row {
|
||
display: flex;
|
||
gap: 15px;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
margin-bottom: 15px;
|
||
}
|
||
.control-row:last-child { margin-bottom: 0; }
|
||
label { font-weight: 600; color: #333; }
|
||
select, button {
|
||
padding: 10px 20px;
|
||
border: none;
|
||
border-radius: 5px;
|
||
font-size: 1em;
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
}
|
||
select {
|
||
background: white;
|
||
border: 2px solid #667eea;
|
||
color: #333;
|
||
}
|
||
button {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
font-weight: 600;
|
||
}
|
||
button:hover { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(0,0,0,0.3); }
|
||
button:active { transform: translateY(0); }
|
||
.can-table-container {
|
||
background: #f8f9fa;
|
||
border-radius: 10px;
|
||
padding: 20px;
|
||
overflow-x: auto;
|
||
}
|
||
table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
background: white;
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
}
|
||
th {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
padding: 15px;
|
||
text-align: left;
|
||
font-weight: 600;
|
||
}
|
||
td {
|
||
padding: 12px 15px;
|
||
border-bottom: 1px solid #e9ecef;
|
||
font-family: 'Courier New', monospace;
|
||
}
|
||
tr:hover { background: #f8f9fa; }
|
||
.flash-row {
|
||
animation: flashAnimation 0.3s ease-in-out;
|
||
}
|
||
@keyframes flashAnimation {
|
||
0%, 100% { background-color: transparent; }
|
||
50% { background-color: #fff3cd; }
|
||
}
|
||
.mono { font-family: 'Courier New', monospace; }
|
||
h2 {
|
||
color: #333;
|
||
margin: 30px 0 20px 0;
|
||
padding-bottom: 10px;
|
||
border-bottom: 3px solid #667eea;
|
||
}
|
||
.file-list {
|
||
background: #f8f9fa;
|
||
border-radius: 10px;
|
||
padding: 20px;
|
||
}
|
||
.file-item {
|
||
background: white;
|
||
padding: 15px;
|
||
margin-bottom: 10px;
|
||
border-radius: 8px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
transition: all 0.3s;
|
||
}
|
||
.file-item:hover { transform: translateX(5px); box-shadow: 0 3px 10px rgba(0,0,0,0.1); }
|
||
.file-name { font-weight: 600; color: #333; }
|
||
.file-size { color: #666; margin-left: 15px; }
|
||
.download-btn {
|
||
padding: 8px 16px;
|
||
font-size: 0.9em;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div class="header">
|
||
<h1>🚗 ESP32 CAN Logger</h1>
|
||
<p>Real-time CAN Bus Monitor & Data Logger</p>
|
||
</div>
|
||
<div class="content">
|
||
<div class="status-grid">
|
||
<div class="status-card" id="logging-status">
|
||
<h3>LOGGING STATUS</h3>
|
||
<div class="value">OFF</div>
|
||
</div>
|
||
<div class="status-card" id="sd-status">
|
||
<h3>SD CARD</h3>
|
||
<div class="value">NOT READY</div>
|
||
</div>
|
||
<div class="status-card">
|
||
<h3>MESSAGES</h3>
|
||
<div class="value" id="msg-count">0</div>
|
||
</div>
|
||
<div class="status-card">
|
||
<h3>SPEED</h3>
|
||
<div class="value" id="msg-speed">0 msg/s</div>
|
||
</div>
|
||
<div class="status-card" id="file-status" style="grid-column: span 2;">
|
||
<h3>CURRENT FILE</h3>
|
||
<div class="value" id="current-file" style="font-size: 1.3em;">-</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="control-panel">
|
||
<h2>⚙️ Control Panel</h2>
|
||
<div class="control-row">
|
||
<label for="can-speed">CAN Speed:</label>
|
||
<select id="can-speed">
|
||
<option value="0">125 Kbps</option>
|
||
<option value="1">250 Kbps</option>
|
||
<option value="2">500 Kbps</option>
|
||
<option value="3" selected>1 Mbps</option>
|
||
</select>
|
||
<button onclick="setCanSpeed()">Apply Speed</button>
|
||
</div>
|
||
<div class="control-row">
|
||
<button onclick="refreshFiles()">🔄 Refresh Files</button>
|
||
<button onclick="clearMessages()">🗑️ Clear Display</button>
|
||
</div>
|
||
</div>
|
||
|
||
<h2>📊 Real-time CAN Messages (by ID)</h2>
|
||
<div class="can-table-container">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>CAN ID</th>
|
||
<th>DLC</th>
|
||
<th>Data</th>
|
||
<th>Count</th>
|
||
<th>Last Time (ms)</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="can-messages"></tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<h2>💾 Log Files</h2>
|
||
<div class="file-list" id="file-list">
|
||
<p style="text-align: center; color: #666;">Loading files...</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
let ws;
|
||
let reconnectInterval;
|
||
let canMessages = {}; // CAN ID별 메시지 저장 객체
|
||
let messageOrder = []; // 메시지 표시 순서
|
||
|
||
function initWebSocket() {
|
||
ws = new WebSocket('ws://' + window.location.hostname + ':81');
|
||
|
||
ws.onopen = function() {
|
||
console.log('WebSocket connected');
|
||
clearInterval(reconnectInterval);
|
||
|
||
// 연결 직후 파일 목록 요청
|
||
setTimeout(() => {
|
||
refreshFiles();
|
||
}, 500);
|
||
};
|
||
|
||
ws.onclose = function() {
|
||
console.log('WebSocket disconnected');
|
||
reconnectInterval = setInterval(initWebSocket, 3000);
|
||
};
|
||
|
||
ws.onmessage = function(event) {
|
||
const data = JSON.parse(event.data);
|
||
console.log('수신:', data.type);
|
||
|
||
if (data.type === 'status') {
|
||
updateStatus(data);
|
||
} else if (data.type === 'can') {
|
||
addCanMessage(data);
|
||
} else if (data.type === 'canBatch') {
|
||
// 일괄 메시지 처리
|
||
updateCanBatch(data.messages);
|
||
} else if (data.type === 'files') {
|
||
console.log('파일 목록 수신:', data.files ? data.files.length : 0);
|
||
if (data.error) {
|
||
document.getElementById('file-list').innerHTML =
|
||
`<p style="text-align: center; color: #e74c3c;">Error: ${data.error}</p>`;
|
||
} else {
|
||
updateFileList(data.files);
|
||
}
|
||
}
|
||
};
|
||
}
|
||
|
||
function updateStatus(data) {
|
||
const loggingCard = document.getElementById('logging-status');
|
||
const sdCard = document.getElementById('sd-status');
|
||
const fileCard = document.getElementById('file-status');
|
||
|
||
if (data.logging) {
|
||
loggingCard.classList.add('status-on');
|
||
loggingCard.classList.remove('status-off');
|
||
loggingCard.querySelector('.value').textContent = 'ON';
|
||
} else {
|
||
loggingCard.classList.add('status-off');
|
||
loggingCard.classList.remove('status-on');
|
||
loggingCard.querySelector('.value').textContent = 'OFF';
|
||
}
|
||
|
||
if (data.sdReady) {
|
||
sdCard.classList.add('status-on');
|
||
sdCard.classList.remove('status-off');
|
||
sdCard.querySelector('.value').textContent = 'READY';
|
||
} else {
|
||
sdCard.classList.add('status-off');
|
||
sdCard.classList.remove('status-on');
|
||
sdCard.querySelector('.value').textContent = 'NOT READY';
|
||
}
|
||
|
||
// 현재 파일명 표시
|
||
if (data.currentFile && data.currentFile !== '') {
|
||
fileCard.classList.add('status-on');
|
||
fileCard.classList.remove('status-off');
|
||
document.getElementById('current-file').textContent = data.currentFile;
|
||
} else {
|
||
fileCard.classList.remove('status-on', 'status-off');
|
||
document.getElementById('current-file').textContent = '-';
|
||
}
|
||
|
||
document.getElementById('msg-count').textContent = data.msgCount.toLocaleString();
|
||
document.getElementById('msg-speed').textContent = data.msgSpeed + ' msg/s';
|
||
}
|
||
|
||
function addCanMessage(data) {
|
||
const canId = data.id;
|
||
|
||
// 새로운 CAN ID인 경우 순서에 추가
|
||
if (!canMessages[canId]) {
|
||
messageOrder.push(canId);
|
||
}
|
||
|
||
// CAN ID별로 최신 메시지 저장
|
||
canMessages[canId] = {
|
||
timestamp: data.timestamp,
|
||
dlc: data.dlc,
|
||
data: data.data,
|
||
updateCount: (canMessages[canId]?.updateCount || 0) + 1
|
||
};
|
||
}
|
||
|
||
function updateCanBatch(messages) {
|
||
// 여러 메시지를 한 번에 처리
|
||
messages.forEach(msg => {
|
||
const canId = msg.id;
|
||
|
||
if (!canMessages[canId]) {
|
||
messageOrder.push(canId);
|
||
}
|
||
|
||
// ESP32에서 받은 실제 count 값 사용
|
||
canMessages[canId] = {
|
||
timestamp: msg.timestamp,
|
||
dlc: msg.dlc,
|
||
data: msg.data,
|
||
updateCount: msg.count // 실제 수신 횟수
|
||
};
|
||
});
|
||
|
||
// 한 번만 테이블 업데이트
|
||
updateCanTable();
|
||
}
|
||
|
||
function updateCanTable() {
|
||
const tbody = document.getElementById('can-messages');
|
||
|
||
// 기존 행들의 ID를 Map으로 저장
|
||
const existingRows = new Map();
|
||
Array.from(tbody.rows).forEach(row => {
|
||
existingRows.set(row.dataset.canId, row);
|
||
});
|
||
|
||
// CAN ID 순서대로 업데이트
|
||
messageOrder.forEach(canId => {
|
||
const msg = canMessages[canId];
|
||
let row = existingRows.get(canId);
|
||
|
||
if (row) {
|
||
// 기존 행 업데이트
|
||
row.cells[1].textContent = msg.dlc;
|
||
row.cells[2].textContent = msg.data;
|
||
row.cells[3].textContent = msg.updateCount;
|
||
row.cells[4].textContent = msg.timestamp;
|
||
|
||
// 업데이트 강조
|
||
row.classList.add('flash-row');
|
||
setTimeout(() => row.classList.remove('flash-row'), 300);
|
||
} else {
|
||
// 새 행 추가
|
||
row = tbody.insertRow();
|
||
row.dataset.canId = canId;
|
||
|
||
row.innerHTML = `
|
||
<td class="mono">0x${canId}</td>
|
||
<td>${msg.dlc}</td>
|
||
<td class="mono">${msg.data}</td>
|
||
<td>${msg.updateCount}</td>
|
||
<td>${msg.timestamp}</td>
|
||
`;
|
||
|
||
row.classList.add('flash-row');
|
||
setTimeout(() => row.classList.remove('flash-row'), 300);
|
||
}
|
||
});
|
||
}
|
||
|
||
function updateFileList(files) {
|
||
const fileList = document.getElementById('file-list');
|
||
|
||
if (!files || files.length === 0) {
|
||
fileList.innerHTML = '<p style="text-align: center; color: #666;">No log files found</p>';
|
||
return;
|
||
}
|
||
|
||
fileList.innerHTML = '';
|
||
files.forEach(file => {
|
||
const fileItem = document.createElement('div');
|
||
fileItem.className = 'file-item';
|
||
fileItem.innerHTML = `
|
||
<div>
|
||
<span class="file-name">📄 ${file.name}</span>
|
||
<span class="file-size">(${formatBytes(file.size)})</span>
|
||
</div>
|
||
<button class="download-btn" onclick="downloadFile('${file.name}')">⬇ Download</button>
|
||
`;
|
||
fileList.appendChild(fileItem);
|
||
});
|
||
|
||
console.log(`파일 목록 업데이트: ${files.length}개`);
|
||
}
|
||
|
||
function formatBytes(bytes) {
|
||
if (bytes < 1024) return bytes + ' B';
|
||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB';
|
||
return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
|
||
}
|
||
|
||
function setCanSpeed() {
|
||
const speed = document.getElementById('can-speed').value;
|
||
ws.send(JSON.stringify({cmd: 'setSpeed', speed: parseInt(speed)}));
|
||
}
|
||
|
||
function refreshFiles() {
|
||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||
ws.send(JSON.stringify({cmd: 'getFiles'}));
|
||
console.log('파일 목록 요청 전송');
|
||
} else {
|
||
console.log('WebSocket 연결 안 됨');
|
||
document.getElementById('file-list').innerHTML =
|
||
'<p style="text-align: center; color: #e74c3c;">WebSocket not connected. Reconnecting...</p>';
|
||
}
|
||
}
|
||
|
||
function clearMessages() {
|
||
canMessages = {};
|
||
messageOrder = [];
|
||
document.getElementById('can-messages').innerHTML = '';
|
||
}
|
||
|
||
function downloadFile(filename) {
|
||
window.location.href = '/download?file=' + encodeURIComponent(filename);
|
||
}
|
||
|
||
initWebSocket();
|
||
|
||
// 페이지 로드 시 파일 목록 요청 (2초 후 재시도)
|
||
setTimeout(() => {
|
||
if (!document.getElementById('file-list').querySelector('.file-item')) {
|
||
console.log('파일 목록 재요청');
|
||
refreshFiles();
|
||
}
|
||
}, 2000);
|
||
</script>
|
||
</body>
|
||
</html>
|
||
)rawliteral";
|
||
|
||
#endif |