00004 monitor 창 graph연결 버튼 추가, 핸드폰 자동화면 변경

여전히 메인 웹페이지에서 monitor, Transmit 밖에 선택지가 없는데 다시한번 누락되어 있는 부분 확인해줘, 그리고 핸드폰 에서 웹페이지 접속시 가로창 넘게 표시되어 이미지가 짤리는데 핸드폰 화면에 맞추어 글자와 이미지들이 들어갈 수 있게 자동 조절하게 해줄 수 있어? 특히 전송 웹페이지에 data byte(hex) 입력 칸들이 화면에 다 안들어가서 짤린상태로 전체 값들을 넣을 수 가 없어
This commit is contained in:
2025-10-05 16:36:18 +00:00
parent c5b940318f
commit 2dd7f9177f
4 changed files with 724 additions and 145 deletions

View File

@@ -16,6 +16,7 @@
#include <freertos/semphr.h>
#include "index.h"
#include "transmit.h"
#include "graph.h" // 그래프 페이지 추가
// GPIO 핀 정의
#define CAN_INT_PIN 27
@@ -738,6 +739,10 @@ void setup() {
server.send_P(200, "text/html", transmit_html);
});
server.on("/graph", HTTP_GET, []() {
server.send_P(200, "text/html", graph_html);
});
server.on("/download", HTTP_GET, []() {
if (server.hasArg("file")) {
String filename = "/" + server.arg("file");
@@ -783,8 +788,10 @@ void setup() {
Serial.println(" 1. WiFi: ESP32_CAN_Logger (12345678)");
Serial.print(" 2. http://");
Serial.println(WiFi.softAPIP());
Serial.println(" 3. Monitor: /");
Serial.println(" 4. Transmit: /transmit");
Serial.println(" 3. Pages:");
Serial.println(" - Monitor: /");
Serial.println(" - Transmit: /transmit");
Serial.println(" - Graph: /graph");
Serial.println("========================================\n");
}

495
graph.h Normal file
View File

@@ -0,0 +1,495 @@
#ifndef GRAPH_H
#define GRAPH_H
const char graph_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CAN Signal Graph</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #43cea2 0%, #185a9d 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1600px;
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, #43cea2 0%, #185a9d 100%);
color: white;
padding: 30px;
text-align: center;
}
.header h1 { font-size: 2.5em; margin-bottom: 10px; }
.nav {
background: #2c3e50;
padding: 15px 30px;
display: flex;
gap: 20px;
}
.nav a {
color: white;
text-decoration: none;
padding: 10px 20px;
border-radius: 5px;
transition: all 0.3s;
}
.nav a:hover { background: #34495e; }
.nav a.active { background: #3498db; }
.content { padding: 30px; }
.dbc-upload {
background: #f8f9fa;
padding: 25px;
border-radius: 10px;
margin-bottom: 20px;
}
.upload-area {
border: 3px dashed #43cea2;
border-radius: 10px;
padding: 40px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
}
.upload-area:hover { background: #f0f9ff; border-color: #185a9d; }
.upload-area input { display: none; }
.signal-selector {
background: #f8f9fa;
padding: 20px;
border-radius: 10px;
margin-bottom: 20px;
}
.signal-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 15px;
margin-top: 15px;
}
.signal-item {
background: white;
padding: 15px;
border-radius: 8px;
border: 2px solid #ddd;
cursor: pointer;
transition: all 0.3s;
}
.signal-item:hover { border-color: #43cea2; transform: translateY(-2px); }
.signal-item.selected { border-color: #185a9d; background: #e3f2fd; }
.signal-name { font-weight: 600; color: #333; margin-bottom: 5px; }
.signal-info { font-size: 0.85em; color: #666; }
.graph-container {
background: white;
padding: 20px;
border-radius: 10px;
margin-bottom: 20px;
}
.graph-canvas {
position: relative;
height: 400px;
}
.controls {
display: flex;
gap: 10px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.btn {
padding: 12px 25px;
border: none;
border-radius: 5px;
font-size: 1em;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
.btn-primary { background: linear-gradient(135deg, #43cea2 0%, #185a9d 100%); color: white; }
.btn-success { background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); color: white; }
.btn-danger { background: linear-gradient(135deg, #eb3349 0%, #f45c43 100%); color: white; }
.btn:hover { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(0,0,0,0.3); }
h2 {
color: #333;
margin: 30px 0 20px 0;
padding-bottom: 10px;
border-bottom: 3px solid #43cea2;
}
.status { padding: 15px; background: #fff3cd; border-radius: 5px; margin-bottom: 20px; }
.status.success { background: #d4edda; color: #155724; }
.status.error { background: #f8d7da; color: #721c24; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📈 CAN Signal Graph</h1>
<p>Real-time Signal Visualization</p>
</div>
<div class="nav">
<a href="/">📊 Monitor</a>
<a href="/transmit">📡 Transmit</a>
<a href="/graph" class="active">📈 Graph</a>
</div>
<div class="content">
<div id="status" class="status" style="display:none;"></div>
<h2>📁 Upload DBC File</h2>
<div class="dbc-upload">
<div class="upload-area" onclick="document.getElementById('dbc-file').click()">
<input type="file" id="dbc-file" accept=".dbc" onchange="loadDBCFile(event)">
<p style="font-size: 1.2em; margin-bottom: 10px;">🗂 Click to upload DBC file</p>
<p style="color: #666;">or drag and drop here</p>
</div>
</div>
<div id="signal-section" style="display:none;">
<h2>🎯 Select Signals (Max 6)</h2>
<div class="controls">
<button class="btn btn-success" onclick="startGraphing()"> Start Graphing</button>
<button class="btn btn-danger" onclick="stopGraphing()"> Stop</button>
<button class="btn btn-primary" onclick="clearSelection()">🗑 Clear Selection</button>
</div>
<div class="signal-selector">
<div id="signal-list" class="signal-grid"></div>
</div>
<h2>📊 Real-time Graphs</h2>
<div id="graphs"></div>
</div>
</div>
</div>
<script>
let ws;
let dbcData = {};
let selectedSignals = [];
let charts = {};
let graphing = false;
const MAX_SIGNALS = 6;
const MAX_DATA_POINTS = 60;
const COLORS = ['#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF', '#FF9F40'];
function initWebSocket() {
ws = new WebSocket('ws://' + window.location.hostname + ':81');
ws.onopen = function() {
console.log('WebSocket connected');
};
ws.onclose = function() {
console.log('WebSocket disconnected');
setTimeout(initWebSocket, 3000);
};
ws.onmessage = function(event) {
const data = JSON.parse(event.data);
if (data.type === 'canBatch' && graphing) {
processCANData(data.messages);
}
};
}
function loadDBCFile(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(e) {
const content = e.target.result;
parseDBCContent(content);
};
reader.readAsText(file);
}
function parseDBCContent(content) {
dbcData = {messages: {}};
const lines = content.split('\n');
let currentMessage = null;
for (let line of lines) {
line = line.trim();
if (line.startsWith('BO_ ')) {
const match = line.match(/BO_ (\d+) (\w+):/);
if (match) {
const id = parseInt(match[1]);
const name = match[2];
currentMessage = {id: id, name: name, signals: []};
dbcData.messages[id] = currentMessage;
}
}
else if (line.startsWith('SG_ ') && currentMessage) {
const match = line.match(/SG_ (\w+) : (\d+)\|(\d+)@([01])([+-]) \(([^,]+),([^)]+)\) \[([^\]]+)\] "([^"]*)"/);
if (match) {
const signal = {
name: match[1],
startBit: parseInt(match[2]),
bitLength: parseInt(match[3]),
byteOrder: match[4] === '0' ? 'motorola' : 'intel',
signed: match[5] === '-',
factor: parseFloat(match[6]),
offset: parseFloat(match[7]),
unit: match[9],
messageId: currentMessage.id,
messageName: currentMessage.name
};
currentMessage.signals.push(signal);
}
}
}
displaySignals();
showStatus('DBC file loaded successfully! Found ' +
Object.keys(dbcData.messages).length + ' messages.', 'success');
}
function displaySignals() {
const signalList = document.getElementById('signal-list');
signalList.innerHTML = '';
for (let msgId in dbcData.messages) {
const msg = dbcData.messages[msgId];
msg.signals.forEach(signal => {
const item = document.createElement('div');
item.className = 'signal-item';
item.onclick = () => toggleSignal(signal, item);
item.innerHTML =
'<div class="signal-name">' + signal.name + '</div>' +
'<div class="signal-info">' +
'ID: 0x' + signal.messageId.toString(16).toUpperCase() + ' | ' +
signal.bitLength + ' bits | ' +
(signal.unit || 'no unit') +
'</div>';
signalList.appendChild(item);
});
}
document.getElementById('signal-section').style.display = 'block';
}
function toggleSignal(signal, element) {
const index = selectedSignals.findIndex(s =>
s.messageId === signal.messageId && s.name === signal.name);
if (index >= 0) {
selectedSignals.splice(index, 1);
element.classList.remove('selected');
} else {
if (selectedSignals.length >= MAX_SIGNALS) {
showStatus('Maximum ' + MAX_SIGNALS + ' signals allowed!', 'error');
return;
}
selectedSignals.push(signal);
element.classList.add('selected');
}
}
function clearSelection() {
selectedSignals = [];
document.querySelectorAll('.signal-item').forEach(item => {
item.classList.remove('selected');
});
}
function startGraphing() {
if (selectedSignals.length === 0) {
showStatus('Please select at least one signal!', 'error');
return;
}
graphing = true;
createGraphs();
showStatus('Graphing started for ' + selectedSignals.length + ' signals', 'success');
}
function stopGraphing() {
graphing = false;
showStatus('Graphing stopped', 'success');
}
function createGraphs() {
const graphsDiv = document.getElementById('graphs');
graphsDiv.innerHTML = '';
charts = {};
selectedSignals.forEach((signal, index) => {
const container = document.createElement('div');
container.className = 'graph-container';
const canvas = document.createElement('canvas');
canvas.id = 'chart-' + index;
container.innerHTML = '<h3>' + signal.name +
' (0x' + signal.messageId.toString(16).toUpperCase() + ')' +
(signal.unit ? ' [' + signal.unit + ']' : '') + '</h3>';
container.appendChild(canvas);
graphsDiv.appendChild(container);
const ctx = canvas.getContext('2d');
charts[index] = new Chart(ctx, {
type: 'line',
data: {
labels: [],
datasets: [{
label: signal.name,
data: [],
borderColor: COLORS[index % COLORS.length],
backgroundColor: COLORS[index % COLORS.length] + '33',
tension: 0.4,
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
display: true,
title: { display: true, text: 'Time' }
},
y: {
display: true,
title: { display: true, text: signal.unit || 'Value' }
}
},
animation: false
}
});
});
}
function processCANData(messages) {
const now = new Date().toLocaleTimeString();
messages.forEach(canMsg => {
const msgId = parseInt(canMsg.id, 16);
selectedSignals.forEach((signal, index) => {
if (signal.messageId === msgId && charts[index]) {
const value = decodeSignal(signal, canMsg.data);
const chart = charts[index];
chart.data.labels.push(now);
chart.data.datasets[0].data.push(value);
if (chart.data.labels.length > MAX_DATA_POINTS) {
chart.data.labels.shift();
chart.data.datasets[0].data.shift();
}
chart.update('none');
}
});
});
}
function decodeSignal(signal, hexData) {
const bytes = [];
for (let i = 0; i < hexData.length; i += 3) {
bytes.push(parseInt(hexData.substr(i, 2), 16));
}
let rawValue = 0;
const startByte = Math.floor(signal.startBit / 8);
const startBitInByte = signal.startBit % 8;
if (signal.byteOrder === 'intel') {
for (let i = 0; i < signal.bitLength; i++) {
const bitPos = signal.startBit + i;
const byteIdx = Math.floor(bitPos / 8);
const bitIdx = bitPos % 8;
if (byteIdx < bytes.length) {
const bit = (bytes[byteIdx] >> bitIdx) & 1;
rawValue |= (bit << i);
}
}
} else {
for (let i = 0; i < signal.bitLength; i++) {
const bitPos = signal.startBit - i;
const byteIdx = Math.floor(bitPos / 8);
const bitIdx = bitPos % 8;
if (byteIdx < bytes.length) {
const bit = (bytes[byteIdx] >> bitIdx) & 1;
rawValue |= (bit << (signal.bitLength - 1 - i));
}
}
}
if (signal.signed && (rawValue & (1 << (signal.bitLength - 1)))) {
rawValue -= (1 << signal.bitLength);
}
return rawValue * signal.factor + signal.offset;
}
function showStatus(message, type) {
const status = document.getElementById('status');
status.textContent = message;
status.className = 'status ' + type;
status.style.display = 'block';
setTimeout(() => { status.style.display = 'none'; }, 5000);
}
const uploadArea = document.querySelector('.upload-area');
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
uploadArea.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
['dragenter', 'dragover'].forEach(eventName => {
uploadArea.addEventListener(eventName, () => {
uploadArea.style.borderColor = '#185a9d';
uploadArea.style.background = '#f0f9ff';
});
});
['dragleave', 'drop'].forEach(eventName => {
uploadArea.addEventListener(eventName, () => {
uploadArea.style.borderColor = '#43cea2';
uploadArea.style.background = '';
});
});
uploadArea.addEventListener('drop', function(e) {
const file = e.dataTransfer.files[0];
if (file && file.name.endsWith('.dbc')) {
const reader = new FileReader();
reader.onload = function(ev) {
parseDBCContent(ev.target.result);
};
reader.readAsText(file);
}
});
initWebSocket();
</script>
</body>
</html>
)rawliteral";
#endif

153
index.h
View File

@@ -6,7 +6,7 @@ const char index_html[] PROGMEM = R"rawliteral(
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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; }
@@ -14,7 +14,7 @@ const char index_html[] PROGMEM = R"rawliteral(
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
padding: 10px;
}
.container {
max-width: 1400px;
@@ -27,64 +27,68 @@ const char index_html[] PROGMEM = R"rawliteral(
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
padding: 20px;
text-align: center;
}
.header h1 { font-size: 2.5em; margin-bottom: 10px; }
.header p { opacity: 0.9; font-size: 1.1em; }
.header h1 { font-size: 1.8em; margin-bottom: 5px; }
.header p { opacity: 0.9; font-size: 0.9em; }
.nav {
background: #2c3e50;
padding: 15px 30px;
padding: 10px;
display: flex;
gap: 20px;
gap: 10px;
flex-wrap: wrap;
justify-content: center;
}
.nav a {
color: white;
text-decoration: none;
padding: 10px 20px;
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: 30px; }
.content { padding: 15px; }
.status-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
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: 20px;
padding: 15px;
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-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: 20px;
padding: 15px;
border-radius: 10px;
margin-bottom: 30px;
margin-bottom: 20px;
}
.control-row {
display: flex;
gap: 15px;
gap: 10px;
align-items: center;
flex-wrap: wrap;
margin-bottom: 15px;
margin-bottom: 10px;
}
.control-row:last-child { margin-bottom: 0; }
label { font-weight: 600; color: #333; }
label { font-weight: 600; color: #333; font-size: 0.9em; }
select, button {
padding: 10px 20px;
padding: 8px 15px;
border: none;
border-radius: 5px;
font-size: 1em;
font-size: 0.9em;
cursor: pointer;
transition: all 0.3s;
}
@@ -103,11 +107,13 @@ const char index_html[] PROGMEM = R"rawliteral(
.can-table-container {
background: #f8f9fa;
border-radius: 10px;
padding: 20px;
padding: 10px;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
table {
width: 100%;
min-width: 500px;
border-collapse: collapse;
background: white;
border-radius: 8px;
@@ -116,14 +122,16 @@ const char index_html[] PROGMEM = R"rawliteral(
th {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 15px;
padding: 10px 8px;
text-align: left;
font-weight: 600;
font-size: 0.85em;
}
td {
padding: 12px 15px;
padding: 8px;
border-bottom: 1px solid #e9ecef;
font-family: 'Courier New', monospace;
font-size: 0.8em;
}
tr:hover { background: #f8f9fa; }
.flash-row {
@@ -136,50 +144,69 @@ const char index_html[] PROGMEM = R"rawliteral(
.mono { font-family: 'Courier New', monospace; }
h2 {
color: #333;
margin: 30px 0 20px 0;
padding-bottom: 10px;
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: 20px;
padding: 15px;
}
.file-item {
background: white;
padding: 15px;
margin-bottom: 10px;
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; }
.file-size { color: #666; margin-left: 15px; }
.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: 8px 16px;
font-size: 0.9em;
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>
<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="/" 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 STATUS</h3>
<h3>LOGGING</h3>
<div class="value">OFF</div>
</div>
<div class="status-card" id="sd-status">
@@ -192,16 +219,16 @@ const char index_html[] PROGMEM = R"rawliteral(
</div>
<div class="status-card">
<h3>SPEED</h3>
<div class="value" id="msg-speed">0 msg/s</div>
<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: 1.3em;">-</div>
<div class="value" id="current-file" style="font-size: 1em;">-</div>
</div>
</div>
<div class="control-panel">
<h2> Control Panel</h2>
<h2>Control Panel</h2>
<div class="control-row">
<label for="can-speed">CAN Speed:</label>
<select id="can-speed">
@@ -210,33 +237,33 @@ const char index_html[] PROGMEM = R"rawliteral(
<option value="2">500 Kbps</option>
<option value="3" selected>1 Mbps</option>
</select>
<button onclick="setCanSpeed()">Apply Speed</button>
<button onclick="setCanSpeed()">Apply</button>
</div>
<div class="control-row">
<button onclick="refreshFiles()">🔄 Refresh Files</button>
<button onclick="clearMessages()">🗑 Clear Display</button>
<button onclick="refreshFiles()">Refresh Files</button>
<button onclick="clearMessages()">Clear Display</button>
</div>
</div>
<h2>📊 Real-time CAN Messages (by ID)</h2>
<h2>CAN Messages (by ID)</h2>
<div class="can-table-container">
<table>
<thead>
<tr>
<th>CAN ID</th>
<th>ID</th>
<th>DLC</th>
<th>Data</th>
<th>Count</th>
<th>Last Time (ms)</th>
<th>Time(ms)</th>
</tr>
</thead>
<tbody id="can-messages"></tbody>
</table>
</div>
<h2>💾 Log Files</h2>
<h2>Log Files</h2>
<div class="file-list" id="file-list">
<p style="text-align: center; color: #666;">Loading files...</p>
<p style="text-align: center; color: #666; font-size: 0.9em;">Loading...</p>
</div>
</div>
</div>
@@ -263,7 +290,6 @@ const char index_html[] PROGMEM = R"rawliteral(
ws.onmessage = function(event) {
const data = JSON.parse(event.data);
console.log(':', data.type);
if (data.type === 'status') {
updateStatus(data);
@@ -272,10 +298,9 @@ const char index_html[] PROGMEM = R"rawliteral(
} 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>';
'<p style="text-align: center; color: #e74c3c; font-size: 0.9em;">Error: ' + data.error + '</p>';
} else {
updateFileList(data.files);
}
@@ -318,7 +343,7 @@ const char index_html[] PROGMEM = R"rawliteral(
}
document.getElementById('msg-count').textContent = data.msgCount.toLocaleString();
document.getElementById('msg-speed').textContent = data.msgSpeed + ' msg/s';
document.getElementById('msg-speed').textContent = data.msgSpeed + '/s';
}
function addCanMessage(data) {
@@ -387,7 +412,7 @@ const char index_html[] PROGMEM = R"rawliteral(
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>';
fileList.innerHTML = '<p style="text-align: center; color: #666; font-size: 0.9em;">No log files</p>';
return;
}
@@ -402,15 +427,13 @@ const char index_html[] PROGMEM = R"rawliteral(
const fileItem = document.createElement('div');
fileItem.className = 'file-item';
fileItem.innerHTML =
'<div>' +
'<span class="file-name">📄 ' + file.name + '</span>' +
'<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>';
'<button class="download-btn" onclick="downloadFile(\'' + file.name + '\')">Download</button>';
fileList.appendChild(fileItem);
});
console.log(' : ' + files.length + ' ()');
}
function formatBytes(bytes) {
@@ -427,11 +450,6 @@ const char index_html[] PROGMEM = R"rawliteral(
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>';
}
}
@@ -446,12 +464,7 @@ const char index_html[] PROGMEM = R"rawliteral(
}
initWebSocket();
setTimeout(() => {
if (!document.getElementById('file-list').querySelector('.file-item')) {
console.log(' ');
refreshFiles();
}
}, 2000);
setTimeout(() => { refreshFiles(); }, 2000);
</script>
</body>
</html>

View File

@@ -6,7 +6,7 @@ const char transmit_html[] PROGMEM = R"rawliteral(
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>CAN Transmitter</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
@@ -14,7 +14,7 @@ const char transmit_html[] PROGMEM = R"rawliteral(
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
min-height: 100vh;
padding: 20px;
padding: 10px;
}
.container {
max-width: 1200px;
@@ -27,39 +27,42 @@ const char transmit_html[] PROGMEM = R"rawliteral(
.header {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: white;
padding: 30px;
padding: 20px;
text-align: center;
}
.header h1 { font-size: 2.5em; margin-bottom: 10px; }
.header p { opacity: 0.9; font-size: 1.1em; }
.header h1 { font-size: 1.8em; margin-bottom: 5px; }
.header p { opacity: 0.9; font-size: 0.9em; }
.nav {
background: #2c3e50;
padding: 15px 30px;
padding: 10px;
display: flex;
gap: 20px;
gap: 10px;
flex-wrap: wrap;
justify-content: center;
}
.nav a {
color: white;
text-decoration: none;
padding: 10px 20px;
padding: 10px 15px;
border-radius: 5px;
transition: all 0.3s;
font-size: 0.9em;
}
.nav a:hover { background: #34495e; }
.nav a.active { background: #3498db; }
.content { padding: 30px; }
.content { padding: 15px; }
.message-form {
background: #f8f9fa;
padding: 25px;
padding: 20px;
border-radius: 10px;
margin-bottom: 20px;
}
.form-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 15px;
gap: 12px;
margin-bottom: 12px;
}
.form-group {
display: flex;
@@ -69,12 +72,13 @@ const char transmit_html[] PROGMEM = R"rawliteral(
font-weight: 600;
margin-bottom: 5px;
color: #333;
font-size: 0.9em;
}
.form-group input, .form-group select {
padding: 10px;
border: 2px solid #ddd;
border-radius: 5px;
font-size: 1em;
font-size: 0.95em;
font-family: 'Courier New', monospace;
}
.form-group input:focus, .form-group select:focus {
@@ -83,18 +87,22 @@ const char transmit_html[] PROGMEM = R"rawliteral(
}
.data-bytes {
display: grid;
grid-template-columns: repeat(8, 1fr);
gap: 10px;
grid-template-columns: repeat(auto-fit, minmax(45px, 1fr));
gap: 8px;
max-width: 100%;
}
.data-bytes input {
text-align: center;
text-transform: uppercase;
width: 100%;
min-width: 45px;
padding: 10px 5px;
}
.btn {
padding: 12px 25px;
padding: 12px 20px;
border: none;
border-radius: 5px;
font-size: 1em;
font-size: 0.95em;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
@@ -116,16 +124,16 @@ const char transmit_html[] PROGMEM = R"rawliteral(
.message-list {
background: #f8f9fa;
border-radius: 10px;
padding: 20px;
padding: 15px;
}
.message-item {
background: white;
padding: 15px;
padding: 12px;
margin-bottom: 10px;
border-radius: 8px;
display: grid;
grid-template-columns: 100px 80px 1fr 150px 120px;
gap: 15px;
grid-template-columns: 90px 70px 1fr auto;
gap: 10px;
align-items: center;
border-left: 4px solid #f093fb;
}
@@ -133,66 +141,129 @@ const char transmit_html[] PROGMEM = R"rawliteral(
border-left-color: #38ef7d;
background: #f0fff4;
}
.message-id { font-weight: 700; color: #f5576c; font-family: 'Courier New', monospace; }
.message-id {
font-weight: 700;
color: #f5576c;
font-family: 'Courier New', monospace;
font-size: 0.9em;
}
.message-type {
padding: 5px 10px;
padding: 4px 8px;
border-radius: 5px;
font-size: 0.85em;
font-size: 0.75em;
font-weight: 600;
text-align: center;
}
.type-std { background: #3498db; color: white; }
.type-ext { background: #9b59b6; color: white; }
.message-data { font-family: 'Courier New', monospace; color: #666; }
.message-interval { color: #888; font-size: 0.9em; }
.message-data {
font-family: 'Courier New', monospace;
color: #666;
font-size: 0.85em;
word-break: break-all;
}
.message-interval {
color: #888;
font-size: 0.8em;
}
.message-controls {
display: flex;
gap: 5px;
flex-wrap: wrap;
}
.btn-small {
padding: 5px 12px;
font-size: 0.85em;
padding: 6px 12px;
font-size: 0.8em;
}
h2 {
color: #333;
margin: 30px 0 20px 0;
padding-bottom: 10px;
margin: 20px 0 15px 0;
padding-bottom: 8px;
border-bottom: 3px solid #f093fb;
font-size: 1.3em;
}
.status-bar {
background: #2c3e50;
color: white;
padding: 10px 20px;
padding: 10px 15px;
border-radius: 5px;
margin-bottom: 20px;
margin-bottom: 15px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.9em;
}
.status-bar span { font-weight: 600; }
@media (max-width: 768px) {
body { padding: 5px; }
.header h1 { font-size: 1.5em; }
.content { padding: 10px; }
.form-row { grid-template-columns: 1fr; }
.data-bytes {
grid-template-columns: repeat(4, 1fr);
gap: 6px;
}
.data-bytes input {
min-width: 40px;
font-size: 0.9em;
padding: 8px 4px;
}
.message-item {
grid-template-columns: 1fr;
gap: 8px;
}
.message-controls {
justify-content: flex-start;
}
.control-row {
flex-direction: column;
align-items: stretch;
}
.control-row > * {
width: 100%;
}
.nav {
padding: 8px;
gap: 5px;
}
.nav a {
padding: 8px 10px;
font-size: 0.85em;
}
h2 {
font-size: 1.1em;
}
.status-bar {
flex-direction: column;
gap: 8px;
text-align: center;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📡 CAN Transmitter</h1>
<h1>CAN Transmitter</h1>
<p>Send CAN Messages</p>
</div>
<div class="nav">
<a href="/">📊 Monitor</a>
<a href="/transmit" class="active">📡 Transmit</a>
<a href="/">Monitor</a>
<a href="/transmit" class="active">Transmit</a>
<a href="/graph">Graph</a>
</div>
<div class="content">
<div class="status-bar">
<span id="connection-status">🔴 Disconnected</span>
<span id="tx-count">Sent: 0 messages</span>
<span id="connection-status">Disconnected</span>
<span id="tx-count">Sent: 0</span>
</div>
<h2> Add CAN Message</h2>
<h2>Add CAN Message</h2>
<div class="message-form">
<div class="form-row">
<div class="form-group">
@@ -200,10 +271,10 @@ const char transmit_html[] PROGMEM = R"rawliteral(
<input type="text" id="can-id" placeholder="123" maxlength="8">
</div>
<div class="form-group">
<label>Message Type</label>
<label>Type</label>
<select id="msg-type">
<option value="std">Standard (11-bit)</option>
<option value="ext">Extended (29-bit)</option>
<option value="std">Standard</option>
<option value="ext">Extended</option>
</select>
</div>
<div class="form-group">
@@ -240,21 +311,21 @@ const char transmit_html[] PROGMEM = R"rawliteral(
</div>
</div>
<div style="display: flex; gap: 10px;">
<button class="btn btn-primary" onclick="addMessage()"> Add to List</button>
<button class="btn btn-success" onclick="sendOnce()">📤 Send Once</button>
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
<button class="btn btn-primary" onclick="addMessage()">Add to List</button>
<button class="btn btn-success" onclick="sendOnce()">Send Once</button>
</div>
</div>
<h2>📋 Message List</h2>
<div style="margin-bottom: 15px; display: flex; gap: 10px;">
<button class="btn btn-success" onclick="startAll()"> Start All</button>
<button class="btn btn-danger" onclick="stopAll()"> Stop All</button>
<button class="btn btn-danger" onclick="clearAll()">🗑 Clear All</button>
<h2>Message List</h2>
<div style="margin-bottom: 15px; display: flex; gap: 10px; flex-wrap: wrap;">
<button class="btn btn-success" onclick="startAll()">Start All</button>
<button class="btn btn-danger" onclick="stopAll()">Stop All</button>
<button class="btn btn-danger" onclick="clearAll()">Clear All</button>
</div>
<div class="message-list" id="message-list">
<p style="text-align: center; color: #666;">No messages added yet</p>
<p style="text-align: center; color: #666; font-size: 0.9em;">No messages added yet</p>
</div>
</div>
</div>
@@ -269,12 +340,12 @@ const char transmit_html[] PROGMEM = R"rawliteral(
ws.onopen = function() {
console.log('WebSocket connected');
document.getElementById('connection-status').innerHTML = '🟢 Connected';
document.getElementById('connection-status').innerHTML = 'Connected';
};
ws.onclose = function() {
console.log('WebSocket disconnected');
document.getElementById('connection-status').innerHTML = '🔴 Disconnected';
document.getElementById('connection-status').innerHTML = 'Disconnected';
setTimeout(initWebSocket, 3000);
};
@@ -282,7 +353,7 @@ const char transmit_html[] PROGMEM = R"rawliteral(
const data = JSON.parse(event.data);
if (data.type === 'txStatus') {
txCount = data.count;
document.getElementById('tx-count').textContent = 'Sent: ' + txCount + ' messages';
document.getElementById('tx-count').textContent = 'Sent: ' + txCount;
}
};
}
@@ -294,7 +365,7 @@ const char transmit_html[] PROGMEM = R"rawliteral(
const interval = parseInt(document.getElementById('interval').value);
if (!id || !/^[0-9A-F]+$/.test(id)) {
alert('Invalid CAN ID! Use hex format (e.g., 123, 1A2)');
alert('Invalid CAN ID!');
return;
}
@@ -302,7 +373,7 @@ const char transmit_html[] PROGMEM = R"rawliteral(
for (let i = 0; i < 8; i++) {
const val = document.getElementById('d' + i).value.toUpperCase();
if (!/^[0-9A-F]{0,2}$/.test(val)) {
alert('Invalid data byte D' + i + '! Use hex format (00-FF)');
alert('Invalid data byte D' + i + '!');
return;
}
data.push(val.padStart(2, '0'));
@@ -343,14 +414,13 @@ const char transmit_html[] PROGMEM = R"rawliteral(
data: data.join('')
};
ws.send(JSON.stringify(cmd));
console.log('Sent:', cmd);
}
function updateMessageList() {
const list = document.getElementById('message-list');
if (messages.length === 0) {
list.innerHTML = '<p style="text-align: center; color: #666;">No messages added yet</p>';
list.innerHTML = '<p style="text-align: center; color: #666; font-size: 0.9em;">No messages</p>';
return;
}
@@ -359,21 +429,16 @@ const char transmit_html[] PROGMEM = R"rawliteral(
const item = document.createElement('div');
item.className = 'message-item' + (msg.active ? ' active' : '');
item.innerHTML = `
<div class="message-id">0x${msg.id}</div>
<div class="message-type type-${msg.type}">${msg.type.toUpperCase()}</div>
<div class="message-data">${msg.data.slice(0, msg.dlc).join(' ')}</div>
<div class="message-interval">Every ${msg.interval}ms</div>
<div class="message-controls">
<button class="btn ${msg.active ? 'btn-danger' : 'btn-success'} btn-small"
onclick="toggleMessage(${index})">
${msg.active ? ' Stop' : ' Start'}
</button>
<button class="btn btn-danger btn-small" onclick="deleteMessage(${index})">
🗑
</button>
</div>
`;
item.innerHTML =
'<div class="message-id">0x' + msg.id + '</div>' +
'<div class="message-type type-' + msg.type + '">' + msg.type.toUpperCase() + '</div>' +
'<div class="message-data">' + msg.data.slice(0, msg.dlc).join(' ') + '</div>' +
'<div class="message-controls">' +
'<button class="btn ' + (msg.active ? 'btn-danger' : 'btn-success') + ' btn-small" onclick="toggleMessage(' + index + ')">' +
(msg.active ? 'Stop' : 'Start') +
'</button>' +
'<button class="btn btn-danger btn-small" onclick="deleteMessage(' + index + ')">Delete</button>' +
'</div>';
list.appendChild(item);
});
@@ -426,7 +491,6 @@ const char transmit_html[] PROGMEM = R"rawliteral(
}
}
// Hex input validation
document.querySelectorAll('input[id^="d"]').forEach(input => {
input.addEventListener('input', function() {
this.value = this.value.toUpperCase().replace(/[^0-9A-F]/g, '');