fix: correct monitoring flag and battery/memory offsets in _decode_monitor_status

section[6] is the monitoring flag (was wrongly section[1] — section[1] is always
0x00 in both states). Battery and memory fields use relative-from-end offsets
(section[-11:-9], section[-9:-5], section[-5:-1]) instead of absolute positions,
which broke when the payload grew by 3 bytes in monitoring mode.

Confirmed from full byte diff of 142 0xE3 frames in 4-8-26/2ndtry capture.
SFM start_monitoring now polls /device/monitor/status every 5s for up to 60s
instead of a fixed 25s delay (unit runs ~40s on-device sensor check before
confirming monitoring state).

Also corrects stale 1C→6E response anomaly claim in protocol reference — no
exceptions to the 0xFF−SUB rule are known.
This commit is contained in:
2026-04-08 23:41:11 -04:00
parent dda5683572
commit 990cb8850e
4 changed files with 345 additions and 56 deletions
+56 -22
View File
@@ -813,13 +813,14 @@ function populateDeviceBar() {
// ── Monitoring ─────────────────────────────────────────────────────────────────
async function refreshMonitorStatus() {
if (!devHost()) return;
if (!devHost()) return null;
try {
const r = await fetch(`${api()}/device/monitor/status?${deviceParams()}`);
if (!r.ok) return;
if (!r.ok) return null;
const s = await r.json();
updateMonitorPanel(s);
} catch (_) {}
return s;
} catch (_) { return null; }
}
function updateMonitorPanel(s) {
@@ -832,24 +833,23 @@ function updateMonitorPanel(s) {
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 = '—';
badge.textContent = 'MONITORING';
badge.className = 'mon-status-badge monitoring';
panel.className = 'monitoring';
startB.disabled = true;
stopB.disabled = false;
} 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` : '—';
badge.textContent = 'IDLE';
badge.className = 'mon-status-badge idle';
panel.className = 'idle';
startB.disabled = false;
stopB.disabled = true;
}
// Battery and memory are available in both states — update if present,
// keep previous value if this was an optimistic update with no real data.
if (s.battery_v != null) batEl.textContent = `${s.battery_v.toFixed(2)} V`;
if (s.memory_total_kb != null) memTEl.textContent = `${s.memory_total_kb} KB`;
if (s.memory_free_kb != null) memFEl.textContent = `${s.memory_free_kb} KB`;
}
async function startMonitoring() {
@@ -860,13 +860,47 @@ async function startMonitoring() {
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();
// Optimistically show MONITORING immediately. The unit may run a ~40s on-device
// sensor check before fully entering monitor mode. We poll status every 5s for
// up to 60s, updating the badge when is_monitoring flips to true.
updateMonitorPanel({ is_monitoring: true });
setStatus('Monitoring started — sensor check in progress (~40s)…', 'loading');
btn.textContent = '▶ Start';
_pollMonitorConfirm(0);
} catch (e) {
setStatus(`Start monitoring failed: ${e.message}`, 'error');
btn.disabled = false;
btn.textContent = '▶ Start';
}
}
async function _pollMonitorConfirm(attempt) {
// Poll /device/monitor/status every 5s for up to 60s after startMonitoring().
// Updates the panel on each successful poll. Resolves once is_monitoring is true
// or after 12 attempts (60s), whichever comes first.
const MAX_ATTEMPTS = 12;
const INTERVAL_MS = 5000;
if (attempt >= MAX_ATTEMPTS) {
const s = await refreshMonitorStatus();
if (!s || !s.is_monitoring) {
setStatus('Warning: unit did not confirm monitoring state after 60s. Check device.', 'error');
} else {
setStatus('Monitoring active.', 'ok');
}
return;
}
await new Promise(res => setTimeout(res, INTERVAL_MS));
const s = await refreshMonitorStatus();
if (s && s.is_monitoring) {
setStatus('Monitoring active.', 'ok');
} else {
const elapsed = (attempt + 1) * 5;
setStatus(`Sensor check in progress… (${elapsed}s elapsed)`, 'loading');
_pollMonitorConfirm(attempt + 1);
}
btn.textContent = '▶ Start';
}
async function stopMonitoring() {