Files
esp32s3_canlogger_mcp2515/settings.h
2026-03-27 21:21:38 +00:00

563 lines
28 KiB
C++
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#ifndef SETTINGS_H
#define SETTINGS_H
const char settings_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Settings - Byun CAN Logger</title>
<style>
:root {
--bg: #0e1117;
--panel: #161b24;
--card: #1c2230;
--border: #2d3748;
--accent: #43cea2;
--green: #3fb950;
--blue: #58a6ff;
--red: #f85149;
--yellow: #e3b341;
--purple: #bc8cff;
--text: #e6edf3;
--muted: #8b949e;
--r: 8px;
}
* { margin:0; padding:0; box-sizing:border-box; }
html, body { min-height:100%; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: var(--bg); color: var(--text);
overflow-x: hidden; font-size: 14px;
}
.header {
background: linear-gradient(135deg,#1a2744 0%,#1e1a3a 100%);
padding: 11px 16px; border-bottom: 1px solid var(--border);
display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
}
.header h1 { font-size:1.0em; font-weight:700; color:var(--accent); display:flex; align-items:center; gap:8px; }
.header p { font-size:0.78em; color:var(--muted); margin:0; }
.nav {
background: var(--panel); border-bottom: 1px solid var(--border);
display: flex; overflow-x: auto; -webkit-overflow-scrolling: touch; scrollbar-width: none;
}
.nav::-webkit-scrollbar { display:none; }
.nav a {
display: inline-flex; align-items: center;
padding: 10px 13px; text-decoration: none; color: var(--muted);
font-size: 0.78em; font-weight: 500;
border-bottom: 2px solid transparent; white-space: nowrap;
transition: color .2s, border-color .2s;
}
.nav a:hover { color:var(--text); }
.nav a.active { color:var(--accent); border-bottom-color:var(--accent); }
.content { padding: 12px; }
h2 {
color:var(--accent); margin:14px 0 10px;
font-size:0.82em; font-weight:700; text-transform:uppercase;
letter-spacing:.5px; padding-bottom:6px; border-bottom:1px solid var(--border);
}
h3 { color:var(--text); font-size:0.85em; font-weight:600; margin-bottom:10px; }
.btn, button {
padding:6px 13px; border:1px solid var(--border);
border-radius:var(--r); background:var(--bg); color:var(--muted);
font-size:0.8em; font-weight:600; cursor:pointer;
font-family:inherit; transition:all .15s; white-space:nowrap;
-webkit-tap-highlight-color:transparent; touch-action:manipulation;
}
.btn:hover, button:hover { border-color:var(--accent); color:var(--accent); }
.btn:active, button:active { transform:scale(.97); }
.btn-primary { border-color:var(--blue); color:var(--blue); }
.btn-success { border-color:var(--accent); color:var(--accent); }
.btn-danger { border-color:var(--red); color:var(--red); }
.btn-warning { border-color:var(--yellow);color:var(--yellow);}
.btn-secondary { border-color:var(--muted); color:var(--muted); }
.btn-primary:hover { background:rgba(88,166,255,.10); }
.btn-success:hover { background:rgba(67,206,162,.10); }
.btn-danger:hover { background:rgba(248,81,73,.10); }
.btn-warning:hover { background:rgba(227,179,65,.10); }
.btn-secondary:hover { background:rgba(139,148,158,.10);}
.btn-small { padding:4px 9px; font-size:.75em; }
.button-group { display:flex; gap:6px; flex-wrap:wrap; margin-top:10px; }
label {
display:block; font-weight:600; color:var(--muted);
margin-bottom:4px; font-size:.78em;
text-transform:uppercase; letter-spacing:.3px;
}
input[type="text"], input[type="password"], input[type="number"], select, textarea {
width:100%; padding:7px 10px;
border:1px solid var(--border); border-radius:var(--r);
font-size:.85em; font-family:inherit;
background:var(--bg); color:var(--text); transition:border-color .2s;
}
input:focus, select:focus, textarea:focus {
outline:none; border-color:var(--accent);
box-shadow:0 0 0 2px rgba(67,206,162,.10);
}
input[type="checkbox"] { width:15px; height:15px; cursor:pointer; accent-color:var(--accent); }
select option { background:var(--panel); color:var(--text); }
.settings-section {
background:var(--panel); border:1px solid var(--border);
border-radius:var(--r); padding:13px; margin-bottom:10px;
}
.form-group { margin-bottom:12px; }
.form-group:last-child { margin-bottom:0; }
.help-text { font-size:.76em; color:var(--muted); margin-top:4px; line-height:1.4; }
.checkbox-group {
display:flex; align-items:center; gap:8px;
margin-bottom:8px; padding:7px 10px;
background:var(--card); border-radius:6px; border:1px solid var(--border);
}
.checkbox-group label { text-transform:none; cursor:pointer; margin-bottom:0; color:var(--text); font-size:.85em; letter-spacing:0; }
.alert {
padding:9px 13px; border-radius:var(--r);
margin-bottom:10px; display:none;
align-items:center; gap:9px;
font-size:.83em; font-weight:600;
border:1px solid var(--border); background:var(--panel); color:var(--text);
}
.alert.show { display:flex; }
.alert-success { border-color:rgba(67,206,162,.4); color:var(--accent); background:rgba(67,206,162,.07); }
.alert-info { border-color:rgba(88,166,255,.4); color:var(--blue); background:rgba(88,166,255,.07); }
.alert-warning { border-color:rgba(227,179,65,.4); color:var(--yellow); background:rgba(227,179,65,.07); }
.alert-icon { font-size:1.15em; }
.alert-text { flex:1; }
.connection-status {
position:fixed; top:10px; right:10px;
padding:4px 11px; border-radius:20px;
font-size:.75em; font-weight:600; z-index:1000;
border:1px solid var(--border); background:var(--panel); color:var(--muted);
}
.connection-status.connected { border-color:rgba(67,206,162,.5); color:var(--accent); background:rgba(67,206,162,.08); }
.connection-status.disconnected { border-color:rgba(248,81,73,.5); color:var(--red); background:rgba(248,81,73,.08); }
@media (max-width:480px) { .content{padding:8px;} }
/* ── Settings-specific ── */
.section-title {
color:var(--accent); font-size:.85em; font-weight:700;
margin-bottom:14px; padding-bottom:7px; border-bottom:1px solid var(--border);
display:flex; align-items:center; gap:7px; text-transform:uppercase; letter-spacing:.4px;
}
.sub-title {
color:var(--text); font-size:.82em; font-weight:700;
margin:14px 0 10px; padding:7px 10px;
background:var(--card); border-radius:6px;
border-left:3px solid var(--blue);
display:flex; align-items:center; gap:6px; flex-wrap:wrap;
}
.sub-title.ap { border-left-color:var(--accent); }
.sta-block {
background:var(--card); padding:13px; border-radius:var(--r);
margin-top:10px; border:1px solid var(--border); border-left:3px solid var(--blue);
transition:opacity .2s;
}
.sta-block.disabled { opacity:.4; pointer-events:none; }
.btn-save { border-color:var(--accent); color:var(--accent); flex:1; min-width:130px; padding:9px 18px; }
.btn-save:hover { background:rgba(67,206,162,.10); }
.btn-back { border-color:var(--muted); color:var(--muted); flex:1; min-width:130px; padding:9px 18px; }
.btn-back:hover { background:rgba(139,148,158,.10); }
.info-box {
background:rgba(88,166,255,.06); border:1px solid rgba(88,166,255,.2);
border-left:3px solid var(--blue); padding:11px 13px; border-radius:var(--r); margin-top:12px;
}
.info-box-title { font-weight:700; color:var(--blue); margin-bottom:5px; display:flex; align-items:center; gap:5px; font-size:.83em; }
.info-box-text { color:var(--muted); font-size:.80em; line-height:1.7; }
/* WiFi status badges */
.wbadge {
display:inline-flex; align-items:center; gap:5px;
padding:3px 9px; border-radius:10px; font-size:.72em; font-weight:600;
}
.wbadge.on { background:rgba(67,206,162,.12); color:var(--accent); border:1px solid rgba(67,206,162,.3); }
.wbadge.off { background:rgba(248,81,73,.10); color:var(--red); border:1px solid rgba(248,81,73,.25); }
.wbadge.ap { background:rgba(88,166,255,.10); color:var(--blue); border:1px solid rgba(88,166,255,.25); }
.wdot { width:6px; height:6px; border-radius:50%; background:currentColor; display:inline-block; }
/* IP row */
.ip-row {
display:flex; align-items:center; gap:8px; flex-wrap:wrap;
margin-top:8px; padding:7px 10px;
background:var(--bg); border-radius:6px; border:1px solid var(--border);
}
.ip-lbl { font-size:.72em; color:var(--muted); font-weight:700; text-transform:uppercase; min-width:55px; }
.ip-val { font-family:monospace; font-size:.88em; color:var(--text); font-weight:600; }
/* Scan */
.ssid-row { display:flex; gap:6px; align-items:flex-start; }
.ssid-row input { flex:1; }
.scan-wrap { position:relative; margin-top:4px; }
.scan-list {
display:none; border:1px solid var(--border);
border-radius:var(--r); background:var(--card);
max-height:220px; overflow-y:auto; z-index:100; position:relative;
}
.scan-list.show { display:block; }
.scan-item {
display:flex; align-items:center; justify-content:space-between;
padding:8px 12px; cursor:pointer; border-bottom:1px solid var(--border);
transition:background .12s; gap:8px;
}
.scan-item:last-child { border-bottom:none; }
.scan-item:hover { background:rgba(67,206,162,.08); }
.scan-ssid { font-size:.85em; color:var(--text); font-weight:500; }
.scan-meta { font-size:.74em; color:var(--muted); white-space:nowrap; }
.scan-msg { padding:12px; text-align:center; color:var(--muted); font-size:.82em; }
/* STA status boxes */
.sta-ok {
display:none; margin-top:9px; padding:9px 12px;
background:rgba(67,206,162,.08); border-radius:6px;
border:1px solid rgba(67,206,162,.2);
}
.sta-fail {
display:none; margin-top:9px; padding:9px 12px;
background:rgba(248,81,73,.06); border-radius:6px;
border:1px solid rgba(248,81,73,.2);
color:var(--red); font-size:.83em; font-weight:600;
}
</style>
</head>
<body>
<div id="ws-status" class="connection-status disconnected">○ WS</div>
<div class="header">
<h1> Settings</h1>
<p>WiFi </p>
</div>
<div class="nav">
<a href="/">📊 Monitor</a>
<a href="/transmit">📤 Transmit</a>
<a href="/graph">📈 Graph</a>
<a href="/graph-view">📊 Graph View</a>
<a href="/settings" class="active">⚙️ Settings</a>
<a href="/serial">📟 Serial1</a>
<a href="/serial2">📟 Serial2</a>
</div>
<div class="content">
<div id="alert-success" class="alert alert-success">
<span class="alert-icon">✓</span>
<span class="alert-text">설정이 저장되었습니다! 재부팅 후 적용됩니다.</span>
</div>
<div id="alert-loading" class="alert alert-info">
<span class="alert-icon">⏳</span>
<span class="alert-text">설정을 불러오는 중...</span>
</div>
<div id="alert-error" class="alert alert-warning">
<span class="alert-icon">⚠️</span>
<span class="alert-text"></span>
</div>
<div class="settings-section">
<div class="section-title">
<span>📶</span>
<span>WiFi Configuration</span>
</div>
<!-- AP Mode -->
<div class="sub-title ap">
<span>📡</span>
<span>AP Mode (Access Point)</span>
<span id="ap-badge" class="wbadge ap" style="margin-left:auto;">
<span class="wdot"></span> AP 활성
</span>
</div>
<div class="ip-row">
<span class="ip-lbl">AP IP</span>
<span class="ip-val" id="ap-ip-val">192.168.4.1</span>
<span class="ip-lbl" style="margin-left:12px;">클라이언트</span>
<span class="ip-val" id="ap-clients-val">—</span>
</div>
<div class="form-group" style="margin-top:12px;">
<label for="wifi-ssid">AP SSID ( )</label>
<input type="text" id="wifi-ssid" placeholder="Byun_CAN_Logger" maxlength="31">
<div class="help-text">ESP32가 생성할 WiFi 네트워크 이름 (최대 31자)</div>
</div>
<div class="form-group">
<label for="wifi-password">AP Password</label>
<input type="password" id="wifi-password" placeholder="최소 8자 이상 (빈칸=개방)" minlength="8" maxlength="63">
<div class="help-text">WiFi 접속 비밀번호 (8~63자, 빈칸=개방 네트워크)</div>
</div>
<hr style="margin:18px 0; border:none; border-top:1px solid var(--border);">
<!-- APSTA Mode -->
<div class="sub-title">
<span>🌐</span>
<span>APSTA Mode (AP + Station)</span>
<span id="sta-live-badge" class="wbadge off" style="margin-left:auto; display:none;">
<span class="wdot"></span>
<span id="sta-badge-text"></span>
</span>
</div>
<div class="checkbox-group">
<input type="checkbox" id="sta-enable" onchange="toggleSTA()">
<label for="sta-enable">Station AP를 WiFi에 </label>
</div>
<div id="sta-block" class="sta-block disabled">
<div class="form-group">
<label for="sta-ssid"> WiFi SSID</label>
<div class="ssid-row">
<input type="text" id="sta-ssid" placeholder="공유기 SSID 또는 스캔으로 선택" maxlength="31">
<button class="btn btn-primary btn-small" id="scan-btn" onclick="scanWifi()">🔍 스캔</button>
</div>
<div class="scan-wrap">
<div id="scan-list" class="scan-list"></div>
</div>
<div class="help-text">스캔 버튼으로 주변 네트워크를 검색하거나 직접 입력</div>
</div>
<div class="form-group">
<label for="sta-password"> WiFi Password</label>
<input type="password" id="sta-password" placeholder="공유기 비밀번호 (개방=빈칸)" maxlength="63">
<div class="help-text">외부 WiFi 비밀번호</div>
</div>
<!-- STA -->
<div id="sta-ok" class="sta-ok">
<div style="color:var(--accent); font-weight:700; font-size:.83em; margin-bottom:5px;"> Station </div>
<div class="ip-row" style="margin-top:0; background:transparent; border:none; padding:0; gap:6px;">
<span class="ip-lbl">STA IP</span>
<span class="ip-val" id="sta-ip">—</span>
</div>
</div>
<div id="sta-fail" class="sta-fail">
Station , 30
</div>
</div>
</div>
<div class="button-group">
<button class="btn btn-save" onclick="saveSettings()">💾 설정 저장</button>
<button class="btn btn-back" onclick="location.href='/'">← 모니터로</button>
</div>
<div class="info-box">
<div class="info-box-title"><span></span><span>중요 안내</span></div>
<div class="info-box-text">
WiFi ESP32 ( ).<br>
<strong>APSTA :</strong> AP(192.168.4.1) WiFi를 .<br>
Station AP , 30 .<br>
AP와 Station은 AP .
</div>
</div>
</div>
<script>
let ws;
let scanOpen = false;
/* ── WebSocket ─────────────────────────────── */
function initWS() {
const el = document.getElementById('ws-status');
ws = new WebSocket('ws://' + window.location.hostname + ':81/');
ws.onopen = function() {
el.className = 'connection-status connected';
el.textContent = ' WS';
loadSettings();
};
ws.onclose = function() {
el.className = 'connection-status disconnected';
el.textContent = ' WS';
showAlert('alert-loading', ' . ...', 'alert-warning');
setTimeout(initWS, 3000);
};
ws.onerror = function(e) { console.error('WS error', e); };
ws.onmessage = function(event) {
try {
const d = JSON.parse(event.data);
if (d.type === 'settings') {
applySettings(d);
hideAlert('alert-loading');
} else if (d.type === 'settingsSaved') {
if (d.success) showAlert('alert-success', ' ! .', 'alert-success');
} else if (d.type === 'update') {
updateWifiStatus(d.staConnected, d.staIP, d.apIP, d.apClients);
}
} catch(e) { console.error('Parse error', e); }
};
}
/* ── Settings apply ────────────────────────── */
function applySettings(d) {
document.getElementById('wifi-ssid').value = d.ssid || 'Byun_CAN_Logger';
document.getElementById('wifi-password').value = d.password || '';
document.getElementById('sta-enable').checked = d.staEnable || false;
document.getElementById('sta-ssid').value = d.staSSID || '';
document.getElementById('sta-password').value = d.staPassword || '';
toggleSTA();
updateWifiStatus(d.staConnected, d.staIP, d.apIP, d.apClients);
}
/* ── WiFi status UI ────────────────────────── */
function updateWifiStatus(staConn, staIP, apIP, apClients) {
if (apIP && apIP !== '0.0.0.0') {
document.getElementById('ap-ip-val').textContent = apIP;
}
if (apClients !== undefined) {
document.getElementById('ap-clients-val').textContent = apClients + '';
}
const staEnable = document.getElementById('sta-enable').checked;
const badge = document.getElementById('sta-live-badge');
const btext = document.getElementById('sta-badge-text');
const okBox = document.getElementById('sta-ok');
const failBox= document.getElementById('sta-fail');
const staIpEl= document.getElementById('sta-ip');
if (staEnable) {
badge.style.display = 'inline-flex';
if (staConn && staIP && staIP !== '0.0.0.0') {
badge.className = 'wbadge on';
btext.textContent = staIP;
okBox.style.display = 'block';
failBox.style.display = 'none';
staIpEl.textContent = staIP;
} else {
badge.className = 'wbadge off';
btext.textContent = '';
okBox.style.display = 'none';
failBox.style.display = 'block';
}
} else {
badge.style.display = 'none';
okBox.style.display = 'none';
failBox.style.display = 'none';
}
}
/* ── Load / Save ───────────────────────────── */
function loadSettings() {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({cmd:'getSettings'}));
showAlert('alert-loading', ' ...', 'alert-info');
}
}
function saveSettings() {
const ssid = document.getElementById('wifi-ssid').value.trim();
const password = document.getElementById('wifi-password').value;
const staEnable = document.getElementById('sta-enable').checked;
const staSSID = document.getElementById('sta-ssid').value.trim();
const staPass = document.getElementById('sta-password').value;
if (!ssid) { showErr('AP SSID를 .'); return; }
if (ssid.length > 31) { showErr('AP SSID는 31.'); return; }
if (password.length > 0 && password.length < 8) { showErr('AP 8.'); return; }
if (password.length > 63) { showErr('AP 63.'); return; }
if (staEnable) {
if (!staSSID) { showErr('Station SSID를 .'); return; }
if (staSSID.length > 31) { showErr('Station SSID는 31.'); return; }
if (staPass.length > 0 && staPass.length < 8) { showErr('Station 8.'); return; }
}
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
cmd:'saveSettings', ssid, password,
staEnable, staSSID, staPassword:staPass
}));
} else {
showErr('WebSocket . .');
}
}
/* ── WiFi Scan ─────────────────────────────── */
function scanWifi() {
const btn = document.getElementById('scan-btn');
const list = document.getElementById('scan-list');
btn.textContent = ' ...';
btn.disabled = true;
list.innerHTML = '<div class="scan-msg">📡 주변 WiFi 검색 중 (3~5초)...</div>';
list.classList.add('show');
scanOpen = true;
fetch('/wifi-scan')
.then(r => r.json())
.then(data => {
btn.textContent = '🔍 ';
btn.disabled = false;
const nets = (data.networks || []).sort((a,b) => b.rssi - a.rssi);
if (nets.length === 0) {
list.innerHTML = '<div class="scan-msg">검색된 네트워크 없음</div>';
return;
}
list.innerHTML = nets.map(n => {
const bars = n.rssi >= -60 ? '' :
n.rssi >= -75 ? '·' :
n.rssi >= -85 ? '··' : '···';
const lock = n.enc ? '🔒' : '🔓';
const ch = n.channel ? ' ch' + n.channel : '';
const ssid = n.ssid || '( )';
return '<div class="scan-item" onclick="selectSSID(\'' +
ssid.replace(/\\/g,'\\\\').replace(/'/g,"\\'") + '\')">' +
'<span class="scan-ssid">' + ssid + '</span>' +
'<span class="scan-meta">' + lock + ' ' + bars + ' ' + n.rssi + 'dBm' + ch + '</span>' +
'</div>';
}).join('');
})
.catch(err => {
btn.textContent = '🔍 ';
btn.disabled = false;
list.innerHTML = '<div class="scan-msg">스캔 실패 — 다시 시도하세요</div>';
console.error('Scan error:', err);
});
}
function selectSSID(ssid) {
document.getElementById('sta-ssid').value = ssid;
document.getElementById('scan-list').classList.remove('show');
document.getElementById('sta-password').focus();
scanOpen = false;
}
document.addEventListener('click', function(e) {
if (scanOpen &&
!e.target.closest('.scan-wrap') &&
!e.target.closest('#scan-btn')) {
document.getElementById('scan-list').classList.remove('show');
scanOpen = false;
}
});
/* ── STA toggle ────────────────────────────── */
function toggleSTA() {
const en = document.getElementById('sta-enable').checked;
const blk = document.getElementById('sta-block');
const bdg = document.getElementById('sta-live-badge');
blk.classList.toggle('disabled', !en);
bdg.style.display = en ? 'inline-flex' : 'none';
if (!en) {
document.getElementById('sta-ok').style.display = 'none';
document.getElementById('sta-fail').style.display = 'none';
}
}
/* ── Alert helpers ─────────────────────────── */
function showAlert(id, msg, cls) {
const el = document.getElementById(id);
if (!el) return;
const t = el.querySelector('.alert-text');
if (t && msg) t.textContent = msg;
el.className = 'alert ' + cls + ' show';
if (cls === 'alert-success') setTimeout(() => hideAlert(id), 5000);
}
function hideAlert(id) {
const el = document.getElementById(id);
if (el) el.classList.remove('show');
}
function showErr(msg) { showAlert('alert-error', msg, 'alert-warning'); }
window.addEventListener('load', initWS);
</script>
</body>
</html>
)rawliteral";
#endif