00006 graph 창 변수 dbc 선택 유지
graph 페이지에서 변수를 선택하고 start 누를 시 그래프 표출을 또다른 창 이동해서 표현하는 것이 나을 것 같아 그리고 선택한 변수들은 다른 페이지 이동해도 선택이 리셋되지 않게 선택한변수들을 유지하게 해줘
This commit is contained in:
@@ -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
148
graph.h
@@ -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
468
graph_viewer.h
Normal 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
17
index.h
@@ -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');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user