00006 graph 창 변수 dbc 선택 유지

graph 페이지에서 변수를 선택하고 start 누를 시 그래프 표출을 또다른 창 이동해서 표현하는 것이 나을 것 같아 그리고 선택한 변수들은 다른 페이지 이동해도 선택이 리셋되지 않게 선택한변수들을 유지하게 해줘
This commit is contained in:
2025-10-06 17:08:25 +00:00
parent 3af955852a
commit 1bf6186305
4 changed files with 586 additions and 55 deletions

View File

@@ -17,6 +17,7 @@
#include "index.h"
#include "transmit.h"
#include "graph.h" // 그래프 페이지 추가
#include "graph_viewer.h" // 새로 추가
// GPIO 핀 정의
#define CAN_INT_PIN 27
@@ -742,7 +743,12 @@ void setup() {
server.on("/graph", HTTP_GET, []() {
server.send_P(200, "text/html", graph_html);
});
// ⭐ 새로 추가: 그래프 뷰어 페이지
server.on("/graph-view", HTTP_GET, []() {
server.send_P(200, "text/html", graph_viewer_html);
});
server.on("/download", HTTP_GET, []() {
if (server.hasArg("file")) {
String filename = "/" + server.arg("file");

148
graph.h
View File

@@ -206,8 +206,12 @@ const char graph_html[] PROGMEM = R"rawliteral(
<div id="signal-list" class="signal-grid"></div>
</div>
<h2>Real-time Graphs</h2>
<div id="graphs"></div>
<div style="background: #e3f2fd; padding: 15px; border-radius: 8px; margin-top: 15px; border-left: 4px solid #185a9d;">
<p style="color: #333; font-size: 0.9em; margin: 0;">
<strong> Info:</strong> Click "Start" to open a new window with real-time graphs.
Your selected signals will be saved automatically.
</p>
</div>
</div>
</div>
</div>
@@ -518,6 +522,9 @@ const char graph_html[] PROGMEM = R"rawliteral(
}
document.getElementById('signal-section').style.display = 'block';
// 저장된 선택 복원
setTimeout(() => loadSelectedSignals(), 100);
}
function toggleSignal(signal, element) {
@@ -535,6 +542,9 @@ const char graph_html[] PROGMEM = R"rawliteral(
selectedSignals.push(signal);
element.classList.add('selected');
}
// 선택한 신호 저장
saveSelectedSignals();
}
function clearSelection() {
@@ -542,6 +552,69 @@ const char graph_html[] PROGMEM = R"rawliteral(
document.querySelectorAll('.signal-item').forEach(item => {
item.classList.remove('selected');
});
// 저장된 선택 삭제
saveSelectedSignals();
}
// 선택한 신호를 localStorage에 저장
function saveSelectedSignals() {
try {
localStorage.setItem('selected_signals', JSON.stringify(selectedSignals));
console.log('Saved', selectedSignals.length, 'signals');
} catch(e) {
console.error('Failed to save selected signals:', e);
}
}
// localStorage에서 선택한 신호 복원
function loadSelectedSignals() {
try {
const saved = localStorage.getItem('selected_signals');
if (saved) {
const signals = JSON.parse(saved);
// 신호 목록이 표시된 후에 선택 상태 복원
signals.forEach(savedSignal => {
// DBC에 해당 신호가 있는지 확인
let found = false;
for (let msgId in dbcData.messages) {
const msg = dbcData.messages[msgId];
const signal = msg.signals.find(s =>
s.messageId === savedSignal.messageId && s.name === savedSignal.name);
if (signal) {
selectedSignals.push(signal);
found = true;
break;
}
}
});
// UI 업데이트
document.querySelectorAll('.signal-item').forEach(item => {
const signalName = item.querySelector('.signal-name').textContent;
const signalInfo = item.querySelector('.signal-info').textContent;
const idMatch = signalInfo.match(/ID: 0x([0-9A-F]+)/);
if (idMatch) {
const msgId = parseInt(idMatch[1], 16);
const isSelected = selectedSignals.some(s =>
s.messageId === msgId && s.name === signalName);
if (isSelected) {
item.classList.add('selected');
}
}
});
if (selectedSignals.length > 0) {
showStatus('Restored ' + selectedSignals.length + ' selected signals', 'success');
}
}
} catch(e) {
console.error('Failed to load selected signals:', e);
}
}
function startGraphing() {
@@ -550,67 +623,34 @@ const char graph_html[] PROGMEM = R"rawliteral(
return;
}
graphing = true;
startTime = Date.now(); // 시작 시간 기록
createGraphs();
showStatus('Graphing ' + selectedSignals.length + ' signals', 'success');
// 선택한 신호 저장 (새 창에서 사용)
saveSelectedSignals();
// 새 창 열기
const width = 1200;
const height = 800;
const left = (screen.width - width) / 2;
const top = (screen.height - height) / 2;
window.open(
'/graph-view',
'CAN_Graph_Viewer',
'width=' + width + ',height=' + height + ',left=' + left + ',top=' + top + ',resizable=yes,scrollbars=yes'
);
showStatus('Graph viewer opened in new window', 'success');
}
function stopGraphing() {
graphing = false;
showStatus('Stopped', 'success');
showStatus('Use the stop button in the graph viewer window', 'error');
}
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 =
'<div class="graph-header">' +
'<div class="graph-title">' + signal.name + ' (0x' + signal.messageId.toString(16).toUpperCase() + ')' +
(signal.unit ? ' [' + signal.unit + ']' : '') + '</div>' +
'<div class="graph-value" id="value-' + index + '">-</div>' +
'</div>';
container.appendChild(canvas);
graphsDiv.appendChild(container);
charts[index] = new SimpleChart(canvas, signal, index);
});
// 이 함수는 새 창에서 사용됨
}
function processCANData(messages) {
// 경과 시간 계산 (초 단위, 소수점 1자리)
const elapsedTime = ((Date.now() - startTime) / 1000).toFixed(1);
messages.forEach(canMsg => {
const idStr = canMsg.id.replace(/\s/g, '').toUpperCase();
const msgId = parseInt(idStr, 16);
selectedSignals.forEach((signal, index) => {
if (signal.messageId === msgId && charts[index]) {
try {
const value = decodeSignal(signal, canMsg.data);
charts[index].addData(value, elapsedTime);
const valueDiv = document.getElementById('value-' + index);
if (valueDiv) {
valueDiv.textContent = value.toFixed(2) + (signal.unit ? ' ' + signal.unit : '');
}
} catch(e) {
console.error('Error decoding signal', signal.name, ':', e);
}
}
});
});
// 이 함수는 새 창에서 사용됨
}
function decodeSignal(signal, hexData) {

468
graph_viewer.h Normal file
View File

@@ -0,0 +1,468 @@
#ifndef GRAPH_VIEWER_H
#define GRAPH_VIEWER_H
const char graph_viewer_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 Viewer</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #1a1a1a;
color: white;
overflow-x: hidden;
}
.header {
background: linear-gradient(135deg, #43cea2 0%, #185a9d 100%);
padding: 15px;
text-align: center;
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
}
.header h1 { font-size: 1.5em; }
.controls {
background: #2a2a2a;
padding: 10px 15px;
display: flex;
gap: 10px;
justify-content: center;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
}
.btn {
padding: 8px 20px;
border: none;
border-radius: 5px;
font-size: 0.9em;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
.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); }
.graphs {
padding: 15px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
gap: 15px;
}
.graph-container {
background: #2a2a2a;
padding: 15px;
border-radius: 10px;
border: 2px solid #3a3a3a;
}
.graph-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
padding-bottom: 8px;
border-bottom: 2px solid #43cea2;
}
.graph-title {
font-size: 1em;
font-weight: 600;
color: #43cea2;
}
.graph-value {
font-size: 1.2em;
font-weight: 700;
color: #38ef7d;
font-family: 'Courier New', monospace;
}
canvas {
width: 100%;
height: 250px;
border: 1px solid #3a3a3a;
border-radius: 5px;
background: #1a1a1a;
}
.status {
position: fixed;
top: 10px;
right: 10px;
padding: 10px 15px;
border-radius: 5px;
background: #2a2a2a;
border: 2px solid #43cea2;
font-size: 0.9em;
z-index: 1000;
}
.status.disconnected {
border-color: #eb3349;
background: #3a2a2a;
}
@media (max-width: 768px) {
.graphs { grid-template-columns: 1fr; }
canvas { height: 200px; }
}
</style>
</head>
<body>
<div class="header">
<h1>Real-time CAN Signal Graphs</h1>
</div>
<div class="controls">
<button class="btn btn-success" onclick="startGraphing()">Start</button>
<button class="btn btn-danger" onclick="stopGraphing()">Stop</button>
<button class="btn btn-danger" onclick="window.close()">Close</button>
</div>
<div class="status" id="status">Connecting...</div>
<div class="graphs" id="graphs"></div>
<script>
let ws;
let charts = {};
let graphing = false;
let startTime = 0;
let selectedSignals = [];
let dbcData = {};
const MAX_DATA_POINTS = 60;
const COLORS = [
{line: '#FF6384', fill: 'rgba(255, 99, 132, 0.2)'},
{line: '#36A2EB', fill: 'rgba(54, 162, 235, 0.2)'},
{line: '#FFCE56', fill: 'rgba(255, 206, 86, 0.2)'},
{line: '#4BC0C0', fill: 'rgba(75, 192, 192, 0.2)'},
{line: '#9966FF', fill: 'rgba(153, 102, 255, 0.2)'},
{line: '#FF9F40', fill: 'rgba(255, 159, 64, 0.2)'}
];
class SimpleChart {
constructor(canvas, signal, colorIndex) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.signal = signal;
this.data = [];
this.labels = [];
this.colors = COLORS[colorIndex % COLORS.length];
this.currentValue = 0;
this.resizeCanvas();
window.addEventListener('resize', () => this.resizeCanvas());
}
resizeCanvas() {
const rect = this.canvas.getBoundingClientRect();
this.canvas.width = rect.width * window.devicePixelRatio;
this.canvas.height = rect.height * window.devicePixelRatio;
this.ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
this.width = rect.width;
this.height = rect.height;
this.draw();
}
addData(value, label) {
this.data.push(value);
this.labels.push(label);
this.currentValue = value;
if (this.data.length > MAX_DATA_POINTS) {
this.data.shift();
this.labels.shift();
}
this.draw();
}
draw() {
if (this.data.length === 0) return;
const ctx = this.ctx;
const padding = 40;
const graphWidth = this.width - padding * 2;
const graphHeight = this.height - padding * 2;
ctx.clearRect(0, 0, this.width, this.height);
const minValue = Math.min(...this.data);
const maxValue = Math.max(...this.data);
const range = maxValue - minValue || 1;
// 축
ctx.strokeStyle = '#444';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(padding, padding);
ctx.lineTo(padding, this.height - padding);
ctx.lineTo(this.width - padding, this.height - padding);
ctx.stroke();
// Y축 라벨
ctx.fillStyle = '#aaa';
ctx.font = '11px Arial';
ctx.textAlign = 'right';
ctx.fillText(maxValue.toFixed(2), padding - 5, padding + 5);
ctx.fillText(minValue.toFixed(2), padding - 5, this.height - padding);
// X축 라벨
ctx.textAlign = 'center';
ctx.fillStyle = '#aaa';
ctx.font = '10px Arial';
if (this.labels.length > 0) {
ctx.fillText(this.labels[0] + 's', padding, this.height - padding + 15);
if (this.labels.length > 1) {
const lastIdx = this.labels.length - 1;
ctx.fillText(this.labels[lastIdx] + 's',
padding + (graphWidth / (MAX_DATA_POINTS - 1)) * lastIdx,
this.height - padding + 15);
}
}
ctx.fillText('Time (sec)', this.width / 2, this.height - 5);
// 그리드
ctx.strokeStyle = '#333';
ctx.lineWidth = 1;
for (let i = 1; i < 5; i++) {
const y = padding + (graphHeight / 5) * i;
ctx.beginPath();
ctx.moveTo(padding, y);
ctx.lineTo(this.width - padding, y);
ctx.stroke();
}
if (this.data.length < 2) return;
// 영역 채우기
ctx.fillStyle = this.colors.fill;
ctx.beginPath();
ctx.moveTo(padding, this.height - padding);
for (let i = 0; i < this.data.length; i++) {
const x = padding + (graphWidth / (MAX_DATA_POINTS - 1)) * i;
const y = this.height - padding - ((this.data[i] - minValue) / range) * graphHeight;
ctx.lineTo(x, y);
}
ctx.lineTo(padding + (graphWidth / (MAX_DATA_POINTS - 1)) * (this.data.length - 1), this.height - padding);
ctx.closePath();
ctx.fill();
// 선
ctx.strokeStyle = this.colors.line;
ctx.lineWidth = 2;
ctx.beginPath();
for (let i = 0; i < this.data.length; i++) {
const x = padding + (graphWidth / (MAX_DATA_POINTS - 1)) * i;
const y = this.height - padding - ((this.data[i] - minValue) / range) * graphHeight;
if (i === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
}
ctx.stroke();
// 포인트
ctx.fillStyle = this.colors.line;
for (let i = 0; i < this.data.length; i++) {
const x = padding + (graphWidth / (MAX_DATA_POINTS - 1)) * i;
const y = this.height - padding - ((this.data[i] - minValue) / range) * graphHeight;
ctx.beginPath();
ctx.arc(x, y, 2, 0, Math.PI * 2);
ctx.fill();
}
}
}
function initWebSocket() {
const hostname = window.location.hostname;
ws = new WebSocket('ws://' + hostname + ':81');
ws.onopen = function() {
console.log('WebSocket connected');
updateStatus('Connected', false);
};
ws.onclose = function() {
console.log('WebSocket disconnected');
updateStatus('Disconnected', true);
setTimeout(initWebSocket, 3000);
};
ws.onmessage = function(event) {
try {
const data = JSON.parse(event.data);
if (data.type === 'canBatch' && graphing) {
processCANData(data.messages);
}
} catch(e) {
console.error('Error:', e);
}
};
}
function updateStatus(text, isError) {
const status = document.getElementById('status');
status.textContent = text;
status.className = 'status' + (isError ? ' disconnected' : '');
}
function loadData() {
try {
const signals = localStorage.getItem('selected_signals');
const dbc = localStorage.getItem('dbc_content');
if (!signals || !dbc) {
alert('No signals selected. Please select signals first.');
window.close();
return false;
}
selectedSignals = JSON.parse(signals);
// DBC 파싱 (간단 버전 - 필요한 정보만)
dbcData = {messages: {}};
selectedSignals.forEach(sig => {
if (!dbcData.messages[sig.messageId]) {
dbcData.messages[sig.messageId] = {
id: sig.messageId,
signals: []
};
}
dbcData.messages[sig.messageId].signals.push(sig);
});
return true;
} catch(e) {
console.error('Failed to load data:', e);
alert('Failed to load signal data');
window.close();
return false;
}
}
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 =
'<div class="graph-header">' +
'<div class="graph-title">' + signal.name + ' (0x' + signal.messageId.toString(16).toUpperCase() + ')' +
(signal.unit ? ' [' + signal.unit + ']' : '') + '</div>' +
'<div class="graph-value" id="value-' + index + '">-</div>' +
'</div>';
container.appendChild(canvas);
graphsDiv.appendChild(container);
charts[index] = new SimpleChart(canvas, signal, index);
});
}
function startGraphing() {
if (!graphing) {
graphing = true;
startTime = Date.now();
updateStatus('Graphing...', false);
}
}
function stopGraphing() {
graphing = false;
updateStatus('Stopped', false);
}
function processCANData(messages) {
const elapsedTime = ((Date.now() - startTime) / 1000).toFixed(1);
messages.forEach(canMsg => {
const idStr = canMsg.id.replace(/\s/g, '').toUpperCase();
const msgId = parseInt(idStr, 16);
selectedSignals.forEach((signal, index) => {
if (signal.messageId === msgId && charts[index]) {
try {
const value = decodeSignal(signal, canMsg.data);
charts[index].addData(value, elapsedTime);
const valueDiv = document.getElementById('value-' + index);
if (valueDiv) {
valueDiv.textContent = value.toFixed(2) + (signal.unit ? ' ' + signal.unit : '');
}
} catch(e) {
console.error('Error decoding signal:', e);
}
}
});
});
}
function decodeSignal(signal, hexData) {
const bytes = [];
if (typeof hexData === 'string') {
const cleanHex = hexData.replace(/\s/g, '').toUpperCase();
for (let i = 0; i < cleanHex.length && i < 16; i += 2) {
bytes.push(parseInt(cleanHex.substring(i, i + 2), 16));
}
}
while (bytes.length < 8) bytes.push(0);
let rawValue = 0;
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 = 7 - (bitPos % 8);
if (byteIdx < bytes.length && byteIdx >= 0) {
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;
}
// 초기화
if (loadData()) {
createGraphs();
initWebSocket();
startGraphing();
}
</script>
</body>
</html>
)rawliteral";
#endif

17
index.h
View File

@@ -274,6 +274,23 @@ const char index_html[] PROGMEM = R"rawliteral(
let canMessages = {};
let messageOrder = [];
// CAN 속도 설정 저장 및 복원
function saveCanSpeed() {
const speed = document.getElementById('can-speed').value;
try {
localStorage.setItem('canSpeed', speed);
} catch(e) {}
}
function loadCanSpeed() {
try {
const savedSpeed = localStorage.getItem('canSpeed');
if (savedSpeed !== null) {
document.getElementById('can-speed').value = savedSpeed;
}
} catch(e) {}
}
function initWebSocket() {
ws = new WebSocket('ws://' + window.location.hostname + ':81');