기본 동작 확인

This commit is contained in:
2026-04-30 17:40:07 +00:00
parent fe9874eb5a
commit 4dc7c5f0f6
6 changed files with 2389 additions and 0 deletions

157
graph.h Normal file
View File

@@ -0,0 +1,157 @@
#ifndef GRAPH_H
#define GRAPH_H
const char graph_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html><html lang="ko"><head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title> - CANFD Logger</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:'Segoe UI',sans-serif;background:#0d1117;color:#c9d1d9;font-size:13px}
nav{background:#161b22;border-bottom:1px solid #30363d;padding:8px 16px;display:flex;align-items:center;gap:12px;flex-wrap:wrap}
nav a{color:#58a6ff;text-decoration:none;padding:4px 10px;border-radius:6px;font-size:12px}
nav a:hover{background:#21262d} nav a.active{background:#1f6feb;color:#fff}
.title{color:#e6edf3;font-weight:700;font-size:15px}
.container{padding:12px}
.card{background:#161b22;border:1px solid #30363d;border-radius:8px;padding:14px;margin-bottom:10px}
.card h3{font-size:12px;color:#8b949e;margin-bottom:12px;text-transform:uppercase}
input[type=text],select{background:#21262d;border:1px solid #30363d;color:#c9d1d9;padding:5px 8px;border-radius:6px;font-size:12px}
.btn{padding:5px 12px;border:none;border-radius:6px;cursor:pointer;font-size:12px;font-weight:600}
.btn-blue{background:#1f6feb;color:#fff} .btn-green{background:#238636;color:#fff}
.btn-gray{background:#21262d;color:#c9d1d9;border:1px solid #30363d}
.row{display:flex;gap:8px;align-items:flex-end;flex-wrap:wrap;margin-bottom:8px}
.field{margin-bottom:4px} label{display:block;font-size:11px;color:#8b949e;margin-bottom:3px}
canvas{width:100%;height:300px;background:#0d1117;border:1px solid #30363d;border-radius:6px}
.sig-list{display:flex;gap:8px;flex-wrap:wrap;margin:8px 0}
.sig-chip{background:#21262d;border:1px solid #30363d;border-radius:12px;padding:3px 10px;font-size:11px;display:flex;align-items:center;gap:6px}
.sig-color{width:10px;height:10px;border-radius:50%;display:inline-block}
</style>
</head><body>
<nav>
<span class="title"> CANFD Logger</span>
<a href="/"></a><a href="/transmit"></a>
<a href="/graph" class="active"></a><a href="/settings"></a>
</nav>
<div class="container">
<div class="card">
<h3> (CAN FD)</h3>
<div class="row">
<div class="field"><label>CAN ID (hex)</label><input type="text" id="sigId" value="0x123" style="width:90px"></div>
<div class="field"><label> </label><input type="text" id="sigStart" value="0" style="width:60px"></div>
<div class="field"><label> </label><input type="text" id="sigLen" value="8" style="width:60px"></div>
<div class="field"><label></label><input type="text" id="sigScale" value="1" style="width:60px"></div>
<div class="field"><label></label><input type="text" id="sigName" value="Signal 1" style="width:90px"></div>
<div class="field" style="margin-bottom:4px"><button class="btn btn-blue" onclick="addSignal()">+ </button></div>
</div>
<div class="sig-list" id="sigList"></div>
<canvas id="chart"></canvas>
<div style="margin-top:8px;display:flex;gap:8px">
<button class="btn btn-gray" onclick="clearSignals()"> </button>
<span style="font-size:11px;color:#8b949e;margin-top:8px"> 8 , 500 </span>
</div>
</div>
</div>
<script>
const COLORS=['#58a6ff','#3fb950','#d29922','#f85149','#a371f7','#ffa657','#79c0ff','#56d364'];
const DLC_LEN=[0,1,2,3,4,5,6,7,8,12,16,20,24,32,48,64];
let signals=[], ws, reconnTimer;
const MAX_PTS=500;
function addSignal(){
if(signals.length>=8) return alert(' 8');
let id=parseInt(document.getElementById('sigId').value,16);
let s=parseInt(document.getElementById('sigStart').value);
let l=parseInt(document.getElementById('sigLen').value);
let sc=parseFloat(document.getElementById('sigScale').value)||1;
let nm=document.getElementById('sigName').value||('Signal '+(signals.length+1));
let color=COLORS[signals.length%COLORS.length];
signals.push({id,startBit:s,bitLen:l,scale:sc,name:nm,color,pts:[],lastVal:null});
renderChips(); draw();
}
function clearSignals(){ signals=[]; renderChips(); draw(); }
function renderChips(){
let html='';
signals.forEach((s,i)=>{ html+=`<div class="sig-chip"><span class="sig-color" style="background:${s.color}"></span>${s.name} (0x${s.id.toString(16).toUpperCase()})<span onclick="removeSignal(${i})" style="cursor:pointer;color:#8b949e"></span></div>`; });
document.getElementById('sigList').innerHTML=html;
}
function removeSignal(i){ signals.splice(i,1); renderChips(); draw(); }
function extractBits(data,start,len){
let v=0;
for(let i=0;i<len;i++){
let bit=start+i; let b=Math.floor(bit/8); let bit2=7-(bit%8);
if(b<data.length&&(data[b]&(1<<bit2))) v|=(1<<(len-1-i));
}
return v;
}
function processMsg(m){
signals.forEach(s=>{
if(s.id===m.id){
let v=extractBits(m.data||[],s.startBit,s.bitLen)*s.scale;
let ts=Date.now();
s.pts.push({x:ts,y:v}); s.lastVal=v;
if(s.pts.length>MAX_PTS) s.pts.shift();
}
});
draw();
}
const cv=document.getElementById('chart');
const ctx=cv.getContext('2d');
function draw(){
let W=cv.offsetWidth, H=cv.offsetHeight;
cv.width=W; cv.height=H;
ctx.fillStyle='#0d1117'; ctx.fillRect(0,0,W,H);
if(signals.length===0){
ctx.fillStyle='#8b949e'; ctx.font='13px sans-serif'; ctx.textAlign='center';
ctx.fillText(' .',W/2,H/2); return;
}
let allPts=signals.flatMap(s=>s.pts);
if(allPts.length<2) return;
let now=Date.now();
let minX=now-30000, maxX=now;
let allY=allPts.map(p=>p.y);
let minY=Math.min(...allY), maxY=Math.max(...allY);
if(minY===maxY){minY-=1;maxY+=1;}
let pad=30;
function toX(x){return pad+(x-minX)/(maxX-minX)*(W-pad*2);}
function toY(y){return H-pad-(y-minY)/(maxY-minY)*(H-pad*2);}
// 격자
ctx.strokeStyle='#21262d'; ctx.lineWidth=1;
for(let i=0;i<=4;i++){let y=pad+i*(H-pad*2)/4;ctx.beginPath();ctx.moveTo(pad,y);ctx.lineTo(W-pad,y);ctx.stroke();}
// 축 레이블
ctx.fillStyle='#8b949e'; ctx.font='10px sans-serif'; ctx.textAlign='right';
for(let i=0;i<=4;i++){let y=pad+i*(H-pad*2)/4;let v=(maxY-(maxY-minY)*i/4).toFixed(2);ctx.fillText(v,pad-4,y+4);}
// 신호
signals.forEach(s=>{
if(s.pts.length<2) return;
ctx.strokeStyle=s.color; ctx.lineWidth=1.5; ctx.beginPath();
s.pts.forEach((p,i)=>{let x=toX(p.x),y=toY(p.y);i===0?ctx.moveTo(x,y):ctx.lineTo(x,y);});
ctx.stroke();
// 현재값
let last=s.pts[s.pts.length-1];
ctx.fillStyle=s.color; ctx.font='11px monospace'; ctx.textAlign='left';
ctx.fillText(s.name+': '+(s.lastVal!==null?s.lastVal.toFixed(2):'--'), toX(last.x)+4, toY(last.y)-4);
});
}
function connect(){
ws=new WebSocket('ws://'+location.hostname+':81/');
ws.onopen=()=>clearTimeout(reconnTimer);
ws.onclose=()=>reconnTimer=setTimeout(connect,3000);
ws.onmessage=e=>{
try{
let d=JSON.parse(e.data);
if(d.type==='update'&&d.messages) d.messages.forEach(m=>processMsg(m));
}catch(ex){}
};
}
connect();
setInterval(draw,100);
</script>
</body></html>
)rawliteral";
#endif