Files
slmm/templates/index.html
serversdwn f9139d6aa3 feat: Add comprehensive NL-43/NL-53 Communication Guide and command references
- Introduced a new communication guide detailing protocol basics, transport modes, and a quick startup checklist.
- Added a detailed list of commands with their functions and usage for NL-43/NL-53 devices.
- Created a verified quick reference for command formats to prevent common mistakes.
- Implemented an improvements document outlining critical fixes, security enhancements, reliability upgrades, and code quality improvements for the SLMM project.
- Enhanced the frontend with a new button to retrieve all device settings, along with corresponding JavaScript functionality.
- Added a test script for the new settings retrieval API endpoint to demonstrate its usage and validate functionality.
2025-12-25 00:36:46 +00:00

595 lines
20 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SLMM NL43 Standalone</title>
<style>
body { font-family: system-ui, -apple-system, sans-serif; margin: 24px; max-width: 900px; }
fieldset { margin-bottom: 16px; padding: 12px; }
label { display: block; margin-bottom: 6px; font-weight: 600; }
input { width: 100%; padding: 8px; margin-bottom: 10px; }
button { padding: 8px 12px; margin-right: 8px; }
#log { background: #f6f8fa; border: 1px solid #d0d7de; padding: 12px; min-height: 120px; white-space: pre-wrap; }
</style>
</head>
<body>
<h1>SLMM NL43 Standalone</h1>
<p>Configure a unit (host/port), then use controls to Start/Stop and fetch live status.</p>
<fieldset>
<legend>Unit Config</legend>
<label>Unit ID</label>
<input id="unitId" value="nl43-1" />
<label>Host</label>
<input id="host" value="127.0.0.1" />
<label>Port</label>
<input id="port" type="number" value="80" />
<button onclick="saveConfig()">Save Config</button>
<button onclick="loadConfig()">Load Config</button>
</fieldset>
<fieldset>
<legend>Measurement Controls</legend>
<button onclick="start()">Start</button>
<button onclick="stop()">Stop</button>
<button onclick="pause()">Pause</button>
<button onclick="resume()">Resume</button>
<button onclick="reset()">Reset</button>
<button onclick="store()">Store Data</button>
<button onclick="live()">Fetch Live (DOD)</button>
<button onclick="getResults()">Get Results (DLC)</button>
</fieldset>
<fieldset>
<legend>Power Management</legend>
<button onclick="sleepDevice()">Sleep</button>
<button onclick="wakeDevice()">Wake</button>
<button onclick="getSleepStatus()">Check Sleep Status</button>
</fieldset>
<fieldset>
<legend>Device Info</legend>
<button onclick="getBattery()">Get Battery</button>
<button onclick="getClock()">Get Clock</button>
<button onclick="syncClock()">Sync Clock to PC</button>
</fieldset>
<fieldset>
<legend>Measurement Settings</legend>
<button onclick="getAllSettings()" style="margin-bottom: 12px; font-weight: bold;">Get ALL Settings</button>
<div style="margin-bottom: 8px;">
<label style="display: inline; margin-right: 8px;">Frequency Weighting:</label>
<button onclick="getFreqWeighting()">Get</button>
<button onclick="setFreqWeighting('A')">Set A</button>
<button onclick="setFreqWeighting('C')">Set C</button>
<button onclick="setFreqWeighting('Z')">Set Z</button>
</div>
<div>
<label style="display: inline; margin-right: 8px;">Time Weighting:</label>
<button onclick="getTimeWeighting()">Get</button>
<button onclick="setTimeWeighting('F')">Set Fast</button>
<button onclick="setTimeWeighting('S')">Set Slow</button>
<button onclick="setTimeWeighting('I')">Set Impulse</button>
</div>
</fieldset>
<fieldset>
<legend>Live Stream (DRD)</legend>
<button id="streamBtn" onclick="toggleStream()">Start Stream</button>
<span id="streamStatus" style="margin-left: 12px; color: #888;">Not connected</span>
</fieldset>
<fieldset>
<legend>FTP File Download</legend>
<button onclick="enableFTP()">Enable FTP</button>
<button onclick="disableFTP()">Disable FTP</button>
<button onclick="checkFTPStatus()">Check FTP Status</button>
<button onclick="listFiles()">List Files</button>
<div id="fileList" style="margin-top: 12px; max-height: 200px; overflow-y: auto; background: #f6f8fa; border: 1px solid #d0d7de; padding: 8px;"></div>
</fieldset>
<fieldset>
<legend>Status</legend>
<pre id="status">No data yet.</pre>
</fieldset>
<fieldset>
<legend>Log</legend>
<div id="log"></div>
</fieldset>
<script>
const logEl = document.getElementById('log');
const statusEl = document.getElementById('status');
const streamBtn = document.getElementById('streamBtn');
const streamStatus = document.getElementById('streamStatus');
let ws = null;
let streamUpdateCount = 0;
function log(msg) {
logEl.textContent += msg + "\n";
logEl.scrollTop = logEl.scrollHeight;
}
async function saveConfig() {
const unitId = document.getElementById('unitId').value;
const host = document.getElementById('host').value;
const port = parseInt(document.getElementById('port').value, 10);
const res = await fetch(`/api/nl43/${unitId}/config`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ host, tcp_port: port })
});
const data = await res.json();
log(`Saved config: ${JSON.stringify(data)}`);
}
async function loadConfig() {
const unitId = document.getElementById('unitId').value;
const res = await fetch(`/api/nl43/${unitId}/config`);
if (!res.ok) {
log(`Load config failed: ${res.status}`);
return;
}
const response = await res.json();
const data = response.data;
document.getElementById('host').value = data.host;
document.getElementById('port').value = data.tcp_port;
log(`Loaded config: ${JSON.stringify(data)}`);
}
async function start() {
const unitId = document.getElementById('unitId').value;
const res = await fetch(`/api/nl43/${unitId}/start`, { method: 'POST' });
log(`Start: ${res.status}`);
}
async function stop() {
const unitId = document.getElementById('unitId').value;
const res = await fetch(`/api/nl43/${unitId}/stop`, { method: 'POST' });
log(`Stop: ${res.status}`);
}
async function store() {
const unitId = document.getElementById('unitId').value;
const res = await fetch(`/api/nl43/${unitId}/store`, { method: 'POST' });
const data = await res.json();
log(`Store: ${res.status} - ${data.message || JSON.stringify(data)}`);
}
async function live() {
const unitId = document.getElementById('unitId').value;
const res = await fetch(`/api/nl43/${unitId}/live`);
const data = await res.json();
if (!res.ok) {
log(`Live failed: ${res.status} ${JSON.stringify(data)}`);
return;
}
statusEl.textContent = JSON.stringify(data.data, null, 2);
log(`Live: ${JSON.stringify(data.data)}`);
}
async function getResults() {
const unitId = document.getElementById('unitId').value;
const res = await fetch(`/api/nl43/${unitId}/results`);
const data = await res.json();
if (!res.ok) {
log(`Get Results failed: ${res.status} ${JSON.stringify(data)}`);
return;
}
statusEl.textContent = JSON.stringify(data.data, null, 2);
log(`Results (DLC): Retrieved final calculation data`);
}
// New measurement control functions
async function pause() {
const unitId = document.getElementById('unitId').value;
const res = await fetch(`/api/nl43/${unitId}/pause`, { method: 'POST' });
const data = await res.json();
log(`Pause: ${res.status} - ${data.message || JSON.stringify(data)}`);
}
async function resume() {
const unitId = document.getElementById('unitId').value;
const res = await fetch(`/api/nl43/${unitId}/resume`, { method: 'POST' });
const data = await res.json();
log(`Resume: ${res.status} - ${data.message || JSON.stringify(data)}`);
}
async function reset() {
const unitId = document.getElementById('unitId').value;
const res = await fetch(`/api/nl43/${unitId}/reset`, { method: 'POST' });
const data = await res.json();
log(`Reset: ${res.status} - ${data.message || JSON.stringify(data)}`);
}
// Power management functions
async function sleepDevice() {
const unitId = document.getElementById('unitId').value;
const res = await fetch(`/api/nl43/${unitId}/sleep`, { method: 'POST' });
const data = await res.json();
if (res.ok) {
log(`Device entering sleep mode`);
} else {
log(`Sleep failed: ${res.status} - ${data.detail}`);
}
}
async function wakeDevice() {
const unitId = document.getElementById('unitId').value;
const res = await fetch(`/api/nl43/${unitId}/wake`, { method: 'POST' });
const data = await res.json();
if (res.ok) {
log(`Device waking from sleep mode`);
} else {
log(`Wake failed: ${res.status} - ${data.detail}`);
}
}
async function getSleepStatus() {
const unitId = document.getElementById('unitId').value;
const res = await fetch(`/api/nl43/${unitId}/sleep/status`);
const data = await res.json();
if (res.ok) {
log(`Sleep Status: ${data.sleep_status}`);
} else {
log(`Get Sleep Status failed: ${res.status} - ${data.detail}`);
}
}
// Device info functions
async function getBattery() {
const unitId = document.getElementById('unitId').value;
const res = await fetch(`/api/nl43/${unitId}/battery`);
const data = await res.json();
if (res.ok) {
log(`Battery Level: ${data.battery_level}%`);
} else {
log(`Get Battery failed: ${res.status} - ${data.detail}`);
}
}
async function getClock() {
const unitId = document.getElementById('unitId').value;
const res = await fetch(`/api/nl43/${unitId}/clock`);
const data = await res.json();
if (res.ok) {
log(`Device Clock: ${data.clock}`);
} else {
log(`Get Clock failed: ${res.status} - ${data.detail}`);
}
}
async function syncClock() {
const unitId = document.getElementById('unitId').value;
const now = new Date();
const datetime = `${now.getFullYear()}/${String(now.getMonth() + 1).padStart(2, '0')}/${String(now.getDate()).padStart(2, '0')},${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
const res = await fetch(`/api/nl43/${unitId}/clock`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ datetime })
});
const data = await res.json();
if (res.ok) {
log(`Clock synced to: ${datetime}`);
} else {
log(`Sync Clock failed: ${res.status} - ${data.detail}`);
}
}
// Measurement settings functions
async function getAllSettings() {
const unitId = document.getElementById('unitId').value;
log('Retrieving all device settings (this may take 10-15 seconds)...');
const res = await fetch(`/api/nl43/${unitId}/settings`);
const data = await res.json();
if (!res.ok) {
log(`Get All Settings failed: ${res.status} - ${data.detail || JSON.stringify(data)}`);
return;
}
// Display in status area
statusEl.textContent = JSON.stringify(data.settings, null, 2);
// Log summary
log('=== ALL DEVICE SETTINGS ===');
Object.entries(data.settings).forEach(([key, value]) => {
log(`${key}: ${value}`);
});
log('===========================');
}
async function getFreqWeighting() {
const unitId = document.getElementById('unitId').value;
const res = await fetch(`/api/nl43/${unitId}/frequency-weighting?channel=Main`);
const data = await res.json();
if (res.ok) {
log(`Frequency Weighting (Main): ${data.frequency_weighting}`);
} else {
log(`Get Freq Weighting failed: ${res.status} - ${data.detail}`);
}
}
async function setFreqWeighting(weighting) {
const unitId = document.getElementById('unitId').value;
const res = await fetch(`/api/nl43/${unitId}/frequency-weighting`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ weighting, channel: 'Main' })
});
const data = await res.json();
if (res.ok) {
log(`Frequency Weighting set to: ${weighting}`);
} else {
log(`Set Freq Weighting failed: ${res.status} - ${data.detail}`);
}
}
async function getTimeWeighting() {
const unitId = document.getElementById('unitId').value;
const res = await fetch(`/api/nl43/${unitId}/time-weighting?channel=Main`);
const data = await res.json();
if (res.ok) {
log(`Time Weighting (Main): ${data.time_weighting}`);
} else {
log(`Get Time Weighting failed: ${res.status} - ${data.detail}`);
}
}
async function setTimeWeighting(weighting) {
const unitId = document.getElementById('unitId').value;
const res = await fetch(`/api/nl43/${unitId}/time-weighting`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ weighting, channel: 'Main' })
});
const data = await res.json();
if (res.ok) {
log(`Time Weighting set to: ${weighting}`);
} else {
log(`Set Time Weighting failed: ${res.status} - ${data.detail}`);
}
}
function toggleStream() {
if (ws && ws.readyState === WebSocket.OPEN) {
stopStream();
} else {
startStream();
}
}
function startStream() {
const unitId = document.getElementById('unitId').value;
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${wsProtocol}//${window.location.host}/api/nl43/${unitId}/stream`;
log(`Connecting to WebSocket: ${wsUrl}`);
streamUpdateCount = 0;
ws = new WebSocket(wsUrl);
ws.onopen = () => {
log('WebSocket connected - DRD streaming started');
streamBtn.textContent = 'Stop Stream';
streamStatus.textContent = 'Connected';
streamStatus.style.color = '#0a0';
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.error) {
log(`Stream error: ${data.error} - ${data.detail || ''}`);
return;
}
streamUpdateCount++;
// Update status display with live data
const displayData = {
unit_id: data.unit_id,
timestamp: data.timestamp,
lp: data.lp,
leq: data.leq,
lmax: data.lmax,
lmin: data.lmin,
lpeak: data.lpeak
};
statusEl.textContent = JSON.stringify(displayData, null, 2);
// Log every 10th update to avoid spamming
if (streamUpdateCount % 10 === 0) {
log(`Stream update #${streamUpdateCount}: Lp=${data.lp} Leq=${data.leq} Lpeak=${data.lpeak}`);
}
};
ws.onerror = (error) => {
log('WebSocket error occurred');
console.error('WebSocket error:', error);
};
ws.onclose = () => {
log(`WebSocket closed (received ${streamUpdateCount} updates)`);
streamBtn.textContent = 'Start Stream';
streamStatus.textContent = 'Not connected';
streamStatus.style.color = '#888';
ws = null;
};
}
function stopStream() {
if (ws) {
log('Closing WebSocket...');
ws.close();
}
}
// Cleanup on page unload
window.addEventListener('beforeunload', () => {
if (ws) ws.close();
});
// FTP Functions
async function enableFTP() {
const unitId = document.getElementById('unitId').value;
const res = await fetch(`/api/nl43/${unitId}/ftp/enable`, { method: 'POST' });
const data = await res.json();
log(`Enable FTP: ${res.status} - ${data.message || JSON.stringify(data)}`);
}
async function disableFTP() {
const unitId = document.getElementById('unitId').value;
const res = await fetch(`/api/nl43/${unitId}/ftp/disable`, { method: 'POST' });
const data = await res.json();
log(`Disable FTP: ${res.status} - ${data.message || JSON.stringify(data)}`);
}
async function checkFTPStatus() {
const unitId = document.getElementById('unitId').value;
const res = await fetch(`/api/nl43/${unitId}/ftp/status`);
const data = await res.json();
if (res.ok) {
log(`FTP Status: ${data.ftp_status} (enabled: ${data.ftp_enabled})`);
} else {
log(`FTP Status check failed: ${res.status}`);
}
}
let currentPath = '/';
async function listFiles(path = '/') {
currentPath = path;
const unitId = document.getElementById('unitId').value;
const res = await fetch(`/api/nl43/${unitId}/ftp/files?path=${encodeURIComponent(path)}`);
const data = await res.json();
if (!res.ok) {
log(`List files failed: ${res.status} ${JSON.stringify(data)}`);
return;
}
const fileListEl = document.getElementById('fileList');
fileListEl.innerHTML = '';
// Add breadcrumb navigation
const breadcrumb = document.createElement('div');
breadcrumb.style.marginBottom = '8px';
breadcrumb.style.padding = '4px';
breadcrumb.style.background = '#e1e4e8';
breadcrumb.style.borderRadius = '3px';
breadcrumb.innerHTML = '<strong>Path:</strong> ';
const pathParts = path.split('/').filter(p => p);
let builtPath = '/';
// Root link
const rootLink = document.createElement('a');
rootLink.href = '#';
rootLink.textContent = '/';
rootLink.style.marginRight = '4px';
rootLink.onclick = (e) => { e.preventDefault(); listFiles('/'); };
breadcrumb.appendChild(rootLink);
// Path component links
pathParts.forEach((part, idx) => {
builtPath += part + '/';
const linkPath = builtPath;
const separator = document.createElement('span');
separator.textContent = ' / ';
breadcrumb.appendChild(separator);
const link = document.createElement('a');
link.href = '#';
link.textContent = part;
link.style.marginRight = '4px';
if (idx === pathParts.length - 1) {
link.style.fontWeight = 'bold';
link.style.color = '#000';
}
link.onclick = (e) => { e.preventDefault(); listFiles(linkPath); };
breadcrumb.appendChild(link);
});
fileListEl.appendChild(breadcrumb);
if (data.files.length === 0) {
const emptyDiv = document.createElement('div');
emptyDiv.textContent = 'No files found';
emptyDiv.style.padding = '8px';
fileListEl.appendChild(emptyDiv);
log(`No files found in ${path}`);
return;
}
log(`Found ${data.count} files in ${path}`);
data.files.forEach(file => {
const fileDiv = document.createElement('div');
fileDiv.style.marginBottom = '8px';
fileDiv.style.padding = '4px';
fileDiv.style.borderBottom = '1px solid #ddd';
const icon = file.is_dir ? '📁' : '📄';
const size = file.is_dir ? '' : ` (${(file.size / 1024).toFixed(1)} KB)`;
if (file.is_dir) {
fileDiv.innerHTML = `
${icon} <a href="#" onclick="event.preventDefault(); listFiles('${file.path}');" style="font-weight: bold;">${file.name}</a>
<br><small style="color: #666;">${file.path}</small>
`;
} else {
fileDiv.innerHTML = `
${icon} <strong>${file.name}</strong>${size}
<button onclick="downloadFile('${file.path}')" style="margin-left: 8px; padding: 2px 6px; font-size: 0.9em;">Download</button>
<br><small style="color: #666;">${file.path}</small>
`;
}
fileListEl.appendChild(fileDiv);
});
}
async function downloadFile(remotePath) {
const unitId = document.getElementById('unitId').value;
log(`Downloading file: ${remotePath}...`);
try {
const res = await fetch(`/api/nl43/${unitId}/ftp/download`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ remote_path: remotePath })
});
if (!res.ok) {
const data = await res.json();
log(`Download failed: ${res.status} - ${data.detail || JSON.stringify(data)}`);
return;
}
// Trigger browser download
const blob = await res.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = remotePath.split('/').pop();
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
log(`Downloaded: ${remotePath}`);
} catch (error) {
log(`Download error: ${error.message}`);
}
}
</script>
</body>
</html>