Files
esp32s3_CANFD_logger/transmit.h
2026-04-30 19:00:06 +00:00

268 lines
12 KiB
C

#ifndef TRANSMIT_H
#define TRANSMIT_H
const char transmit_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}
nav{background:#161b22;border-bottom:1px solid #30363d;display:flex;overflow-x:auto;scrollbar-width:none;-webkit-overflow-scrolling:touch}
nav::-webkit-scrollbar{display:none}
nav .nav-title{color:#43cea2;font-weight:700;font-size:1.0em;padding:10px 14px;white-space:nowrap;border-bottom:2px solid transparent}
nav a{display:inline-flex;align-items:center;padding:10px 13px;text-decoration:none;color:#8b949e;font-size:.78em;font-weight:500;border-bottom:2px solid transparent;white-space:nowrap;transition:color .2s,border-color .2s}
nav a:hover{color:#e6edf3}
nav a.active{color:#43cea2;border-bottom-color:#43cea2}
.title{color:#e6edf3;font-weight:700;font-size:15px;margin-right:8px}
.container{padding:12px;display:grid;gap:10px;max-width:900px}
.card{background:#161b22;border:1px solid #30363d;border-radius:8px;padding:14px}
.card h3{font-size:12px;color:#8b949e;margin-bottom:12px;text-transform:uppercase;letter-spacing:.5px}
.row{display:flex;gap:8px;align-items:flex-end;flex-wrap:wrap}
.field{margin-bottom:8px}
label{display:block;font-size:11px;color:#8b949e;margin-bottom:3px}
input[type=text],select{background:#21262d;border:1px solid #30363d;color:#c9d1d9;padding:5px 8px;border-radius:6px;font-size:12px}
input[type=checkbox]{accent-color:#1f6feb}
.w80{width:80px} .w100{width:100px} .w60{width:60px} .w120{width:120px}
.btn{padding:5px 12px;border:none;border-radius:6px;cursor:pointer;font-size:12px;font-weight:600;transition:.2s}
.btn-green{background:#238636;color:#fff} .btn-blue{background:#1f6feb;color:#fff}
.btn-red{background:#da3633;color:#fff} .btn-gray{background:#21262d;color:#c9d1d9;border:1px solid #30363d}
.data-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(52px,1fr));gap:4px;margin:8px 0}
.data-cell{background:#21262d;border:1px solid #30363d;border-radius:4px;padding:4px 6px;text-align:center;font-family:monospace;font-size:13px;color:#e6edf3;cursor:pointer}
.data-cell:focus{outline:none;border-color:#1f6feb;background:#1c2128}
.dlc-info{font-size:11px;color:#8b949e;margin-top:4px}
table{width:100%;border-collapse:collapse;font-size:11px}
th{background:#21262d;color:#8b949e;padding:6px 8px;text-align:left;font-weight:600}
td{padding:5px 8px;border-bottom:1px solid #21262d}
.badge{padding:2px 7px;border-radius:10px;font-size:10px;font-weight:700;display:inline-block}
.badge-fd{background:#2d1b69;color:#a371f7} .badge-cl{background:#033a16;color:#3fb950}
.badge-on{background:#033a16;color:#3fb950} .badge-off{background:#3d0000;color:#f85149}
#log{background:#0d1117;border:1px solid #30363d;border-radius:6px;padding:8px;height:120px;overflow-y:auto;font-family:monospace;font-size:11px;color:#3fb950}
</style>
</head><body>
<nav>
<span class="nav-title"> CANFD Logger</span>
<a href="/"></a>
<a href="/transmit" class="active"></a>
<a href="/graph"></a>
<a href="/graph-view"> </a>
<a href="/settings"></a>
</nav>
<div class="container">
<!-- -->
<div class="card">
<h3>CAN / CAN FD </h3>
<div class="row">
<div class="field">
<label>CAN ID (hex)</label>
<input type="text" id="txId" value="0x123" class="w100">
</div>
<div class="field">
<label>DLC</label>
<select id="txDlc" onchange="onDlcChange()" class="w80">
<option>0</option><option>1</option><option>2</option><option>3</option>
<option>4</option><option>5</option><option>6</option><option>7</option>
<option selected>8</option><option>9</option><option>10</option><option>11</option>
<option>12</option><option>13</option><option>14</option><option>15</option>
</select>
</div>
<div class="field" style="display:flex;align-items:center;gap:6px;margin-bottom:8px">
<input type="checkbox" id="txExt"><label style="display:inline">EXT (29bit)</label>
<input type="checkbox" id="txFD" onchange="onFDChange()"><label style="display:inline">FD</label>
<input type="checkbox" id="txBRS" disabled><label style="display:inline;color:#8b949e" id="brsLabel">BRS</label>
</div>
</div>
<label> (hex, )</label>
<div class="data-grid" id="dataGrid"></div>
<div class="dlc-info" id="dlcInfo">DLC 8 = 8 bytes</div>
<div style="margin-top:10px;display:flex;gap:8px">
<button class="btn btn-green" onclick="sendOnce()"> </button>
<button class="btn btn-gray" onclick="clearData()"></button>
</div>
</div>
<!-- -->
<div class="card">
<h3> ( 16)</h3>
<div class="row" style="margin-bottom:10px">
<div class="field"><label> #</label><select id="slotSel" class="w60" onchange="loadSlot()">
<script>for(let i=0;i<16;i++)document.write('<option>'+i+'</option>')</script>
</select></div>
<div class="field"><label>CAN ID</label><input type="text" id="pId" value="0x200" class="w100"></div>
<div class="field"><label>DLC</label><select id="pDlc" onchange="onPDlcChange()" class="w80">
<script>for(let i=0;i<=15;i++)document.write('<option'+(i===8?' selected':'')+'>'+i+'</option>')</script>
</select></div>
<div class="field"><label>(ms)</label><input type="text" id="pInterval" value="100" class="w80"></div>
</div>
<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px">
<input type="checkbox" id="pExt"><label>EXT</label>
<input type="checkbox" id="pFD" onchange="onPFDChange()"><label>FD</label>
<input type="checkbox" id="pBRS" disabled><label style="color:#8b949e" id="pBrsLabel">BRS</label>
<input type="checkbox" id="pActive"><label style="color:#3fb950"></label>
</div>
<label></label>
<div class="data-grid" id="pDataGrid"></div>
<div style="margin-top:10px;display:flex;gap:8px">
<button class="btn btn-blue" onclick="saveSlot()">💾 </button>
</div>
<div style="margin-top:12px">
<table id="slotTable">
<thead><tr><th>#</th><th>ID</th><th></th><th>DLC</th><th></th><th></th></tr></thead>
<tbody id="slotBody"></tbody>
</table>
</div>
</div>
<!-- -->
<div class="card">
<h3> </h3>
<div id="log"></div>
<button class="btn btn-gray" onclick="document.getElementById('log').innerHTML=''" style="margin-top:6px"></button>
</div>
</div>
<script>
const DLC_LEN=[0,1,2,3,4,5,6,7,8,12,16,20,24,32,48,64];
let ws, reconnTimer;
let slots=Array(16).fill(null).map(()=>({id:'0x200',ext:false,fd:false,brs:false,dlc:8,data:new Array(64).fill(0),interval:100,active:false}));
// ── 단발 데이터 그리드 ────────────────────────
function buildGrid(id,len){
let g=document.getElementById(id); g.innerHTML='';
for(let i=0;i<len;i++){
let c=document.createElement('div');
c.className='data-cell'; c.contentEditable='true';
c.textContent='00'; c.dataset.idx=i;
c.onkeydown=e=>{
if(e.key==='Tab'){e.preventDefault();let n=g.children[i+1];if(n)n.focus();}
if(!/[0-9a-fA-F]|Backspace|Tab|ArrowLeft|ArrowRight/.test(e.key)) e.preventDefault();
};
c.oninput=()=>{let v=c.textContent.replace(/[^0-9a-fA-F]/g,'');if(v.length>2)v=v.slice(-2);c.textContent=v||'00';};
g.appendChild(c);
}
}
function getGridData(id,len){
let g=document.getElementById(id);
let d=[];
for(let i=0;i<len;i++) d.push(parseInt(g.children[i]?.textContent||'0',16)||0);
return d;
}
function setGridData(id,data,len){
let g=document.getElementById(id);
for(let i=0;i<len;i++){
if(g.children[i]) g.children[i].textContent=(data[i]||0).toString(16).toUpperCase().padStart(2,'0');
}
}
function onDlcChange(){
let dlc=parseInt(document.getElementById('txDlc').value);
let len=DLC_LEN[dlc]||0;
document.getElementById('dlcInfo').textContent='DLC '+dlc+' = '+len+' bytes';
buildGrid('dataGrid',len);
}
function onFDChange(){
let fd=document.getElementById('txFD').checked;
document.getElementById('txBRS').disabled=!fd;
document.getElementById('brsLabel').style.color=fd?'#c9d1d9':'#8b949e';
}
function onPDlcChange(){
let dlc=parseInt(document.getElementById('pDlc').value);
buildGrid('pDataGrid',DLC_LEN[dlc]||0);
}
function onPFDChange(){
let fd=document.getElementById('pFD').checked;
document.getElementById('pBRS').disabled=!fd;
document.getElementById('pBrsLabel').style.color=fd?'#c9d1d9':'#8b949e';
}
function clearData(){ let g=document.getElementById('dataGrid'); for(let c of g.children) c.textContent='00'; }
function sendOnce(){
let dlc=parseInt(document.getElementById('txDlc').value);
let len=DLC_LEN[dlc]||0;
let d=getGridData('dataGrid',len);
let id=document.getElementById('txId').value;
let msg={cmd:'sendOnce',id:id,ext:document.getElementById('txExt').checked,
fd:document.getElementById('txFD').checked,brs:document.getElementById('txBRS').checked,
dlc:dlc,data:d};
ws.send(JSON.stringify(msg));
addLog('TX: ID='+id+' DLC='+dlc+(document.getElementById('txFD').checked?' [FD]':'')+
' Data:'+d.slice(0,len).map(b=>b.toString(16).toUpperCase().padStart(2,'0')).join(' '));
}
function loadSlot(){
let i=parseInt(document.getElementById('slotSel').value);
let s=slots[i];
document.getElementById('pId').value=s.id;
document.getElementById('pExt').checked=s.ext;
document.getElementById('pFD').checked=s.fd;
document.getElementById('pBRS').checked=s.brs;
document.getElementById('pBRS').disabled=!s.fd;
document.getElementById('pDlc').value=s.dlc;
document.getElementById('pInterval').value=s.interval;
document.getElementById('pActive').checked=s.active;
let len=DLC_LEN[s.dlc]||0;
buildGrid('pDataGrid',len);
setGridData('pDataGrid',s.data,len);
onPFDChange();
}
function saveSlot(){
let i=parseInt(document.getElementById('slotSel').value);
let dlc=parseInt(document.getElementById('pDlc').value);
let len=DLC_LEN[dlc]||0;
let d=getGridData('pDataGrid',len);
let full=new Array(64).fill(0); for(let j=0;j<len;j++) full[j]=d[j];
slots[i]={id:document.getElementById('pId').value,ext:document.getElementById('pExt').checked,
fd:document.getElementById('pFD').checked,brs:document.getElementById('pBRS').checked,
dlc,data:full,interval:parseInt(document.getElementById('pInterval').value)||0,
active:document.getElementById('pActive').checked};
ws.send(JSON.stringify({cmd:'setTxMsg',idx:i,...slots[i]}));
renderSlots();
addLog(' '+i+' : ID='+slots[i].id+(slots[i].active?' []':' []'));
}
function renderSlots(){
let html='';
slots.forEach((s,i)=>{
if(s.dlc>0||s.active){
html+=`<tr>
<td>${i}</td>
<td>${s.id}</td>
<td>${s.fd?'<span class="badge badge-fd">FD</span>':'<span class="badge badge-cl">CL</span>'}</td>
<td>${s.dlc}</td><td>${s.interval}ms</td>
<td><span class="badge ${s.active?'badge-on':'badge-off'}">${s.active?'ON':'OFF'}</span></td>
</tr>`;
}
});
document.getElementById('slotBody').innerHTML=html||'<tr><td colspan="6" style="color:#8b949e;text-align:center"> </td></tr>';
}
function addLog(msg){
let l=document.getElementById('log');
let t=new Date().toLocaleTimeString('ko-KR',{hour12:false});
l.innerHTML+=`<div style="color:#8b949e">[${t}]</div><div>${msg}</div>`;
l.scrollTop=l.scrollHeight;
}
function connect(){
ws=new WebSocket('ws://'+location.hostname+':81/');
ws.onopen=()=>{ clearTimeout(reconnTimer); };
ws.onclose=()=>{ reconnTimer=setTimeout(connect,3000); };
ws.onmessage=e=>{ try{ handleMsg(JSON.parse(e.data)); }catch(ex){} };
}
function handleMsg(d){ } // 필요시 확장
// 초기화
buildGrid('dataGrid',8);
buildGrid('pDataGrid',8);
document.getElementById('dlcInfo').textContent='DLC 8 = 8 bytes';
connect();
</script>
</body></html>
)rawliteral";
#endif