#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
