feat: Add monitoring functionality to MiniMate protocol and web interface
- Introduced new SUBs for monitoring status, start, and stop commands in protocol.py. - Implemented read_monitor_status, start_monitoring, and stop_monitoring methods in MiniMateProtocol class. - Added new API endpoints for monitoring status retrieval and control in server.py. - Enhanced the web application with a monitoring panel, including battery and memory status display. - Created a new Python script to parse SUB 0x1C response frames for monitoring status. - Documented the monitoring status response format and field locations in markdown and text files.
This commit is contained in:
+138
-3
@@ -130,6 +130,36 @@
|
||||
.di-value.accent { color: var(--blue-lt); font-weight: 600; }
|
||||
.di-value.project-val { color: #e6edf3; max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
|
||||
/* ── Monitor panel ── */
|
||||
#monitor-panel {
|
||||
background: var(--surface);
|
||||
border-bottom: 1px solid var(--border2);
|
||||
padding: 8px 18px;
|
||||
display: none;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
#monitor-panel.monitoring { border-left: 3px solid var(--green); }
|
||||
#monitor-panel.idle { border-left: 3px solid var(--text-mute); }
|
||||
.mon-status-badge {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
padding: 2px 10px;
|
||||
border-radius: 10px;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.mon-status-badge.monitoring { background: rgba(46,160,67,0.2); color: var(--green-lt); }
|
||||
.mon-status-badge.idle { background: var(--surface2); color: var(--text-mute); }
|
||||
.mon-field { display: flex; flex-direction: column; gap: 1px; }
|
||||
.mon-label { color: var(--text-mute); font-size: 10px; text-transform: uppercase; letter-spacing: 0.05em; }
|
||||
.mon-value { color: var(--text); font-family: monospace; font-size: 13px; }
|
||||
#mon-start-btn { background: var(--green); color: #fff; }
|
||||
#mon-start-btn:hover { background: var(--green-lt); }
|
||||
#mon-stop-btn { background: var(--red); color: #fff; }
|
||||
#mon-stop-btn:hover { filter: brightness(1.15); }
|
||||
.mon-spacer { flex: 1; }
|
||||
|
||||
/* ── Status bar ── */
|
||||
#status-bar {
|
||||
background: var(--surface);
|
||||
@@ -479,6 +509,27 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Monitor panel ───────────────────────────────────────────────── -->
|
||||
<div id="monitor-panel">
|
||||
<span class="mon-status-badge idle" id="mon-badge">IDLE</span>
|
||||
<div class="mon-field">
|
||||
<span class="mon-label">Battery</span>
|
||||
<span class="mon-value" id="mon-battery">—</span>
|
||||
</div>
|
||||
<div class="mon-field">
|
||||
<span class="mon-label">Memory total</span>
|
||||
<span class="mon-value" id="mon-mem-total">—</span>
|
||||
</div>
|
||||
<div class="mon-field">
|
||||
<span class="mon-label">Memory free</span>
|
||||
<span class="mon-value" id="mon-mem-free">—</span>
|
||||
</div>
|
||||
<div class="mon-spacer"></div>
|
||||
<button class="btn" id="mon-refresh-btn" onclick="refreshMonitorStatus()" title="Refresh monitoring status">↻ Status</button>
|
||||
<button class="btn" id="mon-start-btn" onclick="startMonitoring()" title="Start monitoring">▶ Start</button>
|
||||
<button class="btn" id="mon-stop-btn" onclick="stopMonitoring()" title="Stop monitoring">■ Stop</button>
|
||||
</div>
|
||||
|
||||
<!-- ── Status bar ─────────────────────────────────────────────────── -->
|
||||
<div id="status-bar">Ready — enter device host and click Connect.</div>
|
||||
|
||||
@@ -723,7 +774,8 @@ async function connectUnit() {
|
||||
populateEventChips();
|
||||
populateConfigFromDeviceInfo();
|
||||
|
||||
document.getElementById('device-bar').style.display = 'flex';
|
||||
document.getElementById('device-bar').style.display = 'flex';
|
||||
document.getElementById('monitor-panel').style.display = 'flex';
|
||||
document.getElementById('load-btn').disabled = eventList.length === 0;
|
||||
document.getElementById('prev-btn').disabled = true;
|
||||
document.getElementById('next-btn').disabled = eventList.length <= 1;
|
||||
@@ -733,6 +785,10 @@ async function connectUnit() {
|
||||
btn.disabled = false; btn.textContent = 'Reconnect';
|
||||
|
||||
setStatus(`Connected — ${eventList.length} event${eventList.length !== 1 ? 's' : ''} stored.`, 'ok');
|
||||
|
||||
// Fetch monitor status in background (non-blocking)
|
||||
refreshMonitorStatus().catch(() => {});
|
||||
|
||||
const cc = unitInfo.compliance_config;
|
||||
if (cc) {
|
||||
if (cc.sample_rate) addPill(`${cc.sample_rate} sps`);
|
||||
@@ -755,6 +811,81 @@ function populateDeviceBar() {
|
||||
geoRange = cc.max_range_geo ?? 6.206;
|
||||
}
|
||||
|
||||
// ── Monitoring ─────────────────────────────────────────────────────────────────
|
||||
async function refreshMonitorStatus() {
|
||||
if (!devHost()) return;
|
||||
try {
|
||||
const r = await fetch(`${api()}/device/monitor/status?${deviceParams()}`);
|
||||
if (!r.ok) return;
|
||||
const s = await r.json();
|
||||
updateMonitorPanel(s);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
function updateMonitorPanel(s) {
|
||||
const panel = document.getElementById('monitor-panel');
|
||||
const badge = document.getElementById('mon-badge');
|
||||
const batEl = document.getElementById('mon-battery');
|
||||
const memTEl = document.getElementById('mon-mem-total');
|
||||
const memFEl = document.getElementById('mon-mem-free');
|
||||
const startB = document.getElementById('mon-start-btn');
|
||||
const stopB = document.getElementById('mon-stop-btn');
|
||||
|
||||
if (s.is_monitoring) {
|
||||
badge.textContent = 'MONITORING';
|
||||
badge.className = 'mon-status-badge monitoring';
|
||||
panel.className = 'monitoring';
|
||||
startB.disabled = true;
|
||||
stopB.disabled = false;
|
||||
batEl.textContent = '—';
|
||||
memTEl.textContent = '—';
|
||||
memFEl.textContent = '—';
|
||||
} else {
|
||||
badge.textContent = 'IDLE';
|
||||
badge.className = 'mon-status-badge idle';
|
||||
panel.className = 'idle';
|
||||
startB.disabled = false;
|
||||
stopB.disabled = true;
|
||||
batEl.textContent = s.battery_v != null ? `${s.battery_v.toFixed(2)} V` : '—';
|
||||
memTEl.textContent = s.memory_total_kb != null ? `${s.memory_total_kb} KB` : '—';
|
||||
memFEl.textContent = s.memory_free_kb != null ? `${s.memory_free_kb} KB` : '—';
|
||||
}
|
||||
}
|
||||
|
||||
async function startMonitoring() {
|
||||
if (!devHost()) return;
|
||||
const btn = document.getElementById('mon-start-btn');
|
||||
btn.disabled = true; btn.textContent = '…';
|
||||
setStatus('Starting monitoring…', 'loading');
|
||||
try {
|
||||
const r = await fetch(`${api()}/device/monitor/start?${deviceParams()}`, { method: 'POST' });
|
||||
if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e.detail || r.statusText); }
|
||||
setStatus('Monitoring started.', 'ok');
|
||||
await refreshMonitorStatus();
|
||||
} catch (e) {
|
||||
setStatus(`Start monitoring failed: ${e.message}`, 'error');
|
||||
btn.disabled = false;
|
||||
}
|
||||
btn.textContent = '▶ Start';
|
||||
}
|
||||
|
||||
async function stopMonitoring() {
|
||||
if (!devHost()) return;
|
||||
const btn = document.getElementById('mon-stop-btn');
|
||||
btn.disabled = true; btn.textContent = '…';
|
||||
setStatus('Stopping monitoring…', 'loading');
|
||||
try {
|
||||
const r = await fetch(`${api()}/device/monitor/stop?${deviceParams()}`, { method: 'POST' });
|
||||
if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e.detail || r.statusText); }
|
||||
setStatus('Monitoring stopped.', 'ok');
|
||||
await refreshMonitorStatus();
|
||||
} catch (e) {
|
||||
setStatus(`Stop monitoring failed: ${e.message}`, 'error');
|
||||
btn.disabled = false;
|
||||
}
|
||||
btn.textContent = '■ Stop';
|
||||
}
|
||||
|
||||
// ── Device tab ─────────────────────────────────────────────────────────────────
|
||||
function populateDeviceTab() {
|
||||
document.getElementById('no-device-msg').style.display = 'none';
|
||||
@@ -1021,8 +1152,12 @@ function renderWaveform(data) {
|
||||
if (isGeo) {
|
||||
const scale = geoRange / 32767;
|
||||
plotData = samples.map(s => s * scale);
|
||||
const peak = Math.max(...plotData.map(Math.abs));
|
||||
peakLabel = `${peak.toFixed(5)} in/s`;
|
||||
// Use the device-recorded peak from the 0C waveform record — authoritative
|
||||
// and matches Blastware. Computing from raw samples can catch rogue
|
||||
// near-full-scale values from decoding artifacts.
|
||||
const peakKey = { Tran:'tran_in_s', Vert:'vert_in_s', Long:'long_in_s' }[ch];
|
||||
const devicePeak = data.peak_values?.[peakKey] ?? null;
|
||||
peakLabel = devicePeak != null ? `${devicePeak.toFixed(5)} in/s` : `${Math.max(...plotData.map(Math.abs)).toFixed(5)} in/s`;
|
||||
yUnit = 'in/s';
|
||||
ttFmt = v => `${ch}: ${v.toFixed(5)} in/s`;
|
||||
tickFmt = v => v.toFixed(4);
|
||||
|
||||
Reference in New Issue
Block a user