기본 동작 확인

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

222
graph_viewer.h Normal file
View File

@@ -0,0 +1,222 @@
#ifndef GRAPH_VIEWER_H
#define GRAPH_VIEWER_H
const char graph_viewer_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;max-width:1000px}
.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}
.btn{padding:5px 12px;border:none;border-radius:6px;cursor:pointer;font-size:12px;font-weight:600}
.btn-blue{background:#1f6feb;color:#fff} .btn-gray{background:#21262d;color:#c9d1d9;border:1px solid #30363d}
input[type=text],input[type=file]{background:#21262d;border:1px solid #30363d;color:#c9d1d9;padding:5px 8px;border-radius:6px;font-size:12px}
.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}
.info{font-size:11px;color:#8b949e;margin-top:6px}
.stat{display:inline-block;margin-right:16px;color:#8b949e}
.stat b{color:#e6edf3}
</style>
</head><body>
<nav>
<span class="title"> CANFD Logger</span>
<a href="/"></a><a href="/transmit"></a>
<a href="/graph"></a><a href="/graph-view" class="active"></a>
<a href="/settings"></a>
</nav>
<div class="container">
<div class="card">
<h3>BIN (CAN FD)</h3>
<div class="row">
<div class="field">
<label>BIN ()</label>
<input type="file" id="binFile" accept=".bin" onchange="loadBin()">
</div>
<div class="field" style="margin-bottom:4px">
<button class="btn btn-gray" onclick="document.getElementById('dlUrl').style.display='block'">URL로 </button>
</div>
</div>
<div id="dlUrl" style="display:none;margin-bottom:8px">
<label> URL (SD )</label>
<div class="row">
<input type="text" id="urlInput" placeholder="예: CANFD_20250430_120000.bin" style="width:260px">
<button class="btn btn-blue" onclick="loadUrl()"></button>
</div>
</div>
<div id="fileInfo" class="info"></div>
</div>
<div class="card" id="analyzeCard" style="display:none">
<h3> </h3>
<div class="row">
<div class="field"><label>CAN ID (hex)</label><input type="text" id="vSigId" value="0x123" style="width:90px"></div>
<div class="field"><label> </label><input type="text" id="vSigStart" value="0" style="width:60px"></div>
<div class="field"><label> </label><input type="text" id="vSigLen" value="8" style="width:60px"></div>
<div class="field"><label></label><input type="text" id="vSigScale" value="1" style="width:60px"></div>
<div class="field"><label></label><input type="text" id="vSigName" value="Signal 1" style="width:90px"></div>
<div class="field" style="margin-bottom:4px"><button class="btn btn-blue" onclick="addSig()">+ </button></div>
</div>
<div id="vSigList" style="display:flex;gap:8px;flex-wrap:wrap;margin:8px 0"></div>
<canvas id="vChart"></canvas>
<div style="margin-top:8px">
<span class="stat"> : <b id="timeRange">-</b></span>
<span class="stat"> : <b id="totalMsgs">0</b></span>
<span class="stat">FD : <b id="fdMsgs">0</b></span>
</div>
<div style="margin-top:8px;display:flex;gap:8px">
<button class="btn btn-gray" onclick="clearSigs()"> </button>
<button class="btn btn-blue" onclick="exportCSV()">CSV </button>
</div>
</div>
</div>
<script>
const COLORS=['#58a6ff','#3fb950','#d29922','#f85149','#a371f7','#ffa657','#79c0ff','#56d364'];
const RECORD_SIZE=79; // sizeof(CANFDLog): 8+4+1+1+1+64
let records=[], signals=[];
function parseBin(buf){
records=[];
let n=Math.floor(buf.byteLength/RECORD_SIZE);
let dv=new DataView(buf);
for(let i=0;i<n;i++){
let off=i*RECORD_SIZE;
let tsLo=dv.getUint32(off,true), tsHi=dv.getUint32(off+4,true);
let ts=tsHi*0x100000000+tsLo; // uint64 → JS Number (μs, precision ok for logging)
let id=dv.getUint32(off+8,true);
let dlc=dv.getUint8(off+12);
let len=dv.getUint8(off+13);
let flags=dv.getUint8(off+14);
let data=new Uint8Array(buf,off+15,Math.min(len,64));
records.push({ts,id,dlc,len,flags,data:Array.from(data)});
}
let total=records.length;
let fdCnt=records.filter(r=>(r.flags&0x20)!==0).length;
document.getElementById('totalMsgs').textContent=total.toLocaleString();
document.getElementById('fdMsgs').textContent=fdCnt.toLocaleString();
if(total>0){
let t0=records[0].ts, t1=records[total-1].ts;
document.getElementById('timeRange').textContent=((t1-t0)/1e6).toFixed(3)+'s';
document.getElementById('fileInfo').textContent=
` ${total.toLocaleString()} | FD: ${fdCnt.toLocaleString()} | : ${((t1-t0)/1e6).toFixed(3)}s`;
}
document.getElementById('analyzeCard').style.display='block';
draw();
}
function loadBin(){
let f=document.getElementById('binFile').files[0]; if(!f) return;
document.getElementById('fileInfo').textContent=' ...';
let r=new FileReader();
r.onload=e=>parseBin(e.target.result);
r.readAsArrayBuffer(f);
}
function loadUrl(){
let fn=document.getElementById('urlInput').value.trim(); if(!fn) return;
document.getElementById('fileInfo').textContent=' ...';
fetch('/download?file='+fn).then(r=>r.arrayBuffer()).then(parseBin)
.catch(e=>{ document.getElementById('fileInfo').textContent=' : '+e; });
}
function extractBits(data,start,len){
let v=0;
for(let i=0;i<len;i++){
let bit=start+i,b=Math.floor(bit/8),bit2=7-(bit%8);
if(b<data.length&&(data[b]&(1<<bit2))) v|=(1<<(len-1-i));
}
return v;
}
function addSig(){
if(signals.length>=8) return alert(' 8');
let id=parseInt(document.getElementById('vSigId').value,16);
signals.push({id,startBit:parseInt(document.getElementById('vSigStart').value)||0,
bitLen:parseInt(document.getElementById('vSigLen').value)||8,
scale:parseFloat(document.getElementById('vSigScale').value)||1,
name:document.getElementById('vSigName').value||'Signal',
color:COLORS[signals.length%COLORS.length]});
renderSigList(); draw();
}
function clearSigs(){signals=[];renderSigList();draw();}
function removeSig(i){signals.splice(i,1);renderSigList();draw();}
function renderSigList(){
let h='';
signals.forEach((s,i)=>h+=`<span style="background:#21262d;border:1px solid #30363d;border-radius:12px;padding:3px 10px;font-size:11px;display:inline-flex;align-items:center;gap:6px"><span style="width:10px;height:10px;border-radius:50%;background:${s.color};display:inline-block"></span>${s.name}<span onclick="removeSig(${i})" style="cursor:pointer;color:#8b949e"></span></span>`);
document.getElementById('vSigList').innerHTML=h;
}
const cv=document.getElementById('vChart');
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(!records.length||!signals.length){
ctx.fillStyle='#8b949e';ctx.font='13px sans-serif';ctx.textAlign='center';
ctx.fillText(records.length?' .':'BIN .',W/2,H/2); return;
}
// 각 신호별 포인트 계산
let seriesList=signals.map(s=>{
let pts=records.filter(r=>r.id===s.id).map(r=>({x:r.ts,y:extractBits(r.data,s.startBit,s.bitLen)*s.scale}));
return {s,pts};
}).filter(s=>s.pts.length>0);
if(!seriesList.length) return;
let allPts=seriesList.flatMap(s=>s.pts);
let minX=Math.min(...allPts.map(p=>p.x)), maxX=Math.max(...allPts.map(p=>p.x));
let minY=Math.min(...allPts.map(p=>p.y)), maxY=Math.max(...allPts.map(p=>p.y));
if(minY===maxY){minY-=1;maxY+=1;}
let pad=35;
function toX(x){return pad+(x-minX)/(maxX-minX||1)*(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);}
// X축 레이블
ctx.textAlign='center';
for(let i=0;i<=4;i++){let x=pad+i*(W-pad*2)/4;let t=((minX+(maxX-minX)*i/4-minX)/1e6).toFixed(2)+'s';ctx.fillText(t,x,H-4);}
// 신호 그리기
seriesList.forEach(({s,pts})=>{
ctx.strokeStyle=s.color;ctx.lineWidth=1.5;ctx.beginPath();
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();
ctx.fillStyle=s.color;ctx.font='11px monospace';ctx.textAlign='left';
let last=pts[pts.length-1];
ctx.fillText(s.name+': '+last.y.toFixed(2),toX(last.x)+4,toY(last.y)-4);
});
}
function exportCSV(){
if(!records.length) return;
let cols=['Time_us','ID','DLC','Len','FD','BRS','EXT'];
signals.forEach(s=>cols.push(s.name));
let rows=[cols.join(',')];
records.forEach(r=>{
let row=[r.ts,(r.id>>>0).toString(16).toUpperCase(),r.dlc,r.len,
(r.flags&0x20)?1:0,(r.flags&0x10)?1:0,(r.flags&0x80)?1:0];
signals.forEach(s=>{
if(s.id===r.id) row.push((extractBits(r.data,s.startBit,s.bitLen)*s.scale).toFixed(4));
else row.push('');
});
rows.push(row.join(','));
});
let blob=new Blob([rows.join('\n')],{type:'text/csv'});
let a=document.createElement('a');a.href=URL.createObjectURL(blob);
a.download='canfd_export.csv';a.click();
}
</script>
</body></html>
)rawliteral";
#endif