Upload DBC File
@@ -367,7 +315,7 @@ const char graph_html[] PROGMEM = R"rawliteral(
+
CAN Signal Graph Viewer
ℹ️ Info: Click "Start" to open a new window with real-time graphs. Your selected signals will be saved automatically. diff --git a/graph_viewer.h b/graph_viewer.h index 393de05..2e98843 100644 --- a/graph_viewer.h +++ b/graph_viewer.h @@ -9,219 +9,241 @@ const char graph_viewer_html[] PROGMEM = R"rawliteral(
-
Real-time CAN Signal Graphs (Scatter Mode)
-Viewing 0 signals
+
+
+
+ 📈 CAN Signal Graph
+Viewing 0 signals
+Connecting...
@@ -236,42 +258,46 @@ const char graph_viewer_html[] PROGMEM = R"rawliteral(
-
-
-
-
+
+
+
- X-Axis Scale:
+ Scale:
-
+
+
- X-Axis Range:
-
-
+ Range:
+
+
+
+
- Sort by:
-
-
-
+ Plot:
+
+
+
-
-
+ Sort:
+
+
+
+
+
+
+
+
Connecting...
-
-
- Data Points: 0
- Recording Time: 0s
- Messages Received: 0
+ Byun CAN Logger
-
+ Points:0
+ Time:0s
+ Msgs:0
@@ -285,12 +311,13 @@ const char graph_viewer_html[] PROGMEM = R"rawliteral(
let dbcData = {};
let lastTimestamps = {};
let sortMode = 'selection';
- const MAX_DATA_POINTS = 60;
+ const MAX_DATA_POINTS = 300;
let scaleMode = 'index';
let rangeMode = '10s';
let totalMsgReceived = 0;
- let lastCanCounts = {}; // ★ 각 CAN ID별 마지막 count 저장
- let lastSignalTimes = {}; // ⭐ 각 신호별 마지막 시간 저장 (time-base 모드용)
+ let lastCanCounts = {};
+ let lastSignalTimes = {};
+ let plotMode = 'line'; // ★ 'line' | 'scatter'
const COLORS = [
{line: '#FF6384', fill: 'rgba(255, 99, 132, 0.2)'},
@@ -334,10 +361,13 @@ const char graph_viewer_html[] PROGMEM = R"rawliteral(
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;
+ if (rect.width === 0) return;
+ const dpr = window.devicePixelRatio || 1;
+ this.canvas.width = rect.width * dpr;
+ this.canvas.height = rect.height * dpr;
+ // ★ 버그수정: scale() 누적 → setTransform() 으로 항상 초기화
+ this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
+ this.width = rect.width;
this.height = rect.height;
this.draw();
}
@@ -380,127 +410,170 @@ const char graph_viewer_html[] PROGMEM = R"rawliteral(
}
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.fillStyle = '#1a1a1a';
- ctx.fillRect(0, 0, this.width, this.height);
-
- let displayData = [];
- let displayTimes = [];
- let displayLabels = [];
-
- if (rangeMode === '10s') {
- const currentTime = parseFloat(this.times[this.times.length - 1]);
+ const W = this.width, H = this.height;
+ if (!W || !H) return;
+
+ const PAD_L = 52, PAD_R = 8, PAD_T = 12, PAD_B = 28;
+ const gW = W - PAD_L - PAD_R;
+ const gH = H - PAD_T - PAD_B;
+
+ // 배경
+ ctx.fillStyle = '#111827';
+ ctx.fillRect(0, 0, W, H);
+
+ // ★ 빈 데이터 상태 표시
+ if (this.data.length === 0) {
+ ctx.fillStyle = '#374151';
+ ctx.font = '12px monospace';
+ ctx.textAlign = 'center';
+ ctx.fillText('Waiting for data...', W / 2, H / 2);
+ return;
+ }
+
+ // ── 표시할 데이터 슬라이스 계산 ──
+ let displayData = [], displayTimes = [], displayLabels = [];
+ const windowSec = rangeMode === '30s' ? 30 : 10;
+
+ if (rangeMode === 'all') {
+ displayData = [...this.data];
+ displayTimes = [...this.times];
+ displayLabels = [...this.labels];
+ } else if (scaleMode === 'time') {
+ // time 모드: 시간 기준으로 window 필터
+ const latest = this.times[this.times.length - 1];
+ const cutoff = latest - windowSec;
for (let i = 0; i < this.times.length; i++) {
- if (currentTime - parseFloat(this.times[i]) <= 10) {
+ if (this.times[i] >= cutoff) {
displayData.push(this.data[i]);
displayTimes.push(this.times[i]);
displayLabels.push(this.labels[i]);
}
}
} else {
- displayData = [...this.data];
- displayTimes = [...this.times];
- displayLabels = [...this.labels];
+ // ★ index 모드: 최근 N개만 표시
+ const N = rangeMode === '30s' ? 150 : 60;
+ const start = Math.max(0, this.data.length - N);
+ displayData = this.data.slice(start);
+ displayTimes = this.times.slice(start);
+ displayLabels = this.labels.slice(start);
}
-
+
if (displayData.length === 0) return;
-
+
const minValue = Math.min(...displayData);
const maxValue = Math.max(...displayData);
- const range = maxValue - minValue || 1;
-
- let minTime, maxTime, timeRange;
- if (scaleMode === 'time') {
- minTime = parseFloat(displayTimes[0]);
- maxTime = parseFloat(displayTimes[displayTimes.length - 1]);
- timeRange = maxTime - minTime || 1;
- }
-
- ctx.strokeStyle = '#888';
- ctx.lineWidth = 2;
- ctx.beginPath();
- ctx.moveTo(padding, padding);
- ctx.lineTo(padding, this.height - padding);
- ctx.lineTo(this.width - padding, this.height - padding);
- ctx.stroke();
-
- ctx.fillStyle = '#ccc';
- ctx.font = '11px Arial';
- ctx.textAlign = 'right';
-
- if (this.signal.valueTable) {
- const uniqueValues = [...new Set(displayData)].sort((a, b) => a - b);
- if (uniqueValues.length <= 5) {
- uniqueValues.forEach((val, idx) => {
- const y = this.height - padding - ((val - minValue) / range) * graphHeight;
- const text = this.getValueText(val);
- ctx.fillText(text, padding - 5, y + 5);
- });
+ const range = maxValue - minValue || 1;
+
+ // ── Y 그리드 ──
+ const GRID_Y = 4;
+ ctx.lineWidth = 0.5;
+ for (let i = 0; i <= GRID_Y; i++) {
+ const y = PAD_T + (gH / GRID_Y) * i;
+ ctx.strokeStyle = (i === GRID_Y) ? '#2d3748' : '#1a2035';
+ ctx.beginPath(); ctx.moveTo(PAD_L, y); ctx.lineTo(PAD_L + gW, y); ctx.stroke();
+
+ // Y 라벨
+ const v = maxValue - (range / GRID_Y) * i;
+ let label = '';
+ if (this.signal.valueTable) {
+ const rv = Math.round(v);
+ label = (this.signal.valueTable[rv] !== undefined)
+ ? String(this.signal.valueTable[rv]).substring(0, 7)
+ : v.toFixed(1);
} else {
- ctx.fillText(this.getValueText(maxValue), padding - 5, padding + 5);
- ctx.fillText(this.getValueText(minValue), padding - 5, this.height - padding);
+ label = Math.abs(v) < 100 ? v.toFixed(2) : v.toFixed(0);
}
- } else {
- ctx.fillText(maxValue.toFixed(2), padding - 5, padding + 5);
- ctx.fillText(minValue.toFixed(2), padding - 5, this.height - padding);
+ ctx.fillStyle = '#6b7280';
+ ctx.font = '9px monospace';
+ ctx.textAlign = 'right';
+ ctx.fillText(label, PAD_L - 3, y + 3);
}
-
- ctx.textAlign = 'center';
- ctx.fillStyle = '#ccc';
- ctx.font = '10px Arial';
- if (displayLabels.length > 0) {
- ctx.fillText(displayLabels[0] + 's', padding, this.height - padding + 15);
- if (displayLabels.length > 1) {
- const lastIdx = displayLabels.length - 1;
- let xPos;
- if (scaleMode === 'time') {
- xPos = this.width - padding;
- } else {
- xPos = padding + (graphWidth / (MAX_DATA_POINTS - 1)) * Math.min(lastIdx, MAX_DATA_POINTS - 1);
- }
- ctx.fillText(displayLabels[lastIdx] + 's', xPos, this.height - padding + 15);
- }
+
+ // ── X 그리드 ──
+ for (let i = 0; i <= 4; i++) {
+ const x = PAD_L + (gW / 4) * i;
+ ctx.strokeStyle = '#1a2035';
+ ctx.lineWidth = 0.5;
+ ctx.beginPath(); ctx.moveTo(x, PAD_T); ctx.lineTo(x, PAD_T + gH); ctx.stroke();
}
- ctx.fillText('Time (sec)', this.width / 2, this.height - 5);
-
- ctx.strokeStyle = '#444';
+
+ // ── 축선 ──
+ ctx.strokeStyle = '#2d3748';
ctx.lineWidth = 1;
- for (let i = 1; i < 5; i++) {
- const y = padding + (graphHeight / 5) * i;
+ ctx.beginPath();
+ ctx.moveTo(PAD_L, PAD_T);
+ ctx.lineTo(PAD_L, PAD_T + gH);
+ ctx.lineTo(PAD_L + gW, PAD_T + gH);
+ ctx.stroke();
+
+ // ── 좌표 변환 함수 ──
+ let minTime, timeRange;
+ if (scaleMode === 'time' && displayTimes.length > 1) {
+ minTime = displayTimes[0];
+ timeRange = displayTimes[displayTimes.length - 1] - minTime || 1;
+ }
+ const xOf = (i) => {
+ if (scaleMode === 'time' && displayTimes.length > 1) {
+ return PAD_L + ((displayTimes[i] - minTime) / timeRange) * gW;
+ }
+ // ★ 버그수정: MAX_DATA_POINTS 고정값 대신 실제 표시 데이터 개수 사용
+ return PAD_L + (displayData.length > 1 ? (i / (displayData.length - 1)) * gW : 0);
+ };
+ const yOf = (v) => PAD_T + gH - ((v - minValue) / range) * gH;
+
+ const lineColor = this.colors.line;
+
+ // ── Fill 영역 ──
+ ctx.beginPath();
+ ctx.moveTo(xOf(0), PAD_T + gH);
+ for (let i = 0; i < displayData.length; i++) ctx.lineTo(xOf(i), yOf(displayData[i]));
+ ctx.lineTo(xOf(displayData.length - 1), PAD_T + gH);
+ ctx.closePath();
+ // fill 색상: line 색에 알파 추가
+ ctx.fillStyle = this.colors.fill;
+ ctx.fill();
+
+ // ── Line 또는 Scatter ──
+ if (plotMode === 'line' && displayData.length > 1) {
+ // ★ 라인 그리기
+ ctx.strokeStyle = lineColor;
+ ctx.lineWidth = 1.8;
+ ctx.lineJoin = 'round';
ctx.beginPath();
- ctx.moveTo(padding, y);
- ctx.lineTo(this.width - padding, y);
+ ctx.moveTo(xOf(0), yOf(displayData[0]));
+ for (let i = 1; i < displayData.length; i++) {
+ ctx.lineTo(xOf(i), yOf(displayData[i]));
+ }
ctx.stroke();
}
-
- if (displayData.length < 1) return;
-
- ctx.fillStyle = this.colors.line;
+
+ // 점 그리기
for (let i = 0; i < displayData.length; i++) {
- let x;
-
- if (scaleMode === 'time') {
- const timePos = (parseFloat(displayTimes[i]) - minTime) / timeRange;
- x = padding + graphWidth * timePos;
- } else {
- x = padding + (graphWidth / (MAX_DATA_POINTS - 1)) * i;
+ const isLast = (i === displayData.length - 1);
+ if (plotMode === 'scatter' || isLast) {
+ const r = isLast ? 4.5 : 2.5;
+ ctx.fillStyle = isLast ? '#ffffff' : lineColor;
+ ctx.strokeStyle = lineColor;
+ ctx.lineWidth = isLast ? 1.5 : 0;
+ ctx.beginPath();
+ ctx.arc(xOf(i), yOf(displayData[i]), r, 0, Math.PI * 2);
+ ctx.fill();
+ if (isLast) ctx.stroke();
+ }
+ }
+
+ // ── X 라벨 ──
+ ctx.fillStyle = '#6b7280';
+ ctx.font = '9px monospace';
+ ctx.textAlign = 'center';
+ if (displayLabels.length > 0) {
+ ctx.fillText(displayLabels[0] + 's', PAD_L, PAD_T + gH + 16);
+ ctx.fillText(displayLabels[displayLabels.length-1] + 's', PAD_L + gW, PAD_T + gH + 16);
+ if (displayLabels.length > 4) {
+ const mid = Math.floor(displayLabels.length / 2);
+ ctx.fillText(displayLabels[mid] + 's', PAD_L + gW * 0.5, PAD_T + gH + 16);
}
-
- 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();
}
}
}
@@ -542,9 +615,10 @@ const char graph_viewer_html[] PROGMEM = R"rawliteral(
}
function updateStatus(text, isError) {
- const status = document.getElementById('status');
- status.textContent = text;
- status.className = 'status' + (isError ? ' disconnected' : '');
+ const el = document.getElementById('status-pill');
+ if (!el) return;
+ el.textContent = text;
+ el.className = 'status' + (isError ? ' disconnected' : '');
}
function loadData() {
@@ -605,13 +679,61 @@ const char graph_viewer_html[] PROGMEM = R"rawliteral(
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);
+
+ // ★ 버그수정: createGraphs() 호출 시 데이터 초기화 문제
+ // → 기존 차트 데이터를 보존한 채로 DOM 순서만 재배열
+ const graphsDiv = document.getElementById('graphs');
+ const oldCharts = Object.assign({}, charts);
+ const origSignals = [...selectedSignals];
+ const sorted = sortSignalsForDisplay();
+
+ graphsDiv.innerHTML = '';
+ charts = {};
+
+ sorted.forEach((signal, newIdx) => {
+ const origIdx = origSignals.findIndex(s =>
+ s.name === signal.name && s.messageId === signal.messageId);
+ const container = document.createElement('div');
+ container.className = 'graph-container';
+ const canvas = document.createElement('canvas');
+ canvas.id = 'chart-' + newIdx;
+ container.innerHTML =
+ '' +
+ '
';
+ container.appendChild(canvas);
+ graphsDiv.appendChild(container);
+
+ const chart = new SimpleChart(canvas, signal, newIdx);
+ // 기존 데이터 복사
+ if (origIdx !== -1 && oldCharts[origIdx]) {
+ chart.data = [...oldCharts[origIdx].data];
+ chart.times = [...oldCharts[origIdx].times];
+ chart.labels = [...oldCharts[origIdx].labels];
+ chart.rawValues = [...(oldCharts[origIdx].rawValues || [])];
+ chart.currentValue = oldCharts[origIdx].currentValue;
+ chart.currentText = oldCharts[origIdx].currentText;
+ }
+ charts[newIdx] = chart;
+ chart.draw();
+
+ // 현재 값 표시 복원
+ const vEl = document.getElementById('value-' + newIdx);
+ if (vEl && chart.currentValue !== undefined && chart.currentValue !== null) {
+ if (chart.currentText) {
+ vEl.textContent = chart.currentText;
+ } else {
+ vEl.textContent = chart.currentValue.toFixed(2) + (signal.unit ? ' ' + signal.unit : '');
+ }
+ }
+ });
+
+ console.log('Sort mode changed to:', mode, '(data preserved)');
}
function createGraphs() {
@@ -628,10 +750,11 @@ const char graph_viewer_html[] PROGMEM = R"rawliteral(
const canvas = document.createElement('canvas');
canvas.id = 'chart-' + index;
- container.innerHTML =
+ container.innerHTML =
'' + signal.name +
+ ' (0x' + signal.messageId.toString(16).toUpperCase() + ')' +
+ (signal.unit ? ' [' + signal.unit + ']' : '') + '
' +
+ '-
' +
+ '' +
- '
';
@@ -646,40 +769,44 @@ const char graph_viewer_html[] PROGMEM = R"rawliteral(
function startGraphing() {
if (!graphing) {
- graphing = true;
- startTime = Date.now();
+ graphing = true;
+ startTime = Date.now();
lastTimestamps = {};
- updateStatus('Graphing...', false);
- console.log('Started graphing at', new Date().toISOString());
+ document.getElementById('btn-start').classList.add('active');
+ document.getElementById('btn-stop').classList.remove('active');
+ updateStatus('Recording', false);
}
}
-
+
function stopGraphing() {
graphing = false;
- updateStatus('Stopped', false);
- console.log('Stopped graphing');
+ document.getElementById('btn-start').classList.remove('active');
+ document.getElementById('btn-stop').classList.add('active');
+ updateStatus('Paused', false);
}
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');
-
+ document.getElementById('btn-time-mode').classList.toggle('active', mode === 'time');
Object.values(charts).forEach(chart => chart.draw());
-
- console.log('Scale mode changed to:', mode);
}
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');
-
+ ['10s','30s','all'].forEach(m => {
+ const el = document.getElementById('btn-range-' + m);
+ if (el) el.classList.toggle('active', m === mode);
+ });
+ Object.values(charts).forEach(chart => chart.draw());
+ }
+
+ // ★ 신규: Plot 모드 전환 (line / scatter)
+ function setPlotMode(mode) {
+ plotMode = mode;
+ document.getElementById('btn-plot-line').classList.toggle('active', mode === 'line');
+ document.getElementById('btn-plot-scatter').classList.toggle('active', mode === 'scatter');
Object.values(charts).forEach(chart => chart.draw());
-
- console.log('Range mode changed to:', mode);
}
// ★★★ 새로운 함수: update 타입 메시지 처리
@@ -1010,9 +1137,10 @@ const char graph_viewer_html[] PROGMEM = R"rawliteral(
chart.draw();
});
- lastTimestamps = {};
- lastCanCounts = {}; // ★ count 기록도 초기화
- startTime = Date.now();
+ lastTimestamps = {};
+ lastCanCounts = {};
+ lastSignalTimes = {}; // ★ 버그수정: 시간 계산 리셋
+ startTime = Date.now();
totalMsgReceived = 0;
updateStatistics();
diff --git a/index.h b/index.h
index d3da573..64874ee 100644
--- a/index.h
+++ b/index.h
@@ -9,931 +9,412 @@ const char index_html[] PROGMEM = R"rawliteral(
' + signal.name + ' (0x' + signal.messageId.toString(16).toUpperCase() + ')' +
- (signal.unit ? ' [' + signal.unit + ']' : '') + '
' +
+ '' + signal.name +
+ ' (0x' + signal.messageId.toString(16).toUpperCase() + ')' +
+ (signal.unit ? ' [' + signal.unit + ']' : '') + '
' +
'-
' +
'
-
+
-
🚗 Byun CAN Logger v2.0
Real-time CAN Bus Monitor & Logger + Phone Time Sync + MCP2515 Mode Control
+
📊 Monitor
📤 Transmit
📈 Graph
@@ -943,7 +424,7 @@ const char index_html[] PROGMEM = R"rawliteral(
📟 Serial2
-
+
@@ -1222,7 +703,7 @@ const char index_html[] PROGMEM = R"rawliteral(
Log Files
-
+
diff --git a/serial2_terminal.h b/serial2_terminal.h
index 60f5f0c..8a42b3e 100644
--- a/serial2_terminal.h
+++ b/serial2_terminal.h
@@ -8,533 +8,279 @@ const char serial2_terminal_html[] PROGMEM = R"rawliteral(
Serial2 Terminal - CAN Logger
-
-
+
-
📟 Serial2 Terminal GPIO 6/7
+
📊 Monitor
📤 Transmit
📈 Graph
@@ -544,7 +290,7 @@ const char serial2_terminal_html[] PROGMEM = R"rawliteral(
📟 Serial2
-
+
diff --git a/serial_terminal.h b/serial_terminal.h
index adc0b00..ccf7726 100644
--- a/serial_terminal.h
+++ b/serial_terminal.h
@@ -8,533 +8,279 @@ const char serial_terminal_html[] PROGMEM = R"rawliteral(
Serial1 Terminal - CAN Logger
-
-
+
-
📟 Serial1 Terminal GPIO 17/18
+
📊 Monitor
📤 Transmit
📈 Graph
@@ -544,7 +290,7 @@ const char serial_terminal_html[] PROGMEM = R"rawliteral(
📟 Serial2
-
+
diff --git a/settings.h b/settings.h
index 5bfc0d2..f13a685 100644
--- a/settings.h
+++ b/settings.h
@@ -9,352 +9,211 @@ const char settings_html[] PROGMEM = R"rawliteral(
Settings - Byun CAN Logger
-
-
+
-
⚙️ Settings
Configure WiFi and System Settings
+
📊 Monitor
📤 Transmit
📈 Graph
@@ -364,7 +223,7 @@ const char settings_html[] PROGMEM = R"rawliteral(
📟 Serial2
-
+
CAN Transmit - Sequence Editor
✓
Settings saved successfully!
@@ -420,7 +279,7 @@ const char settings_html[] PROGMEM = R"rawliteral(
- 외부 WiFi 비밀번호
+
diff --git a/transmit.h b/transmit.h
index e582125..702b582 100644
--- a/transmit.h
+++ b/transmit.h
@@ -9,330 +9,224 @@ const char transmit_html[] PROGMEM = R"rawliteral(
✓ WiFi 연결됨:
🔴 Disconnected
-
-
+
-
🚀 CAN Sequence Transmitter
Create and Execute CAN Message Sequences
+
📊 Monitor
📤 Transmit
📈 Graph
@@ -342,7 +236,7 @@ const char transmit_html[] PROGMEM = R"rawliteral(
📟 Serial2
-
+