그래프 수 20개 늘림, 그래프 나열순서 조정

This commit is contained in:
2025-10-13 21:07:23 +00:00
parent bc0a2a448f
commit 268ae6645f
3 changed files with 204 additions and 345 deletions

View File

@@ -131,6 +131,9 @@ uint32_t msgPerSecond = 0;
uint32_t lastMsgCountTime = 0;
uint32_t lastMsgCount = 0;
// 그래프 최대 개수
#define MAX_GRAPH_SIGNALS 20
// CAN 송신용
TxMessage txMessages[MAX_TX_MESSAGES];
uint32_t totalTxCount = 0;

453
graph.h
View File

@@ -74,6 +74,36 @@ const char graph_html[] PROGMEM = R"rawliteral(
border-radius: 10px;
margin-bottom: 15px;
}
.sort-controls {
display: flex;
gap: 10px;
margin-bottom: 15px;
align-items: center;
flex-wrap: wrap;
}
.sort-label {
font-weight: 600;
color: #333;
font-size: 0.9em;
}
.sort-btn {
padding: 8px 15px;
border: 2px solid #43cea2;
background: white;
border-radius: 5px;
font-size: 0.85em;
cursor: pointer;
transition: all 0.3s;
font-weight: 600;
}
.sort-btn:hover {
background: #f0f9ff;
transform: translateY(-2px);
}
.sort-btn.active {
background: #43cea2;
color: white;
}
.signal-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
@@ -93,40 +123,6 @@ const char graph_html[] PROGMEM = R"rawliteral(
.signal-name { font-weight: 600; color: #333; margin-bottom: 5px; font-size: 0.9em; }
.signal-info { font-size: 0.8em; color: #666; }
.graph-container {
background: white;
padding: 15px;
border-radius: 10px;
margin-bottom: 15px;
border: 2px solid #e0e0e0;
}
.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: #333;
}
.graph-value {
font-size: 1.2em;
font-weight: 700;
color: #185a9d;
font-family: 'Courier New', monospace;
}
canvas {
width: 100%;
height: 250px;
border: 1px solid #ddd;
border-radius: 5px;
background: #fafafa;
}
.controls {
display: flex;
gap: 8px;
@@ -159,12 +155,24 @@ const char graph_html[] PROGMEM = R"rawliteral(
.status.success { background: #d4edda; color: #155724; }
.status.error { background: #f8d7da; color: #721c24; }
.selection-info {
background: #e3f2fd;
padding: 12px 15px;
border-radius: 8px;
margin-bottom: 15px;
border-left: 4px solid #185a9d;
font-size: 0.9em;
color: #333;
}
.selection-info strong {
color: #185a9d;
}
@media (max-width: 768px) {
body { padding: 5px; }
.header h1 { font-size: 1.5em; }
.content { padding: 10px; }
.signal-grid { grid-template-columns: 1fr; gap: 8px; }
canvas { height: 200px; }
h2 { font-size: 1.1em; }
}
</style>
@@ -195,7 +203,12 @@ const char graph_html[] PROGMEM = R"rawliteral(
</div>
<div id="signal-section" style="display:none;">
<h2>Select Signals (Max 6)</h2>
<h2>Select Signals (Max 20)</h2>
<div class="selection-info">
<strong>Selected: <span id="selected-count">0</span> / 20</strong>
</div>
<div class="controls">
<button class="btn btn-success" onclick="startGraphing()">Start</button>
<button class="btn btn-danger" onclick="stopGraphing()">Stop</button>
@@ -203,6 +216,12 @@ const char graph_html[] PROGMEM = R"rawliteral(
</div>
<div class="signal-selector">
<div class="sort-controls">
<span class="sort-label">Sort by:</span>
<button class="sort-btn active" id="sort-selection" onclick="setSortMode('selection')">Selection Order</button>
<button class="sort-btn" id="sort-name-asc" onclick="setSortMode('name-asc')">Name (AZ)</button>
<button class="sort-btn" id="sort-name-desc" onclick="setSortMode('name-desc')">Name (ZA)</button>
</div>
<div id="signal-list" class="signal-grid"></div>
</div>
@@ -220,171 +239,9 @@ const char graph_html[] PROGMEM = R"rawliteral(
let ws;
let dbcData = {};
let selectedSignals = [];
let charts = {};
let graphing = false;
let startTime = 0;
const MAX_SIGNALS = 6;
const MAX_DATA_POINTS = 60;
const COLORS = [
{line: '#FF6384', fill: 'rgba(255, 99, 132, 0.1)'},
{line: '#36A2EB', fill: 'rgba(54, 162, 235, 0.1)'},
{line: '#FFCE56', fill: 'rgba(255, 206, 86, 0.1)'},
{line: '#4BC0C0', fill: 'rgba(75, 192, 192, 0.1)'},
{line: '#9966FF', fill: 'rgba(153, 102, 255, 0.1)'},
{line: '#FF9F40', fill: 'rgba(255, 159, 64, 0.1)'}
];
// 커스텀 차트 클래스
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;
// Canvas 크기 설정
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 = '#ddd';
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 = '#666';
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 = '#666';
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);
}
}
// X축 타이틀
ctx.fillText('Time (sec)', this.width / 2, this.height - 5);
// 그리드 라인
ctx.strokeStyle = '#f0f0f0';
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;
if (i === 0) {
ctx.lineTo(x, y);
} else {
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();
}
}
}
let allSignals = [];
let sortMode = 'selection';
const MAX_SIGNALS = 20;
function initWebSocket() {
ws = new WebSocket('ws://' + window.location.hostname + ':81');
@@ -403,10 +260,6 @@ const char graph_html[] PROGMEM = R"rawliteral(
ws.onmessage = function(event) {
try {
const data = JSON.parse(event.data);
if (data.type === 'canBatch' && graphing) {
processCANData(data.messages);
}
} catch(e) {
console.error('Error parsing WebSocket data:', e);
}
@@ -423,13 +276,11 @@ const char graph_html[] PROGMEM = R"rawliteral(
parseDBCContent(content);
document.getElementById('dbc-status').textContent = file.name;
// DBC 파일 저장
saveDBCToLocalStorage(content, file.name);
};
reader.readAsText(file);
}
// DBC 파일을 localStorage에 저장
function saveDBCToLocalStorage(content, filename) {
try {
localStorage.setItem('dbc_content', content);
@@ -440,7 +291,6 @@ const char graph_html[] PROGMEM = R"rawliteral(
}
}
// localStorage에서 DBC 파일 복원
function loadDBCFromLocalStorage() {
try {
const content = localStorage.getItem('dbc_content');
@@ -461,6 +311,7 @@ const char graph_html[] PROGMEM = R"rawliteral(
function parseDBCContent(content) {
dbcData = {messages: {}};
allSignals = [];
const lines = content.split('\n');
let currentMessage = null;
@@ -492,6 +343,7 @@ const char graph_html[] PROGMEM = R"rawliteral(
messageName: currentMessage.name
};
currentMessage.signals.push(signal);
allSignals.push(signal);
}
}
}
@@ -500,31 +352,66 @@ const char graph_html[] PROGMEM = R"rawliteral(
showStatus('DBC loaded: ' + Object.keys(dbcData.messages).length + ' messages', 'success');
}
function setSortMode(mode) {
sortMode = mode;
document.querySelectorAll('.sort-btn').forEach(btn => btn.classList.remove('active'));
document.getElementById('sort-' + mode).classList.add('active');
displaySignals();
console.log('Sort mode changed to:', mode);
}
function sortSignals(signals) {
if (sortMode === 'name-asc') {
return signals.sort((a, b) => a.name.localeCompare(b.name));
} else if (sortMode === 'name-desc') {
return signals.sort((a, b) => b.name.localeCompare(a.name));
} else {
const selected = signals.filter(s => isSignalSelected(s));
const notSelected = signals.filter(s => !isSignalSelected(s));
return [...selected, ...notSelected];
}
}
function isSignalSelected(signal) {
return selectedSignals.some(s =>
s.messageId === signal.messageId && s.name === signal.name);
}
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 + 'bit' +
(signal.unit ? ' | ' + signal.unit : '') +
'</div>';
signalList.appendChild(item);
});
}
const sortedSignals = sortSignals([...allSignals]);
sortedSignals.forEach(signal => {
const item = document.createElement('div');
item.className = 'signal-item';
if (isSignalSelected(signal)) {
item.classList.add('selected');
}
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 + 'bit' +
(signal.unit ? ' | ' + signal.unit : '') +
'</div>';
signalList.appendChild(item);
});
document.getElementById('signal-section').style.display = 'block';
// 저장된 선택 복원
setTimeout(() => loadSelectedSignals(), 100);
updateSelectionCount();
}
function updateSelectionCount() {
document.getElementById('selected-count').textContent = selectedSignals.length;
}
function toggleSignal(signal, element) {
@@ -543,8 +430,12 @@ const char graph_html[] PROGMEM = R"rawliteral(
element.classList.add('selected');
}
// 선택한 신호 저장
updateSelectionCount();
saveSelectedSignals();
if (sortMode === 'selection') {
displaySignals();
}
}
function clearSelection() {
@@ -553,30 +444,39 @@ const char graph_html[] PROGMEM = R"rawliteral(
item.classList.remove('selected');
});
// 저장된 선택 삭제
updateSelectionCount();
saveSelectedSignals();
if (sortMode === 'selection') {
displaySignals();
}
}
// 선택한 신호를 localStorage에 저장
function saveSelectedSignals() {
try {
localStorage.setItem('selected_signals', JSON.stringify(selectedSignals));
console.log('Saved', selectedSignals.length, 'signals');
localStorage.setItem('sort_mode', sortMode);
console.log('Saved', selectedSignals.length, 'signals with sort mode:', sortMode);
} catch(e) {
console.error('Failed to save selected signals:', e);
}
}
// localStorage에서 선택한 신호 복원
function loadSelectedSignals() {
try {
const saved = localStorage.getItem('selected_signals');
const savedSortMode = localStorage.getItem('sort_mode');
if (savedSortMode) {
sortMode = savedSortMode;
document.querySelectorAll('.sort-btn').forEach(btn => btn.classList.remove('active'));
document.getElementById('sort-' + sortMode).classList.add('active');
}
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];
@@ -591,22 +491,7 @@ const char graph_html[] PROGMEM = R"rawliteral(
}
});
// 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');
}
}
});
displaySignals();
if (selectedSignals.length > 0) {
showStatus('Restored ' + selectedSignals.length + ' selected signals', 'success');
@@ -623,12 +508,10 @@ const char graph_html[] PROGMEM = R"rawliteral(
return;
}
// 선택한 신호 저장 (새 창에서 사용)
saveSelectedSignals();
// 새 창 열기
const width = 1200;
const height = 800;
const width = 1400;
const height = 900;
const left = (screen.width - width) / 2;
const top = (screen.height - height) / 2;
@@ -645,64 +528,6 @@ const char graph_html[] PROGMEM = R"rawliteral(
showStatus('Use the stop button in the graph viewer window', 'error');
}
function createGraphs() {
// 이 함수는 새 창에서 사용됨
}
function processCANData(messages) {
// 이 함수는 새 창에서 사용됨
}
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);
}
const physicalValue = rawValue * signal.factor + signal.offset;
return physicalValue;
}
function showStatus(message, type) {
const status = document.getElementById('status');
status.textContent = message;
@@ -741,16 +566,16 @@ const char graph_html[] PROGMEM = R"rawliteral(
parseDBCContent(ev.target.result);
document.getElementById('dbc-status').textContent = file.name;
// DBC 파일 저장
saveDBCToLocalStorage(ev.target.result, file.name);
};
reader.readAsText(file);
}
});
// 페이지 로드 시 localStorage에서 DBC 복원
window.addEventListener('load', function() {
loadDBCFromLocalStorage();
if (loadDBCFromLocalStorage()) {
setTimeout(() => loadSelectedSignals(), 100);
}
});
initWebSocket();

