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

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