자동로깅

This commit is contained in:
2026-01-02 21:37:48 +00:00
parent 8994fe8e52
commit 1963d8d423
2 changed files with 929 additions and 11 deletions

466
index.h
View File

@@ -675,6 +675,158 @@ const char index_html[] PROGMEM = R"rawliteral(
padding: 30px;
}
}
/* 🎯 Auto Trigger 스타일 */
.auto-trigger-section {
background: white;
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.auto-trigger-section h3 {
margin-top: 0;
margin-bottom: 15px;
color: #667eea;
font-size: 1.3em;
}
.trigger-control {
margin-bottom: 20px;
padding: 10px;
background: #f8f9fa;
border-radius: 8px;
}
.trigger-control label {
font-size: 1.1em;
font-weight: 500;
cursor: pointer;
}
.trigger-group {
margin-bottom: 25px;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
border-left: 4px solid #667eea;
}
.trigger-group h4 {
margin-top: 0;
margin-bottom: 10px;
color: #333;
font-size: 1.1em;
}
.trigger-group select {
margin-bottom: 15px;
padding: 8px;
border-radius: 6px;
border: 1px solid #ddd;
}
.trigger-card {
background: white;
padding: 12px;
margin-bottom: 10px;
border-radius: 8px;
border: 1px solid #e0e0e0;
display: flex;
gap: 8px;
align-items: center;
flex-wrap: wrap;
}
.trigger-card input[type="text"],
.trigger-card input[type="number"] {
padding: 6px 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 0.9em;
}
.trigger-card input[type="text"] {
width: 100px;
}
.trigger-card input[type="number"] {
width: 80px;
}
.trigger-card select {
padding: 6px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 0.9em;
}
.trigger-card .btn-delete {
background: #dc3545;
color: white;
border: none;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 0.9em;
}
.trigger-card .btn-delete:hover {
background: #c82333;
}
.btn-add-trigger {
background: #28a745;
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
font-size: 1em;
margin-top: 10px;
}
.btn-add-trigger:hover {
background: #218838;
}
.btn-save-triggers {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 12px 30px;
border-radius: 8px;
cursor: pointer;
font-size: 1.1em;
width: 100%;
margin-top: 15px;
}
.btn-save-triggers:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.trigger-status {
display: inline-block;
padding: 4px 12px;
border-radius: 12px;
font-size: 0.85em;
font-weight: 500;
margin-left: 10px;
}
.trigger-status.active {
background: #d4edda;
color: #155724;
}
.trigger-status.inactive {
background: #f8d7da;
color: #721c24;
}
</style>
</head>
<body>
@@ -829,6 +981,66 @@ const char index_html[] PROGMEM = R"rawliteral(
</div>
</div>
<!-- 🎯 Auto Trigger -->
<div class="auto-trigger-section">
<h3>
🎯 Auto Trigger
<span id="autoTriggerStatus" class="trigger-status inactive">Disabled</span>
</h3>
<!-- Enable/Disable -->
<div class="trigger-control">
<label>
<input type="checkbox" id="autoTriggerEnabled" onchange="toggleAutoTrigger()">
Enable Auto Trigger
</label>
</div>
<!-- 🆕 Auto Trigger -->
<div class="trigger-group" id="autoTriggerFormatGroup" style="display: none;">
<h4>📁 Log Format</h4>
<select id="autoTriggerFormat" style="width: 100%; padding: 8px; border-radius: 5px; border: 1px solid #444; background: #2a2a2a; color: white;">
<option value="bin">📦 Binary (BIN) - High Speed</option>
<option value="csv">📄 CSV - Human Readable</option>
</select>
</div>
<!-- -->
<div class="trigger-group" id="startTriggerGroup" style="display: none;">
<h4> Start Logging Conditions</h4>
<select id="startLogic">
<option value="OR">OR (Any condition matches)</option>
<option value="AND">AND (All conditions match)</option>
</select>
<div id="startTriggersList">
<!-- -->
</div>
<button class="btn-add-trigger" onclick="addStartTrigger()"> Add Start Condition</button>
</div>
<!-- -->
<div class="trigger-group" id="stopTriggerGroup" style="display: none;">
<h4> Stop Logging Conditions</h4>
<select id="stopLogic">
<option value="OR">OR (Any condition matches)</option>
<option value="AND">AND (All conditions match)</option>
</select>
<div id="stopTriggersList">
<!-- -->
</div>
<button class="btn-add-trigger" onclick="addStopTrigger()"> Add Stop Condition</button>
</div>
<button class="btn-save-triggers" onclick="saveAutoTriggers()" style="display: none;" id="saveTriggerBtn">
💾 Save Auto Trigger Settings
</button>
</div>
<h2>CAN Messages (by ID)</h2>
<div class="can-table-container">
<table>
@@ -886,6 +1098,10 @@ const char index_html[] PROGMEM = R"rawliteral(
let commentingFile = '';
// hasInitialSync 제거 - 매번 자동 동기화
// 🎯 Auto Trigger 전역 변수
let startTriggers = [];
let stopTriggers = [];
function updateCurrentTime() {
const now = new Date();
const timeStr = now.toLocaleTimeString('ko-KR', {hour12: false});
@@ -954,7 +1170,11 @@ const char index_html[] PROGMEM = R"rawliteral(
setTimeout(function() {
refreshFiles();
}, 1000);
};
// 🎯 Auto Trigger 설정 로드
setTimeout(loadAutoTriggers, 500);
};
ws.onclose = function() {
console.log('WebSocket disconnected');
@@ -968,25 +1188,39 @@ const char index_html[] PROGMEM = R"rawliteral(
};
ws.onmessage = function(event) {
// 🆕 바이너리/비정상 데이터 필터링
if (typeof event.data !== 'string') {
console.warn('Non-string WebSocket data received, ignoring');
return;
}
// 🆕 빈 데이터 필터링
if (!event.data || event.data.trim() === '') {
return;
}
try {
const data = JSON.parse(event.data);
// ⭐ 디버그: 받은 메시지 타입 출력
// 🆕 유효한 JSON 객체인지 확인
if (!data || typeof data !== 'object') {
console.warn('Invalid JSON object received');
return;
}
console.log('WebSocket message received:', data.type);
// ★ 수정: 'update' 타입 추가 (서버에서 보내는 타입과 일치)
if (data.type === 'status' || data.type === 'update') {
updateStatus(data);
// CAN 메시지 배열이 있으면 처리
if (data.messages && data.messages.length > 0) {
updateCanMessages(data.messages);
}
} else if (data.type === 'canBatch') {
updateCanBatch(data.messages);
} else if (data.type === 'files') {
console.log('Files received:', data.files ? data.files.length : 0, 'files'); // ⭐ 디버그
console.log('Files data:', data.files); // ⭐ 디버그
updateFileList(data.files || data.list); // ★ 수정: 'list' 키도 확인
console.log('Files received:', data.files ? data.files.length : 0, 'files');
console.log('Files data:', data.files);
updateFileList(data.files || data.list);
} else if (data.type === 'deleteResult') {
handleDeleteResult(data);
} else if (data.type === 'timeSyncResult') {
@@ -999,14 +1233,62 @@ const char index_html[] PROGMEM = R"rawliteral(
}
} else if (data.type === 'hwReset') {
console.log(' - ESP32 ...');
// ESP32가 재부팅되므로 WebSocket 연결 끊김
}
// 🎯 Auto Trigger 메시지 처리 - try 블록 내부로 이동!
else if (data.type === 'autoTriggers') {
document.getElementById('autoTriggerEnabled').checked = data.enabled;
document.getElementById('startLogic').value = data.startLogic;
document.getElementById('stopLogic').value = data.stopLogic;
// 🆕 로그 형식 설정
if (data.logFormat) {
document.getElementById('autoTriggerFormat').value = data.logFormat;
}
startTriggers = data.startTriggers || [];
stopTriggers = data.stopTriggers || [];
if (data.enabled) {
document.getElementById('autoTriggerFormatGroup').style.display = 'block';
document.getElementById('startTriggerGroup').style.display = 'block';
document.getElementById('stopTriggerGroup').style.display = 'block';
document.getElementById('saveTriggerBtn').style.display = 'block';
document.getElementById('autoTriggerStatus').textContent = 'Enabled';
document.getElementById('autoTriggerStatus').className = 'trigger-status active';
}
renderStartTriggers();
renderStopTriggers();
}
else if (data.type === 'startTriggersSet') {
console.log(' Start triggers saved:', data.count);
}
else if (data.type === 'stopTriggersSet') {
console.log(' Stop triggers saved:', data.count);
}
// Auto Trigger 상태 업데이트 (update 메시지에서)
if (data.autoTriggerEnabled !== undefined) {
const status = document.getElementById('autoTriggerStatus');
if (data.autoTriggerActive) {
status.textContent = 'Active (Triggered)';
status.className = 'trigger-status active';
} else if (data.autoTriggerEnabled) {
status.textContent = 'Enabled (Waiting)';
status.className = 'trigger-status active';
} else {
status.textContent = 'Disabled';
status.className = 'trigger-status inactive';
}
}
} catch (e) {
console.error('Parse error:', e);
}
};
}
}
function updateStatus(data) {
// 로깅 상태
const loggingCard = document.getElementById('logging-status');
@@ -1588,6 +1870,172 @@ const char index_html[] PROGMEM = R"rawliteral(
});
initWebSocket();
// ============================================
// 🎯 Auto Trigger 관련 함수
// ============================================
// Auto Trigger 토글
function toggleAutoTrigger() {
const enabled = document.getElementById('autoTriggerEnabled').checked;
const formatGroup = document.getElementById('autoTriggerFormatGroup');
const startGroup = document.getElementById('startTriggerGroup');
const stopGroup = document.getElementById('stopTriggerGroup');
const saveBtn = document.getElementById('saveTriggerBtn');
const status = document.getElementById('autoTriggerStatus');
if (enabled) {
formatGroup.style.display = 'block';
startGroup.style.display = 'block';
stopGroup.style.display = 'block';
saveBtn.style.display = 'block';
status.textContent = 'Enabled';
status.className = 'trigger-status active';
if (startTriggers.length === 0) addStartTrigger();
if (stopTriggers.length === 0) addStopTrigger();
} else {
formatGroup.style.display = 'none';
startGroup.style.display = 'none';
stopGroup.style.display = 'none';
saveBtn.style.display = 'none';
status.textContent = 'Disabled';
status.className = 'trigger-status inactive';
}
const logFormat = document.getElementById('autoTriggerFormat').value;
ws.send(JSON.stringify({ cmd: 'setAutoTrigger', enabled: enabled, logFormat: logFormat }));
}
function addStartTrigger() {
const trigger = {
canId: '0x100',
startBit: 0,
bitLength: 8,
op: '==',
value: 0,
enabled: true
};
startTriggers.push(trigger);
renderStartTriggers();
}
function addStopTrigger() {
const trigger = {
canId: '0x100',
startBit: 0,
bitLength: 8,
op: '==',
value: 0,
enabled: true
};
stopTriggers.push(trigger);
renderStopTriggers();
}
function renderStartTriggers() {
const container = document.getElementById('startTriggersList');
container.innerHTML = '';
startTriggers.forEach((trigger, index) => {
const card = document.createElement('div');
card.className = 'trigger-card';
card.innerHTML = `
<label>
<input type="checkbox" ${trigger.enabled ? 'checked' : ''}
onchange="startTriggers[${index}].enabled = this.checked">
Enable
</label>
<input type="text" placeholder="CAN ID (0x100)" value="${trigger.canId}"
onchange="startTriggers[${index}].canId = this.value">
<input type="number" placeholder="Start Bit" min="0" max="63" value="${trigger.startBit}"
onchange="startTriggers[${index}].startBit = parseInt(this.value)">
<input type="number" placeholder="Bit Length" min="1" max="64" value="${trigger.bitLength}"
onchange="startTriggers[${index}].bitLength = parseInt(this.value)">
<select onchange="startTriggers[${index}].op = this.value">
<option value="==" ${trigger.op === '==' ? 'selected' : ''}> == </option>
<option value="!=" ${trigger.op === '!=' ? 'selected' : ''}> != </option>
<option value=">" ${trigger.op === '>' ? 'selected' : ''}> &gt; </option>
<option value="<" ${trigger.op === '<' ? 'selected' : ''}> &lt; </option>
<option value=">=" ${trigger.op === '>=' ? 'selected' : ''}> &gt;= </option>
<option value="<=" ${trigger.op === '<=' ? 'selected' : ''}> &lt;= </option>
</select>
<input type="number" placeholder="Value" value="${trigger.value}"
onchange="startTriggers[${index}].value = parseInt(this.value)">
<button class="btn-delete" onclick="removeStartTrigger(${index})">🗑</button>
`;
container.appendChild(card);
});
}
function renderStopTriggers() {
const container = document.getElementById('stopTriggersList');
container.innerHTML = '';
stopTriggers.forEach((trigger, index) => {
const card = document.createElement('div');
card.className = 'trigger-card';
card.innerHTML = `
<label>
<input type="checkbox" ${trigger.enabled ? 'checked' : ''}
onchange="stopTriggers[${index}].enabled = this.checked">
Enable
</label>
<input type="text" placeholder="CAN ID (0x100)" value="${trigger.canId}"
onchange="stopTriggers[${index}].canId = this.value">
<input type="number" placeholder="Start Bit" min="0" max="63" value="${trigger.startBit}"
onchange="stopTriggers[${index}].startBit = parseInt(this.value)">
<input type="number" placeholder="Bit Length" min="1" max="64" value="${trigger.bitLength}"
onchange="stopTriggers[${index}].bitLength = parseInt(this.value)">
<select onchange="stopTriggers[${index}].op = this.value">
<option value="==" ${trigger.op === '==' ? 'selected' : ''}> == </option>
<option value="!=" ${trigger.op === '!=' ? 'selected' : ''}> != </option>
<option value=">" ${trigger.op === '>' ? 'selected' : ''}> &gt; </option>
<option value="<" ${trigger.op === '<' ? 'selected' : ''}> &lt; </option>
<option value=">=" ${trigger.op === '>=' ? 'selected' : ''}> &gt;= </option>
<option value="<=" ${trigger.op === '<=' ? 'selected' : ''}> &lt;= </option>
</select>
<input type="number" placeholder="Value" value="${trigger.value}"
onchange="stopTriggers[${index}].value = parseInt(this.value)">
<button class="btn-delete" onclick="removeStopTrigger(${index})">🗑</button>
`;
container.appendChild(card);
});
}
function removeStartTrigger(index) {
if (confirm(' ?')) {
startTriggers.splice(index, 1);
renderStartTriggers();
}
}
function removeStopTrigger(index) {
if (confirm(' ?')) {
stopTriggers.splice(index, 1);
renderStopTriggers();
}
}
function saveAutoTriggers() {
const startLogic = document.getElementById('startLogic').value;
const stopLogic = document.getElementById('stopLogic').value;
const logFormat = document.getElementById('autoTriggerFormat').value;
// 로그 형식 설정 먼저 전송
ws.send(JSON.stringify({ cmd: 'setAutoTriggerFormat', logFormat: logFormat }));
ws.send(JSON.stringify({ cmd: 'setStartTriggers', logic: startLogic, triggers: startTriggers }));
ws.send(JSON.stringify({ cmd: 'setStopTriggers', logic: stopLogic, triggers: stopTriggers }));
alert(' Auto Trigger !');
}
function loadAutoTriggers() {
ws.send(JSON.stringify({ cmd: 'getAutoTriggers' }));
}
</script>
</body>
</html>