View File

@@ -23,6 +23,7 @@ const char graph_viewer_html[] PROGMEM = R"rawliteral(
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
}
.header h1 { font-size: 1.5em; }
.header p { font-size: 0.9em; opacity: 0.9; margin-top: 5px; }
.controls {
background: #2a2a2a;
padding: 10px 15px;
@@ -63,7 +64,7 @@ const char graph_viewer_html[] PROGMEM = R"rawliteral(
.graphs {
padding: 15px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
grid-template-columns: repeat(auto-fit, minmax(450px, 1fr));
gap: 15px;
}
.graph-container {
@@ -123,6 +124,7 @@ const char graph_viewer_html[] PROGMEM = R"rawliteral(
<body>
<div class="header">
<h1>Real-time CAN Signal Graphs (Scatter Mode)</h1>
<p>Viewing <span id="graph-count">0</span> signals</p>
</div>
<div class="controls">
@@ -141,6 +143,12 @@ const char graph_viewer_html[] PROGMEM = R"rawliteral(
<button class="btn btn-warning active" id="btn-range-10s" onclick="setRangeMode('10s')">10s Window</button>
<button class="btn btn-warning" id="btn-range-all" onclick="setRangeMode('all')">All Time</button>
</div>
<div class="control-group">
<span class="control-label">Sort by:</span>
<button class="btn btn-info active" id="btn-sort-selection" onclick="setSortMode('selection')">Selection Order</button>
<button class="btn btn-info" id="btn-sort-name-asc" onclick="setSortMode('name-asc')">Name (AZ)</button>
<button class="btn btn-info" id="btn-sort-name-desc" onclick="setSortMode('name-desc')">Name (ZA)</button>
</div>
</div>
<div class="status" id="status">Connecting...</div>
@@ -155,16 +163,31 @@ const char graph_viewer_html[] PROGMEM = R"rawliteral(
let selectedSignals = [];
let dbcData = {};
let lastTimestamps = {};
let sortMode = 'selection';
const MAX_DATA_POINTS = 60;
let scaleMode = 'index'; // 'index' or 'time'
let rangeMode = '10s'; // '10s' or 'all'
let scaleMode = 'index';
let rangeMode = '10s';
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)'}
{line: '#FF9F40', fill: 'rgba(255, 159, 64, 0.2)'},
{line: '#FF6384', fill: 'rgba(255, 99, 132, 0.2)'},
{line: '#4BC0C0', fill: 'rgba(75, 192, 192, 0.2)'},
{line: '#FFCE56', fill: 'rgba(255, 206, 86, 0.2)'},
{line: '#9966FF', fill: 'rgba(153, 102, 255, 0.2)'},
{line: '#36A2EB', fill: 'rgba(54, 162, 235, 0.2)'},
{line: '#FF9F40', fill: 'rgba(255, 159, 64, 0.2)'},
{line: '#E74C3C', fill: 'rgba(231, 76, 60, 0.2)'},
{line: '#3498DB', fill: 'rgba(52, 152, 219, 0.2)'},
{line: '#2ECC71', fill: 'rgba(46, 204, 113, 0.2)'},
{line: '#F39C12', fill: 'rgba(243, 156, 18, 0.2)'},
{line: '#9B59B6', fill: 'rgba(155, 89, 182, 0.2)'},
{line: '#1ABC9C', fill: 'rgba(26, 188, 156, 0.2)'},
{line: '#E67E22', fill: 'rgba(230, 126, 34, 0.2)'},
{line: '#95A5A6', fill: 'rgba(149, 165, 166, 0.2)'}
];
class SimpleChart {
@@ -173,7 +196,7 @@ const char graph_viewer_html[] PROGMEM = R"rawliteral(
this.ctx = canvas.getContext('2d');
this.signal = signal;
this.data = [];
this.times = []; // 실제 시간(초) 저장
this.times = [];
this.labels = [];
this.colors = COLORS[colorIndex % COLORS.length];
this.currentValue = 0;
@@ -198,7 +221,6 @@ const char graph_viewer_html[] PROGMEM = R"rawliteral(
this.labels.push(time);
this.currentValue = value;
// 인덱스 모드에서만 개수 제한
if (scaleMode === 'index' && this.data.length > MAX_DATA_POINTS) {
this.data.shift();
this.times.shift();
@@ -218,13 +240,11 @@ const char graph_viewer_html[] PROGMEM = R"rawliteral(
ctx.clearRect(0, 0, this.width, this.height);
// 표시할 데이터 필터링
let displayData = [];
let displayTimes = [];
let displayLabels = [];
if (rangeMode === '10s') {
// 최근 10초 데이터만
const currentTime = this.times[this.times.length - 1];
for (let i = 0; i < this.times.length; i++) {
if (currentTime - this.times[i] <= 10) {
@@ -234,7 +254,6 @@ const char graph_viewer_html[] PROGMEM = R"rawliteral(
}
}
} else {
// 전체 데이터
displayData = [...this.data];
displayTimes = [...this.times];
displayLabels = [...this.labels];
@@ -246,7 +265,6 @@ const char graph_viewer_html[] PROGMEM = R"rawliteral(
const maxValue = Math.max(...displayData);
const range = maxValue - minValue || 1;
// X축 범위 계산
let minTime, maxTime, timeRange;
if (scaleMode === 'time') {
minTime = displayTimes[0];
@@ -254,7 +272,6 @@ const char graph_viewer_html[] PROGMEM = R"rawliteral(
timeRange = maxTime - minTime || 1;
}
// 축
ctx.strokeStyle = '#444';
ctx.lineWidth = 1;
ctx.beginPath();
@@ -263,14 +280,12 @@ const char graph_viewer_html[] PROGMEM = R"rawliteral(
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';
@@ -289,7 +304,6 @@ const char graph_viewer_html[] PROGMEM = R"rawliteral(
}
ctx.fillText('Time (sec)', this.width / 2, this.height - 5);
// 그리드
ctx.strokeStyle = '#333';
ctx.lineWidth = 1;
for (let i = 1; i < 5; i++) {
@@ -302,28 +316,23 @@ const char graph_viewer_html[] PROGMEM = R"rawliteral(
if (displayData.length < 1) return;
// 점 그리기
ctx.fillStyle = this.colors.line;
for (let i = 0; i < displayData.length; i++) {
let x;
if (scaleMode === 'time') {
// 시간 기반: 실제 시간 간격을 X축에 반영
const timePos = (displayTimes[i] - minTime) / timeRange;
x = padding + graphWidth * timePos;
} else {
// 인덱스 기반: 균등 간격
x = padding + (graphWidth / (MAX_DATA_POINTS - 1)) * i;
}
const y = this.height - padding - ((displayData[i] - minValue) / range) * graphHeight;
// 점 그리기
ctx.beginPath();
ctx.arc(x, y, 5, 0, Math.PI * 2);
ctx.fill();
// 점 테두리
ctx.strokeStyle = '#fff';
ctx.lineWidth = 1;
ctx.stroke();
@@ -369,6 +378,7 @@ const char graph_viewer_html[] PROGMEM = R"rawliteral(
try {
const signals = localStorage.getItem('selected_signals');
const dbc = localStorage.getItem('dbc_content');
const savedSortMode = localStorage.getItem('sort_mode');
if (!signals || !dbc) {
alert('No signals selected. Please select signals first.');
@@ -376,9 +386,12 @@ const char graph_viewer_html[] PROGMEM = R"rawliteral(
return false;
}
if (savedSortMode) {
sortMode = savedSortMode;
}
selectedSignals = JSON.parse(signals);
// DBC 파싱 (간단 버전 - 필요한 정보만)
dbcData = {messages: {}};
selectedSignals.forEach(sig => {
if (!dbcData.messages[sig.messageId]) {
@@ -399,12 +412,35 @@ const char graph_viewer_html[] PROGMEM = R"rawliteral(
}
}
function sortSignalsForDisplay() {
if (sortMode === 'name-asc') {
return selectedSignals.sort((a, b) => a.name.localeCompare(b.name));
} else if (sortMode === 'name-desc') {
return selectedSignals.sort((a, b) => b.name.localeCompare(a.name));
} else {
return selectedSignals;
}
}
function setSortMode(mode) {
sortMode = mode;
document.querySelectorAll('[id^="btn-sort-"]').forEach(btn => btn.classList.remove('active'));
document.getElementById('btn-sort-' + mode).classList.add('active');
createGraphs();
console.log('Sort mode changed to:', mode);
}
function createGraphs() {
const graphsDiv = document.getElementById('graphs');
graphsDiv.innerHTML = '';
charts = {};
selectedSignals.forEach((signal, index) => {
const sortedSignals = sortSignalsForDisplay();
sortedSignals.forEach((signal, index) => {
const container = document.createElement('div');
container.className = 'graph-container';
@@ -423,13 +459,15 @@ const char graph_viewer_html[] PROGMEM = R"rawliteral(
charts[index] = new SimpleChart(canvas, signal, index);
});
document.getElementById('graph-count').textContent = sortedSignals.length;
}
function startGraphing() {
if (!graphing) {
graphing = true;
startTime = Date.now();
lastTimestamps = {}; // 타임스탬프 초기화
lastTimestamps = {};
updateStatus('Graphing...', false);
}
}
@@ -442,11 +480,9 @@ const char graph_viewer_html[] PROGMEM = R"rawliteral(
function setScaleMode(mode) {
scaleMode = mode;
// 버튼 활성화 상태 업데이트
document.getElementById('btn-index-mode').classList.toggle('active', mode === 'index');
document.getElementById('btn-time-mode').classList.toggle('active', mode === 'time');
// 모든 차트 다시 그리기
Object.values(charts).forEach(chart => chart.draw());
console.log('Scale mode changed to:', mode);
@@ -455,11 +491,9 @@ const char graph_viewer_html[] PROGMEM = R"rawliteral(
function setRangeMode(mode) {
rangeMode = mode;
// 버튼 활성화 상태 업데이트
document.getElementById('btn-range-10s').classList.toggle('active', mode === '10s');
document.getElementById('btn-range-all').classList.toggle('active', mode === 'all');
// 모든 차트 다시 그리기
Object.values(charts).forEach(chart => chart.draw());
console.log('Range mode changed to:', mode);
@@ -467,24 +501,22 @@ const char graph_viewer_html[] PROGMEM = R"rawliteral(
function processCANData(messages) {
const elapsedTime = ((Date.now() - startTime) / 1000).toFixed(1);
const sortedSignals = sortSignalsForDisplay();
messages.forEach(canMsg => {
const idStr = canMsg.id.replace(/\s/g, '').toUpperCase();
const msgId = parseInt(idStr, 16);
const timestamp = canMsg.timestamp;
selectedSignals.forEach((signal, index) => {
sortedSignals.forEach((signal, index) => {
if (signal.messageId === msgId && charts[index]) {
try {
// 신호별 고유 키 생성
const signalKey = msgId + '_' + signal.name;
// 이전 타임스탬프와 비교 - 새로운 메시지일 때만 점 추가
if (!lastTimestamps[signalKey] || lastTimestamps[signalKey] !== timestamp) {
const value = decodeSignal(signal, canMsg.data);
charts[index].addData(value, elapsedTime);
// 타임스탬프 업데이트
lastTimestamps[signalKey] = timestamp;
const valueDiv = document.getElementById('value-' + index);
@@ -545,7 +577,6 @@ const char graph_viewer_html[] PROGMEM = R"rawliteral(
return rawValue * signal.factor + signal.offset;
}
// 초기화
if (loadData()) {
createGraphs();
initWebSocket();