diff --git a/ESP32_CAN_Logger.ino b/ESP32_CAN_Logger.ino
index db33485..6b5b946 100644
--- a/ESP32_CAN_Logger.ino
+++ b/ESP32_CAN_Logger.ino
@@ -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;
diff --git a/graph.h b/graph.h
index 3decd7c..d14ae6c 100644
--- a/graph.h
+++ b/graph.h
@@ -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; }
}
@@ -195,7 +203,12 @@ const char graph_html[] PROGMEM = R"rawliteral(
-
Select Signals (Max 6)
+
Select Signals (Max 20)
+
+
+ Selected: 0 / 20
+
+
@@ -203,6 +216,12 @@ const char graph_html[] PROGMEM = R"rawliteral(
+
+ Sort by:
+
+
+
+
@@ -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 =
- '
' + signal.name + '
' +
- '
' +
- 'ID: 0x' + signal.messageId.toString(16).toUpperCase() + ' | ' +
- signal.bitLength + 'bit' +
- (signal.unit ? ' | ' + signal.unit : '') +
- '
';
- 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 =
+ '
' + signal.name + '
' +
+ '
' +
+ 'ID: 0x' + signal.messageId.toString(16).toUpperCase() + ' | ' +
+ signal.bitLength + 'bit' +
+ (signal.unit ? ' | ' + signal.unit : '') +
+ '
';
+ 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();
diff --git a/graph_viewer.h b/graph_viewer.h
index adfaa4b..92daf2c 100644
--- a/graph_viewer.h
+++ b/graph_viewer.h
@@ -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(
@@ -141,6 +143,12 @@ const char graph_viewer_html[] PROGMEM = R"rawliteral(
+
+ Sort by:
+
+
+
+
Connecting...
@@ -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();