파일 커멘트 추가, listen-only모드, tranmit에만 normal모드

This commit is contained in:
2025-11-06 16:59:44 +00:00
parent 2ee1ad905e
commit d970f53186
2 changed files with 668 additions and 28 deletions

364
index.h
View File

@@ -251,6 +251,78 @@ const char index_html[] PROGMEM = R"rawliteral(
.status-card .value { font-size: 1.5em; font-weight: bold; word-break: break-all; }
.status-on { background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%) !important; }
.status-off { background: linear-gradient(135deg, #eb3349 0%, #f45c43 100%) !important; }
/* SD 카드 용량 표시 */
.sd-capacity {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
color: white;
padding: 10px 15px;
border-radius: 8px;
margin-bottom: 15px;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 3px 10px rgba(79, 172, 254, 0.3);
flex-wrap: wrap;
gap: 10px;
}
.sd-capacity-label {
font-size: 0.85em;
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
}
.sd-capacity-values {
display: flex;
gap: 15px;
align-items: center;
flex-wrap: wrap;
}
.sd-capacity-item {
display: flex;
flex-direction: column;
align-items: flex-end;
}
.sd-capacity-item-label {
font-size: 0.7em;
opacity: 0.9;
}
.sd-capacity-value {
font-family: 'Courier New', monospace;
font-size: 1.2em;
font-weight: 700;
}
/* 파일 선택 체크박스 */
.file-checkbox {
width: 20px;
height: 20px;
cursor: pointer;
flex-shrink: 0;
}
.file-selection-controls {
background: #f8f9fa;
padding: 12px;
border-radius: 8px;
margin-bottom: 10px;
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
.file-selection-controls button {
padding: 8px 16px;
font-size: 0.85em;
}
.selection-info {
flex: 1;
min-width: 150px;
color: #666;
font-weight: 600;
font-size: 0.9em;
}
.control-panel {
background: #f8f9fa;
padding: 15px;
@@ -383,6 +455,38 @@ const char index_html[] PROGMEM = R"rawliteral(
font-size: 0.85em;
font-weight: 600;
}
.file-comment {
color: #888;
font-size: 0.8em;
font-style: italic;
margin-top: 2px;
cursor: pointer;
transition: color 0.3s;
}
.file-comment:hover {
color: #667eea;
}
.file-comment.empty {
color: #ccc;
}
.comment-input {
width: 100%;
padding: 6px;
border: 2px solid #667eea;
border-radius: 4px;
font-size: 0.85em;
margin-top: 4px;
font-family: inherit;
}
.comment-actions {
display: flex;
gap: 6px;
margin-top: 6px;
}
.comment-actions button {
padding: 4px 12px;
font-size: 0.8em;
}
.file-actions {
display: flex;
gap: 8px;
@@ -448,8 +552,8 @@ const char index_html[] PROGMEM = R"rawliteral(
<body>
<div class="container">
<div class="header">
<h1>🚗 Byun CAN Logger v1.5</h1>
<p>Real-time CAN Bus Monitor & Logger + File Management</p>
<h1>🚗 Byun CAN Logger v1.6</h1>
<p>Listen-Only Mode - No CAN Bus Impact (RX Only)</p>
</div>
<div class="nav">
@@ -503,6 +607,27 @@ const char index_html[] PROGMEM = R"rawliteral(
</div>
</div>
<div class="sd-capacity" id="sd-capacity">
<div class="sd-capacity-label">
<span>💾</span>
<span>SD CARD CAPACITY</span>
</div>
<div class="sd-capacity-values">
<div class="sd-capacity-item">
<div class="sd-capacity-item-label">TOTAL</div>
<div class="sd-capacity-value" id="sd-total">0 MB</div>
</div>
<div class="sd-capacity-item">
<div class="sd-capacity-item-label">USED</div>
<div class="sd-capacity-value" id="sd-used">0 MB</div>
</div>
<div class="sd-capacity-item">
<div class="sd-capacity-item-label">FREE</div>
<div class="sd-capacity-value" id="sd-free">0 MB</div>
</div>
</div>
</div>
<div class="status-grid">
<div class="status-card status-off" id="logging-status">
<h3>LOGGING</h3>
@@ -572,6 +697,15 @@ const char index_html[] PROGMEM = R"rawliteral(
</div>
<h2>Log Files</h2>
<div class="file-selection-controls">
<button onclick="selectAllFiles()">Select All</button>
<button onclick="deselectAllFiles()">Deselect All</button>
<button onclick="downloadSelectedFiles()" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">Download Selected</button>
<button onclick="deleteSelectedFiles()" style="background: linear-gradient(135deg, #eb3349 0%, #f45c43 100%);">Delete Selected</button>
<div class="selection-info">
<span id="selection-count">0 files selected</span>
</div>
</div>
<div class="file-list" id="file-list">
<p style="text-align: center; color: #666; font-size: 0.9em;">Loading...</p>
</div>
@@ -585,6 +719,166 @@ const char index_html[] PROGMEM = R"rawliteral(
let lastMessageData = {};
const speedNames = {0: '125K', 1: '250K', 2: '500K', 3: '1M'};
let currentLoggingFile = '';
let selectedFiles = new Set();
function updateSelectionCount() {
document.getElementById('selection-count').textContent = selectedFiles.size + ' files selected';
}
function selectAllFiles() {
document.querySelectorAll('.file-checkbox').forEach(cb => {
if (!cb.disabled) {
cb.checked = true;
selectedFiles.add(cb.dataset.filename);
}
});
updateSelectionCount();
}
function deselectAllFiles() {
document.querySelectorAll('.file-checkbox').forEach(cb => {
cb.checked = false;
});
selectedFiles.clear();
updateSelectionCount();
}
function toggleFileSelection(filename, checked) {
if (checked) {
selectedFiles.add(filename);
} else {
selectedFiles.delete(filename);
}
updateSelectionCount();
}
function downloadSelectedFiles() {
if (selectedFiles.size === 0) {
alert('Please select files to download');
return;
}
// 각 파일을 순차적으로 다운로드
let filesArray = Array.from(selectedFiles);
let index = 0;
function downloadNext() {
if (index < filesArray.length) {
downloadFile(filesArray[index]);
index++;
setTimeout(downloadNext, 500); // 500ms 간격으로 다운로드
}
}
downloadNext();
}
function deleteSelectedFiles() {
if (selectedFiles.size === 0) {
alert('Please select files to delete');
return;
}
let filesArray = Array.from(selectedFiles);
let fileList = filesArray.join('\\n');
if (!confirm('Are you sure you want to delete ' + selectedFiles.size + ' files?\\n\\n' + fileList + '\\n\\nThis action cannot be undone.')) {
return;
}
if (ws && ws.readyState === WebSocket.OPEN) {
let filenames = JSON.stringify(filesArray);
ws.send(JSON.stringify({cmd: 'deleteFiles', filenames: filesArray}));
console.log('Delete multiple files command sent:', filesArray);
// 선택 해제
selectedFiles.clear();
updateSelectionCount();
}
}
function editComment(filename, currentComment) {
const fileItem = event.target.closest('.file-item');
const commentDiv = fileItem.querySelector('.file-comment');
// 이미 편집 중이면 무시
if (fileItem.querySelector('.comment-input')) {
return;
}
// 코멘트 편집 UI 생성
const input = document.createElement('textarea');
input.className = 'comment-input';
input.value = currentComment;
input.rows = 2;
input.placeholder = 'Enter comment for this log file...';
const actions = document.createElement('div');
actions.className = 'comment-actions';
const saveBtn = document.createElement('button');
saveBtn.textContent = 'Save';
saveBtn.style.background = 'linear-gradient(135deg, #11998e 0%, #38ef7d 100%)';
saveBtn.onclick = function() {
saveComment(filename, input.value, fileItem);
};
const cancelBtn = document.createElement('button');
cancelBtn.textContent = 'Cancel';
cancelBtn.style.background = 'linear-gradient(135deg, #95a5a6 0%, #7f8c8d 100%)';
cancelBtn.onclick = function() {
cancelEditComment(fileItem, currentComment);
};
actions.appendChild(saveBtn);
actions.appendChild(cancelBtn);
// 기존 코멘트 숨기고 편집 UI 표시
commentDiv.style.display = 'none';
fileItem.querySelector('.file-info').appendChild(input);
fileItem.querySelector('.file-info').appendChild(actions);
input.focus();
}
function saveComment(filename, comment, fileItem) {
if (ws && ws.readyState === WebSocket.OPEN) {
// JSON 이스케이프
const escapedComment = comment.replace(/\n/g, '\\n').replace(/"/g, '\\"');
ws.send(JSON.stringify({cmd: 'saveComment', filename: filename, comment: escapedComment}));
console.log('Save comment:', filename, comment);
// UI 업데이트
const commentDiv = fileItem.querySelector('.file-comment');
const input = fileItem.querySelector('.comment-input');
const actions = fileItem.querySelector('.comment-actions');
if (comment.trim() === '') {
commentDiv.textContent = '💬 Click to add comment';
commentDiv.className = 'file-comment empty';
} else {
commentDiv.textContent = '💬 ' + comment;
commentDiv.className = 'file-comment';
}
commentDiv.style.display = 'block';
commentDiv.onclick = function() { editComment(filename, comment); };
if (input) input.remove();
if (actions) actions.remove();
}
}
function cancelEditComment(fileItem, originalComment) {
const commentDiv = fileItem.querySelector('.file-comment');
const input = fileItem.querySelector('.comment-input');
const actions = fileItem.querySelector('.comment-actions');
commentDiv.style.display = 'block';
if (input) input.remove();
if (actions) actions.remove();
}
function updateCurrentTime() {
const now = new Date();
@@ -659,6 +953,20 @@ const char index_html[] PROGMEM = R"rawliteral(
updateFileList(data.files);
} else if (data.type === 'deleteResult') {
handleDeleteResult(data);
} else if (data.type === 'autoTimeSyncRequest') {
// 서버에서 자동 시간 동기화 요청
console.log('Auto time sync requested by server');
syncTime();
// 동기화 완료 알림
setTimeout(() => {
ws.send(JSON.stringify({cmd: 'requestAutoTimeSync'}));
}, 500);
} else if (data.type === 'commentResult') {
if (data.success) {
console.log('Comment saved successfully');
} else {
alert('Failed to save comment: ' + data.message);
}
}
} catch (e) {
console.error('Parse error:', e);
@@ -763,6 +1071,27 @@ const char index_html[] PROGMEM = R"rawliteral(
} else {
document.getElementById('power-status').classList.remove('low');
}
// SD 카드 용량 업데이트
if (data.sdTotalMB !== undefined) {
const totalGB = (data.sdTotalMB / 1024).toFixed(2);
const usedMB = data.sdUsedMB || 0;
const freeMB = data.sdFreeMB || 0;
document.getElementById('sd-total').textContent = totalGB + ' GB';
if (usedMB >= 1024) {
document.getElementById('sd-used').textContent = (usedMB / 1024).toFixed(2) + ' GB';
} else {
document.getElementById('sd-used').textContent = usedMB + ' MB';
}
if (freeMB >= 1024) {
document.getElementById('sd-free').textContent = (freeMB / 1024).toFixed(2) + ' GB';
} else {
document.getElementById('sd-free').textContent = freeMB + ' MB';
}
}
}
function addCanMessage(data) {
@@ -876,10 +1205,26 @@ const char index_html[] PROGMEM = R"rawliteral(
}
nameHtml += '</div>';
// 코멘트 표시
const comment = file.comment || '';
let commentHtml = '';
if (comment.trim() === '') {
commentHtml = '<div class="file-comment empty" onclick="editComment(\'' + file.name + '\', \'\')">💬 Click to add comment</div>';
} else {
const escapedComment = comment.replace(/'/g, "\\'");
commentHtml = '<div class="file-comment" onclick="editComment(\'' + file.name + '\', \'' + escapedComment + '\')">💬 ' + comment + '</div>';
}
const isChecked = selectedFiles.has(file.name);
fileItem.innerHTML =
'<input type="checkbox" class="file-checkbox" data-filename="' + file.name + '" ' +
'onchange="toggleFileSelection(\'' + file.name + '\', this.checked)" ' +
(isLogging ? 'disabled' : '') + (isChecked ? ' checked' : '') + '>' +
'<div class="file-info">' +
nameHtml +
'<div class="file-size">' + formatBytes(file.size) + '</div>' +
commentHtml +
'</div>' +
'<div class="file-actions">' +
'<button class="download-btn" onclick="downloadFile(\'' + file.name + '\')">Download</button>' +
@@ -888,6 +1233,8 @@ const char index_html[] PROGMEM = R"rawliteral(
'</div>';
fileList.appendChild(fileItem);
});
updateSelectionCount();
}
function formatBytes(bytes) {
@@ -961,7 +1308,18 @@ const char index_html[] PROGMEM = R"rawliteral(
function handleDeleteResult(data) {
if (data.success) {
console.log('File deleted successfully');
if (data.deletedCount !== undefined) {
// 복수 파일 삭제 결과
let message = 'Deleted ' + data.deletedCount + ' file(s) successfully';
if (data.failedCount > 0) {
message += '\nFailed: ' + data.failedCount + ' file(s)';
}
alert(message);
console.log('Multiple files deleted:', data);
} else {
// 단일 파일 삭제 결과
console.log('File deleted successfully');
}
// 파일 목록은 서버에서 자동으로 갱신됨
} else {
alert('Failed to delete file: ' + data.message);