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(
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();