239 lines
11 KiB
C++
239 lines
11 KiB
C++
#ifndef SETTINGS_H
|
|
#define SETTINGS_H
|
|
const char settings_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:16px;max-width:700px}
|
|
.card{background:#161b22;border:1px solid #30363d;border-radius:8px;padding:16px;margin-bottom:12px}
|
|
.card h3{font-size:12px;color:#8b949e;margin-bottom:14px;text-transform:uppercase;letter-spacing:.5px;border-bottom:1px solid #30363d;padding-bottom:8px}
|
|
.field{margin-bottom:12px}
|
|
label{display:block;font-size:12px;color:#8b949e;margin-bottom:4px}
|
|
input[type=text],input[type=password],select{width:100%;background:#21262d;border:1px solid #30363d;color:#c9d1d9;padding:7px 10px;border-radius:6px;font-size:13px}
|
|
input[type=checkbox]{width:16px;height:16px;margin-right:6px;accent-color:#1f6feb}
|
|
.row{display:flex;gap:10px;align-items:flex-end}
|
|
.row .field{flex:1}
|
|
.btn{padding:7px 16px;border:none;border-radius:6px;cursor:pointer;font-size:12px;font-weight:600;transition:.2s}
|
|
.btn-green{background:#238636;color:#fff} .btn-green:hover{background:#2ea043}
|
|
.btn-blue{background:#1f6feb;color:#fff} .btn-blue:hover{background:#388bfd}
|
|
.btn-gray{background:#21262d;color:#c9d1d9;border:1px solid #30363d}
|
|
.info{background:#051d4d;border:1px solid #1f6feb;border-radius:6px;padding:10px;font-size:12px;color:#79c0ff;margin-top:8px}
|
|
.warn{background:#3d2200;border:1px solid #9e6a03;border-radius:6px;padding:10px;font-size:12px;color:#ffa657;margin-top:8px}
|
|
.speed-table{width:100%;border-collapse:collapse;font-size:12px;margin-top:8px}
|
|
.speed-table th{background:#21262d;color:#8b949e;padding:6px 8px;text-align:left}
|
|
.speed-table td{padding:5px 8px;border-bottom:1px solid #21262d}
|
|
.speed-table tr.selected td{background:#051d4d;color:#79c0ff}
|
|
.speed-table tr:hover td{background:#1c2128;cursor:pointer}
|
|
.badge{display:inline-block;padding:2px 7px;border-radius:10px;font-size:10px;font-weight:700}
|
|
.badge-fd{background:#2d1b69;color:#a371f7} .badge-cl{background:#033a16;color:#3fb950}
|
|
#toast{position:fixed;bottom:20px;right:20px;background:#238636;color:#fff;padding:10px 16px;border-radius:8px;display:none;font-size:13px;z-index:999}
|
|
</style>
|
|
</head><body>
|
|
<nav>
|
|
<span class="nav-title">⚡ CANFD Logger</span>
|
|
<a href="/">대시보드</a>
|
|
<a href="/transmit">송신</a>
|
|
<a href="/graph">그래프</a>
|
|
<a href="/graph-view">그래프 뷰어</a>
|
|
<a href="/settings" class="active">설정</a>
|
|
</nav>
|
|
<div class="container">
|
|
|
|
<!-- CAN FD 속도 설정 -->
|
|
<div class="card">
|
|
<h3>CAN FD 속도 / 모드</h3>
|
|
<div class="field">
|
|
<label>속도 프리셋 (클릭하여 선택)</label>
|
|
<table class="speed-table" id="speedTable">
|
|
<thead><tr><th>#</th><th>이름</th><th>중재 비트레이트</th><th>데이터 비트레이트</th><th>타입</th></tr></thead>
|
|
<tbody id="speedBody"></tbody>
|
|
</table>
|
|
</div>
|
|
<div class="field" style="display:flex;align-items:center;gap:8px">
|
|
<input type="checkbox" id="listenOnly"><label for="listenOnly" style="display:inline;color:#c9d1d9">Listen Only 모드 (수신 전용, 버스 간섭 없음)</label>
|
|
</div>
|
|
<div class="info" id="speedInfo">속도 프리셋을 선택하세요.</div>
|
|
<div class="warn">⚠ 속도 변경 후 "CAN 재초기화"를 눌러야 즉시 적용됩니다. SD 로깅 중에는 중지 후 변경하세요.</div>
|
|
<div style="margin-top:12px;display:flex;gap:8px">
|
|
<button class="btn btn-green" onclick="reinitCAN()">⚡ CAN 재초기화</button>
|
|
<button class="btn btn-gray" onclick="saveAndRestart()">💾 저장 후 재시작</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- WiFi 설정 -->
|
|
<div class="card">
|
|
<h3>WiFi 설정 (AP 모드)</h3>
|
|
<div class="row">
|
|
<div class="field"><label>AP SSID</label><input type="text" id="ssid" placeholder="CANFD_Logger"></div>
|
|
<div class="field"><label>AP 비밀번호</label><input type="password" id="password" placeholder="12345678"></div>
|
|
</div>
|
|
<div class="field" style="display:flex;align-items:center;gap:8px">
|
|
<input type="checkbox" id="staEnable" onchange="toggleSTA()"><label for="staEnable" style="display:inline;color:#c9d1d9">STA 모드 추가 연결 (WiFi 라우터 접속 + NTP 동기화)</label>
|
|
</div>
|
|
<div id="staFields" style="display:none">
|
|
<div class="row">
|
|
<div class="field"><label>연결할 WiFi SSID</label><input type="text" id="staSSID"></div>
|
|
<div class="field"><label>WiFi 비밀번호</label><input type="password" id="staPass"></div>
|
|
</div>
|
|
<div class="info" id="staInfo"></div>
|
|
</div>
|
|
<div style="margin-top:12px">
|
|
<button class="btn btn-blue" onclick="saveWifi()">💾 WiFi 설정 저장 (재시작 필요)</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 시스템 정보 -->
|
|
<div class="card">
|
|
<h3>시스템</h3>
|
|
<div style="font-size:12px;color:#8b949e;line-height:2">
|
|
<b style="color:#c9d1d9">핀 배선 (고정)</b><br>
|
|
CAN SPI: SCK=12, MOSI=11, MISO=13, CS=10 | INT: 폴링모드(255)<br>
|
|
SD SDIO: CLK=39, CMD=38, D0=40, D1=41, D2=42, D3=2<br>
|
|
RTC I2C: SDA=8, SCL=18 (DS3231)<br><br>
|
|
<b style="color:#c9d1d9">오실레이터</b>: OSC_20MHz (알리 MCP2518FD 보드)<br>
|
|
<b style="color:#c9d1d9">라이브러리</b>: ACAN2517FD by Pierre Molinaro<br>
|
|
</div>
|
|
<div style="margin-top:12px;display:flex;gap:8px;flex-wrap:wrap">
|
|
<button class="btn btn-gray" onclick="hwReset()">🔌 장치 재시작</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div id="toast"></div>
|
|
|
|
<script>
|
|
const PRESETS=[
|
|
{name:'Classic 125K', arb:'125 kbps', data:'125 kbps', fd:false},
|
|
{name:'Classic 250K', arb:'250 kbps', data:'250 kbps', fd:false},
|
|
{name:'Classic 500K', arb:'500 kbps', data:'500 kbps', fd:false},
|
|
{name:'Classic 1M', arb:'1 Mbps', data:'1 Mbps', fd:false},
|
|
{name:'FD 500K/2M', arb:'500 kbps', data:'2 Mbps', fd:true},
|
|
{name:'FD 500K/4M', arb:'500 kbps', data:'4 Mbps', fd:true},
|
|
{name:'FD 1M/4M', arb:'1 Mbps', data:'4 Mbps', fd:true},
|
|
{name:'FD 1M/8M', arb:'1 Mbps', data:'8 Mbps', fd:true},
|
|
];
|
|
let selIdx=4;
|
|
let ws, reconnTimer;
|
|
|
|
function renderPresets(){
|
|
let html='';
|
|
PRESETS.forEach((p,i)=>{
|
|
let cls=i===selIdx?'class="selected"':'';
|
|
let badge=p.fd?'<span class="badge badge-fd">CAN FD</span>':'<span class="badge badge-cl">Classic</span>';
|
|
html+=`<tr ${cls} onclick="selectPreset(${i})"><td>${i}</td><td>${p.name}</td><td>${p.arb}</td><td>${p.data}</td><td>${badge}</td></tr>`;
|
|
});
|
|
document.getElementById('speedBody').innerHTML=html;
|
|
updateSpeedInfo();
|
|
}
|
|
|
|
function selectPreset(i){
|
|
selIdx=i; renderPresets();
|
|
}
|
|
|
|
function updateSpeedInfo(){
|
|
let p=PRESETS[selIdx];
|
|
document.getElementById('speedInfo').innerHTML=
|
|
`선택: <b>${p.name}</b> | 중재: ${p.arb} | 데이터: ${p.data} | `+
|
|
(p.fd?'<b style="color:#a371f7">CAN FD 모드</b>':'<b style="color:#3fb950">Classic CAN 모드</b>');
|
|
}
|
|
|
|
function connect(){
|
|
ws=new WebSocket('ws://'+location.hostname+':81/');
|
|
ws.onopen=()=>{
|
|
clearTimeout(reconnTimer);
|
|
ws.send(JSON.stringify({cmd:'getSettings'}));
|
|
};
|
|
ws.onclose=()=>{ reconnTimer=setTimeout(connect,3000); };
|
|
ws.onmessage=e=>{
|
|
try{ let d=JSON.parse(e.data); handleMsg(d); }catch(ex){}
|
|
};
|
|
}
|
|
|
|
function handleMsg(d){
|
|
if(d.type==='settings'){
|
|
document.getElementById('ssid').value=d.ssid||'';
|
|
document.getElementById('password').value=d.password||'';
|
|
document.getElementById('staEnable').checked=!!d.staEnable;
|
|
document.getElementById('staSSID').value=d.staSSID||'';
|
|
if(d.staConnected){
|
|
document.getElementById('staInfo').textContent='연결됨: '+d.staIP;
|
|
}
|
|
if(d.speedIdx!==undefined){ selIdx=d.speedIdx; renderPresets(); }
|
|
document.getElementById('listenOnly').checked=!!d.listenOnly;
|
|
toggleSTA();
|
|
}
|
|
else if(d.type==='reinitResult'){
|
|
toast(d.success?'✓ CAN 재초기화 완료: '+d.preset:'✗ 재초기화 실패','#238636');
|
|
}
|
|
else if(d.type==='settingsSaved'){
|
|
toast('설정 저장됨','#238636');
|
|
}
|
|
}
|
|
|
|
function toggleSTA(){
|
|
document.getElementById('staFields').style.display=
|
|
document.getElementById('staEnable').checked?'block':'none';
|
|
}
|
|
|
|
function reinitCAN(){
|
|
ws.send(JSON.stringify({
|
|
cmd:'reinitCAN',
|
|
speedIdx:selIdx,
|
|
listenOnly:document.getElementById('listenOnly').checked
|
|
}));
|
|
}
|
|
|
|
function saveWifi(){
|
|
ws.send(JSON.stringify({
|
|
cmd:'saveSettings',
|
|
ssid:document.getElementById('ssid').value,
|
|
password:document.getElementById('password').value,
|
|
staEnable:document.getElementById('staEnable').checked,
|
|
staSSID:document.getElementById('staSSID').value,
|
|
staPassword:document.getElementById('staPass').value,
|
|
speedIdx:selIdx,
|
|
listenOnly:document.getElementById('listenOnly').checked
|
|
}));
|
|
}
|
|
|
|
function saveAndRestart(){
|
|
if(!confirm('설정을 저장하고 재시작합니다.')) return;
|
|
ws.send(JSON.stringify({
|
|
cmd:'saveSettings',
|
|
ssid:document.getElementById('ssid').value,
|
|
password:document.getElementById('password').value,
|
|
staEnable:document.getElementById('staEnable').checked,
|
|
staSSID:document.getElementById('staSSID').value,
|
|
staPassword:document.getElementById('staPass').value,
|
|
speedIdx:selIdx,
|
|
listenOnly:document.getElementById('listenOnly').checked
|
|
}));
|
|
setTimeout(()=>ws.send(JSON.stringify({cmd:'hwReset'})),500);
|
|
}
|
|
|
|
function hwReset(){ if(confirm('재시작?')) ws.send(JSON.stringify({cmd:'hwReset'})); }
|
|
|
|
function toast(msg,col='#238636'){
|
|
let t=document.getElementById('toast');
|
|
t.textContent=msg; t.style.background=col; t.style.display='block';
|
|
setTimeout(()=>t.style.display='none',3000);
|
|
}
|
|
|
|
renderPresets();
|
|
connect();
|
|
</script>
|
|
</body></html>
|
|
)rawliteral";
|
|
#endif |