현재 고쳐햐 할 점으로 can 중지 시 que가 적산됨 2025년 11월 6일 오전 3:30 GMT+9 작성 내용 기반으로 esp32 로 mcp2515, sdcard 를 spi 로 연결하여 can 최대 속도를 빠짐 없이 실시간 로깅하는 코드야 RTC DS3231로 i2c 연결(softwire 라이브러리)하여 esp32의 시간데이터를 보정하는 기능으로 보다 신뢰성 있는 로깅을 해주지, wifi ap로 연결하면 monitoring페이지가 있고 모니터링 페이지에 초기 접속 시(로깅하지 않은상태) 핸드폰의 시간을 rtc에 저장 하고 시스템 시간을 핸드폰시간으로 맞춰줘 그리고 log files 항목에 파일 리스트를 보여주는데 사용자가 커멘트입력하여 차후 어떤 파일인지 알 수 있게 해줘 또한 컨트롤 패널 항목에 추가로 MCP2515의 CAN 모드를 넣어 MCP2515컨트롤 loop-back, normal, listen-only모드 등을 넣어 사용자가 상황에 맞게 mcp2515를 동작시키게 해줘, settings 페이지에서는 timezone은 불필요한것 같아 삭제 해줘, 이 요구사항을 우선 첨부한 코드를 분석하고 해당 요구사항을 적용하여 수정해줘, 참고로 transmit 페이지는 파일용량이 커서 첨부에 뺐으니 빼고 수정해줘(수정과 상관 없는 graph.h, graph_viewer.h,transmit.h 는 첨부에 뺐음)
434 lines
14 KiB
C++
434 lines
14 KiB
C++
#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>
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
body {
|
||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
min-height: 100vh;
|
||
padding: 10px;
|
||
}
|
||
.container {
|
||
max-width: 800px;
|
||
margin: 0 auto;
|
||
background: white;
|
||
border-radius: 15px;
|
||
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||
overflow: hidden;
|
||
}
|
||
.header {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
padding: 20px;
|
||
text-align: center;
|
||
}
|
||
.header h1 { font-size: 1.8em; margin-bottom: 5px; }
|
||
.header p { opacity: 0.9; font-size: 0.9em; }
|
||
.nav {
|
||
background: #2c3e50;
|
||
padding: 10px;
|
||
display: flex;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
justify-content: center;
|
||
}
|
||
.nav a {
|
||
color: white;
|
||
text-decoration: none;
|
||
padding: 10px 15px;
|
||
border-radius: 5px;
|
||
transition: all 0.3s;
|
||
font-size: 0.9em;
|
||
white-space: nowrap;
|
||
}
|
||
.nav a:hover { background: #34495e; }
|
||
.nav a.active { background: #3498db; }
|
||
.content { padding: 30px; }
|
||
|
||
.settings-section {
|
||
background: #f8f9fa;
|
||
padding: 25px;
|
||
border-radius: 10px;
|
||
margin-bottom: 20px;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
||
}
|
||
|
||
.section-title {
|
||
color: #333;
|
||
font-size: 1.3em;
|
||
margin-bottom: 20px;
|
||
padding-bottom: 10px;
|
||
border-bottom: 3px solid #667eea;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.form-group:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
label {
|
||
display: block;
|
||
font-weight: 600;
|
||
color: #333;
|
||
margin-bottom: 8px;
|
||
font-size: 0.95em;
|
||
}
|
||
|
||
.help-text {
|
||
font-size: 0.85em;
|
||
color: #666;
|
||
margin-top: 5px;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
input[type="text"],
|
||
input[type="password"] {
|
||
width: 100%;
|
||
padding: 12px 15px;
|
||
border: 2px solid #ddd;
|
||
border-radius: 8px;
|
||
font-size: 1em;
|
||
transition: all 0.3s;
|
||
font-family: inherit;
|
||
}
|
||
|
||
input[type="text"]:focus,
|
||
input[type="password"]:focus {
|
||
outline: none;
|
||
border-color: #667eea;
|
||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||
}
|
||
|
||
.button-group {
|
||
display: flex;
|
||
gap: 15px;
|
||
margin-top: 30px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
button {
|
||
flex: 1;
|
||
min-width: 150px;
|
||
padding: 14px 28px;
|
||
border: none;
|
||
border-radius: 8px;
|
||
font-size: 1em;
|
||
font-weight: 700;
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
|
||
.btn-save {
|
||
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
|
||
color: white;
|
||
box-shadow: 0 4px 15px rgba(17, 153, 142, 0.3);
|
||
}
|
||
|
||
.btn-save:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 6px 20px rgba(17, 153, 142, 0.4);
|
||
}
|
||
|
||
.btn-cancel {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
|
||
}
|
||
|
||
.btn-cancel:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
|
||
}
|
||
|
||
button:active {
|
||
transform: translateY(0);
|
||
}
|
||
|
||
.alert {
|
||
padding: 15px 20px;
|
||
border-radius: 8px;
|
||
margin-bottom: 20px;
|
||
display: none;
|
||
align-items: center;
|
||
gap: 12px;
|
||
animation: slideDown 0.3s ease-out;
|
||
}
|
||
|
||
@keyframes slideDown {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(-10px);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
.alert.show {
|
||
display: flex;
|
||
}
|
||
|
||
.alert-success {
|
||
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
|
||
color: white;
|
||
}
|
||
|
||
.alert-info {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
}
|
||
|
||
.alert-warning {
|
||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||
color: white;
|
||
}
|
||
|
||
.alert-icon {
|
||
font-size: 1.5em;
|
||
}
|
||
|
||
.alert-text {
|
||
flex: 1;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.info-box {
|
||
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
|
||
border-left: 4px solid #667eea;
|
||
padding: 15px 20px;
|
||
border-radius: 8px;
|
||
margin-top: 15px;
|
||
}
|
||
|
||
.info-box-title {
|
||
font-weight: 700;
|
||
color: #333;
|
||
margin-bottom: 8px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.info-box-text {
|
||
color: #555;
|
||
font-size: 0.9em;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.content { padding: 20px; }
|
||
.settings-section { padding: 20px; }
|
||
.section-title { font-size: 1.1em; }
|
||
button { min-width: 100%; }
|
||
.button-group { flex-direction: column; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div class="header">
|
||
<h1>⚙️ Settings</h1>
|
||
<p>Configure WiFi and System Settings</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>
|
||
</div>
|
||
|
||
<div class="content">
|
||
<div id="alert-success" class="alert alert-success">
|
||
<span class="alert-icon">✓</span>
|
||
<span class="alert-text">Settings saved successfully!</span>
|
||
</div>
|
||
|
||
<div id="alert-loading" class="alert alert-info">
|
||
<span class="alert-icon">⏳</span>
|
||
<span class="alert-text">Loading settings...</span>
|
||
</div>
|
||
|
||
<div class="settings-section">
|
||
<div class="section-title">
|
||
<span>📶</span>
|
||
<span>WiFi Configuration</span>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="wifi-ssid">WiFi 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">WiFi Password (비밀번호)</label>
|
||
<input type="password" id="wifi-password" placeholder="최소 8자 이상" minlength="8" maxlength="63">
|
||
<div class="help-text">WiFi 접속 시 필요한 비밀번호입니다 (8-63자)</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="button-group">
|
||
<button class="btn-save" onclick="saveSettings()">💾 Save Settings</button>
|
||
<button class="btn-cancel" onclick="location.href='/'">← Back to Monitor</button>
|
||
</div>
|
||
|
||
<div class="info-box" style="margin-top: 30px;">
|
||
<div class="info-box-title">
|
||
<span>⚠️</span>
|
||
<span>중요 안내</span>
|
||
</div>
|
||
<div class="info-box-text">
|
||
• WiFi 설정을 변경한 경우, ESP32를 재부팅해야 새 SSID/비밀번호가 적용됩니다.<br>
|
||
• 시간 설정은 모니터 페이지에서 "Sync from Phone" 버튼을 눌러 핸드폰 시간과 동기화할 수 있습니다.<br>
|
||
• RTC 모듈이 연결된 경우, 시간이 자동으로 보정됩니다.<br>
|
||
• 설정 저장 후 ESP32의 리셋 버튼을 눌러 재부팅하세요.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
let ws;
|
||
|
||
function initWebSocket() {
|
||
ws = new WebSocket('ws://' + window.location.hostname + ':81/');
|
||
|
||
ws.onopen = function() {
|
||
console.log('WebSocket connected');
|
||
loadSettings();
|
||
};
|
||
|
||
ws.onclose = function() {
|
||
console.log('WebSocket disconnected');
|
||
showAlert('alert-loading', '연결 끊김. 재연결 시도 중...', 'alert-warning');
|
||
setTimeout(initWebSocket, 3000);
|
||
};
|
||
|
||
ws.onerror = function(error) {
|
||
console.error('WebSocket error:', error);
|
||
};
|
||
|
||
ws.onmessage = function(event) {
|
||
try {
|
||
const data = JSON.parse(event.data);
|
||
|
||
if (data.type === 'settings') {
|
||
document.getElementById('wifi-ssid').value = data.ssid || 'Byun_CAN_Logger';
|
||
document.getElementById('wifi-password').value = data.password || '';
|
||
|
||
hideAlert('alert-loading');
|
||
console.log('Settings loaded:', data);
|
||
} else if (data.type === 'settingsSaved') {
|
||
if (data.success) {
|
||
showAlert('alert-success', '설정이 저장되었습니다! 재부팅 후 적용됩니다.', 'alert-success');
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.error('Parse error:', e);
|
||
}
|
||
};
|
||
}
|
||
|
||
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;
|
||
|
||
// 입력 검증
|
||
if (ssid.length === 0) {
|
||
alert('WiFi SSID를 입력하세요.');
|
||
return;
|
||
}
|
||
|
||
if (ssid.length > 31) {
|
||
alert('WiFi SSID는 최대 31자까지 입력 가능합니다.');
|
||
return;
|
||
}
|
||
|
||
if (password.length > 0 && password.length < 8) {
|
||
alert('WiFi 비밀번호는 최소 8자 이상이어야 합니다.');
|
||
return;
|
||
}
|
||
|
||
if (password.length > 63) {
|
||
alert('WiFi 비밀번호는 최대 63자까지 입력 가능합니다.');
|
||
return;
|
||
}
|
||
|
||
const settings = {
|
||
cmd: 'saveSettings',
|
||
ssid: ssid,
|
||
password: password
|
||
};
|
||
|
||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||
ws.send(JSON.stringify(settings));
|
||
console.log('Settings saved:', settings);
|
||
} else {
|
||
alert('WebSocket 연결이 끊겼습니다. 페이지를 새로고침하세요.');
|
||
}
|
||
}
|
||
|
||
function showAlert(alertId, message, className) {
|
||
const alert = document.getElementById(alertId);
|
||
if (alert) {
|
||
const textElement = alert.querySelector('.alert-text');
|
||
if (textElement && message) {
|
||
textElement.textContent = message;
|
||
}
|
||
|
||
// 기존 클래스 제거
|
||
alert.className = 'alert ' + className;
|
||
alert.classList.add('show');
|
||
|
||
// 3초 후 자동 숨김 (success 알림만)
|
||
if (className === 'alert-success') {
|
||
setTimeout(() => {
|
||
hideAlert(alertId);
|
||
}, 5000);
|
||
}
|
||
}
|
||
}
|
||
|
||
function hideAlert(alertId) {
|
||
const alert = document.getElementById(alertId);
|
||
if (alert) {
|
||
alert.classList.remove('show');
|
||
}
|
||
}
|
||
|
||
// 페이지 로드 시 WebSocket 연결
|
||
window.addEventListener('load', function() {
|
||
initWebSocket();
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|
||
)rawliteral";
|
||
|
||
#endif |