Files
250928_esp32_spi_sdcard_ads…/index.h
byun 2dd7f9177f 00004 monitor 창 graph연결 버튼 추가, 핸드폰 자동화면 변경
여전히 메인 웹페이지에서 monitor, Transmit 밖에 선택지가 없는데 다시한번 누락되어 있는 부분 확인해줘, 그리고 핸드폰 에서 웹페이지 접속시 가로창 넘게 표시되어 이미지가 짤리는데 핸드폰 화면에 맞추어 글자와 이미지들이 들어갈 수 있게 자동 조절하게 해줄 수 있어? 특히 전송 웹페이지에 data byte(hex) 입력 칸들이 화면에 다 안들어가서 짤린상태로 전체 값들을 넣을 수 가 없어
2025-10-05 16:36:18 +00:00

473 lines
18 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, maximum-scale=1.0, user-scalable=no">
<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: 10px;
}
.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: 20px;
text-align: center;
}
.header h1 { font-size: 1.8em; margin-bottom: 5px; }
.header p { opacity: 0.9; font-size: 0.9em; }
.nav {
background: #2c3e50;
padding: 10px;
display: flex;
gap: 10px;
flex-wrap: wrap;
justify-content: center;
}
.nav a {
color: white;
text-decoration: none;
padding: 10px 15px;
border-radius: 5px;
transition: all 0.3s;
font-size: 0.9em;
white-space: nowrap;
}
.nav a:hover { background: #34495e; }
.nav a.active { background: #3498db; }
.content { padding: 15px; }
.status-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 10px;
margin-bottom: 20px;
}
.status-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 15px;
border-radius: 10px;
text-align: center;
}
.status-card h3 { font-size: 0.75em; opacity: 0.9; margin-bottom: 8px; }
.status-card .value { font-size: 1.5em; font-weight: bold; word-break: break-all; }
.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: 15px;
border-radius: 10px;
margin-bottom: 20px;
}
.control-row {
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
margin-bottom: 10px;
}
.control-row:last-child { margin-bottom: 0; }
label { font-weight: 600; color: #333; font-size: 0.9em; }
select, button {
padding: 8px 15px;
border: none;
border-radius: 5px;
font-size: 0.9em;
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: 10px;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
table {
width: 100%;
min-width: 500px;
border-collapse: collapse;
background: white;
border-radius: 8px;
overflow: hidden;
}
th {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 10px 8px;
text-align: left;
font-weight: 600;
font-size: 0.85em;
}
td {
padding: 8px;
border-bottom: 1px solid #e9ecef;
font-family: 'Courier New', monospace;
font-size: 0.8em;
}
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: 20px 0 15px 0;
padding-bottom: 8px;
border-bottom: 3px solid #667eea;
font-size: 1.3em;
}
.file-list {
background: #f8f9fa;
border-radius: 10px;
padding: 15px;
}
.file-item {
background: white;
padding: 12px;
margin-bottom: 8px;
border-radius: 8px;
display: flex;
justify-content: space-between;
align-items: center;
transition: all 0.3s;
flex-wrap: wrap;
gap: 10px;
}
.file-item:hover { transform: translateX(5px); box-shadow: 0 3px 10px rgba(0,0,0,0.1); }
.file-name { font-weight: 600; color: #333; font-size: 0.9em; }
.file-size { color: #666; margin-left: 10px; font-size: 0.85em; }
.download-btn {
padding: 6px 12px;
font-size: 0.85em;
}
@media (max-width: 768px) {
body { padding: 5px; }
.header h1 { font-size: 1.5em; }
.header p { font-size: 0.85em; }
.content { padding: 10px; }
.status-grid { grid-template-columns: repeat(2, 1fr); gap: 8px; }
.status-card { padding: 10px; }
.status-card h3 { font-size: 0.7em; }
.status-card .value { font-size: 1.2em; }
h2 { font-size: 1.1em; }
.nav a { padding: 8px 12px; font-size: 0.85em; }
table { min-width: 400px; }
th, td { padding: 6px 4px; font-size: 0.75em; }
}
</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="nav">
<a href="/" class="active">Monitor</a>
<a href="/transmit">Transmit</a>
<a href="/graph">Graph</a>
</div>
<div class="content">
<div class="status-grid">
<div class="status-card" id="logging-status">
<h3>LOGGING</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/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: 1em;">-</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</button>
</div>
<div class="control-row">
<button onclick="refreshFiles()">Refresh Files</button>
<button onclick="clearMessages()">Clear Display</button>
</div>
</div>
<h2>CAN Messages (by ID)</h2>
<div class="can-table-container">
<table>
<thead>
<tr>
<th>ID</th>
<th>DLC</th>
<th>Data</th>
<th>Count</th>
<th>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; font-size: 0.9em;">Loading...</p>
</div>
</div>
</div>
<script>
let ws;
let reconnectInterval;
let canMessages = {};
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);
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') {
if (data.error) {
document.getElementById('file-list').innerHTML =
'<p style="text-align: center; color: #e74c3c; font-size: 0.9em;">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 + '/s';
}
function addCanMessage(data) {
const canId = data.id;
if (!canMessages[canId]) {
messageOrder.push(canId);
}
canMessages[canId] = {
timestamp: data.timestamp,
dlc: data.dlc,
data: data.data,
updateCount: data.count
};
}
function updateCanBatch(messages) {
messages.forEach(msg => {
const canId = msg.id;
if (!canMessages[canId]) {
messageOrder.push(canId);
}
canMessages[canId] = {
timestamp: msg.timestamp,
dlc: msg.dlc,
data: msg.data,
updateCount: msg.count
};
});
updateCanTable();
}
function updateCanTable() {
const tbody = document.getElementById('can-messages');
const existingRows = new Map();
Array.from(tbody.rows).forEach(row => {
existingRows.set(row.dataset.canId, row);
});
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; font-size: 0.9em;">No log files</p>';
return;
}
files.sort((a, b) => {
const numA = parseInt(a.name.match(/\d+/)?.[0] || '0');
const numB = parseInt(b.name.match(/\d+/)?.[0] || '0');
return numB - numA;
});
fileList.innerHTML = '';
files.forEach(file => {
const fileItem = document.createElement('div');
fileItem.className = 'file-item';
fileItem.innerHTML =
'<div style="flex: 1; min-width: 0;">' +
'<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);
});
}
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'}));
}
}
function clearMessages() {
canMessages = {};
messageOrder = [];
document.getElementById('can-messages').innerHTML = '';
}
function downloadFile(filename) {
window.location.href = '/download?file=' + encodeURIComponent(filename);
}
initWebSocket();
setTimeout(() => { refreshFiles(); }, 2000);
</script>
</body>
</html>
)rawliteral";
#endif