fix: clarify event handling in waveform viewer

This commit is contained in:
2026-04-06 00:00:06 -04:00
parent ecb1147216
commit dfa09d2a4f
3 changed files with 94 additions and 30 deletions
+44 -10
View File
@@ -25,7 +25,7 @@ CHANGELOG.md ← version history
---
## Current implementation state (v0.6.0)
## Current implementation state (v0.7.0)
Full read pipeline working end-to-end over TCP/cellular:
@@ -128,9 +128,15 @@ setup as it existed when the event was recorded:
"Extended Notes"→ notes
```
These strings are **NOT** present in the 210-byte SUB 0C waveform record. They reflect
the setup at record time, not the current device config — this is why we fetch them from
5A instead of backfilling from the current compliance config.
**IMPORTANT — 5A "Project:" is session-start config, NOT per-event (confirmed 2026-04-05):**
The "Project:" string in the A5 frame 7 payload reflects the compliance setup from when
the *monitoring session first started*, not the individual event's project name. The per-
event project name is correctly stored in the 210-byte 0C waveform record and must be
used as the authoritative source. `_decode_a5_metadata_into` therefore only sets
`project` from 5A when 0C didn't already supply one.
"Client:", "User Name:", "Seis Loc:", and "Extended Notes" are **NOT** present in the 0C
record — 5A remains the sole source for those fields and they are set unconditionally.
`stop_after_metadata=True` (default) stops the 5A loop as soon as `b"Project:"` appears,
then sends the termination frame.
@@ -265,20 +271,47 @@ Anchor: `b'\x01\x2c\x00\x00\xbe\x80\x00\x00\x00\x00'`, search `cfg[0:150]`
### SUB 0C — Waveform Record (210 bytes = data[11:11+0xD2])
**sub_code=0x10 (Waveform single-shot) — 9-byte timestamp header:**
| Offset | Field | Type |
|---|---|---|
| 0 | day | uint8 |
| 1 | sub_code | uint8 (`0x10` = Waveform single-shot, `0x03` = Waveform continuous) |
| 1 | sub_code | uint8 (`0x10`) |
| 2 | month | uint8 |
| 34 | year | uint16 BE |
| 5 | unknown | uint8 (always 0) |
| 6 | hour | uint8 |
| 7 | minute | uint8 |
| 8 | second | uint8 |
| 87 | peak_vector_sum | float32 BE |
| label+6 | PPV per channel | float32 BE (search for `"Tran"`, `"Vert"`, `"Long"`, `"MicL"`) |
PPV labels are NOT 4-byte aligned. The label-offset+6 approach is the only reliable method.
**sub_code=0x03 (Waveform continuous) — 10-byte timestamp header (1-byte shift):**
Confirmed 2026-04-03 against Blastware event report (15:20:17 Apr 3 2026).
Raw wire bytes: `10 03 10 04 07 ea 00 0f 14 11`
| Offset | Field | Type | Notes |
|---|---|---|---|
| 0 | unknown_a | uint8 | `0x10` observed |
| 1 | day | uint8 | doubles as sub_code position in 0x10 layout |
| 2 | unknown_b | uint8 | `0x10` observed |
| 3 | month | uint8 | |
| 45 | year | uint16 BE | |
| 6 | unknown | uint8 | |
| 7 | hour | uint8 | |
| 8 | minute | uint8 | |
| 9 | second | uint8 | |
**Peak values (both record types):**
| Location | Field | Type |
|---|---|---|
| `tran_pos - 12` | peak_vector_sum | float32 BE — label-relative, NOT fixed offset |
| `label + 6` | PPV per channel | float32 BE (search for `"Tran"`, `"Vert"`, `"Long"`, `"MicL"`) |
PPV labels are NOT 4-byte aligned. The label-relative approach is the only reliable method.
`peak_vector_sum` is exactly 12 bytes before the `"Tran"` label — confirmed for both
sub_code=0x10 and sub_code=0x03. Do NOT use fixed offset 87 (only incidentally correct
for 0x10 records).
---
@@ -299,9 +332,10 @@ Server retries once on `ProtocolError` for TCP connections (handles cold-boot ti
| Capture | Location | Contents |
|---|---|---|
| 1-2-26 | `bridges/captures/1-2-26/` | SUB 5A BW TX frames — used to confirm 5A frame format, 11-byte params, DLE-aware checksum |
| 1-2-26 | `bridges/captures/1-2-26/` | SUB 5A BW TX frames — confirmed 5A frame format, 11-byte params, DLE-aware checksum |
| 3-11-26 | `bridges/captures/3-11-26/` | Full compliance setup write, Aux Trigger capture |
| 3-31-26 | `bridges/captures/3-31-26/` | Complete event download cycle (148 BW / 147 S3 frames) — confirmed 1E/0A/0C/1F sequence |
| 3-31-26 | `bridges/captures/3-31-26/` | Complete event download cycle (148 BW / 147 S3 frames) — confirmed 1E/0A/0C/1F sequence; only 1 event stored so token=0xFE appeared to work |
| 4-3-26 | `bridges/captures/4-3-26/` | Browse-mode S3 capture with 2+ events — confirmed all-zero params for 1F, 1F response layout, null sentinel, 0A context requirement |
---
+19 -10
View File
@@ -672,9 +672,14 @@ def _decode_a5_metadata_into(frames_data: list[bytes], event: Event) -> None:
b"Seis Loc:" at data[735]
b"Extended Notes" at data[774]
All frames are concatenated for a single-pass needle search. Fields already
set from the 0C waveform record are overwritten — A5 data is more complete
(the 210-byte 0C record only carries "Project:", not client/operator/etc.).
All frames are concatenated for a single-pass needle search.
NOTE: 5A appears to return the compliance config from when the *monitoring
session first started*, not per-event config. This means:
- "Project:" from 5A must NOT overwrite a value already set from the 0C record,
because 0C carries the correct per-event project name.
- "Client:", "User Name:", "Seis Loc:", "Extended Notes" are NOT present in the
210-byte 0C record at all, so 5A remains the sole source for those fields.
Modifies event in-place.
"""
@@ -709,13 +714,17 @@ def _decode_a5_metadata_into(frames_data: list[bytes], event: Event) -> None:
event.project_info = ProjectInfo()
pi = event.project_info
# Overwrite with A5 values — they are event-time authoritative.
# 0C waveform record only carried "Project:"; A5 carries the full set.
if project: pi.project = project
if client: pi.client = client
if operator: pi.operator = operator
if location: pi.sensor_location = location
if notes: pi.notes = notes
# "project" comes from 0C (per-event, set during _decode_waveform_record_into).
# 5A returns session-start compliance config — its "project" value is NOT
# per-event authoritative. Only use the 5A project as a fallback if 0C
# didn't supply one.
# client / operator / sensor_location / notes are NOT in the 0C record at all
# (confirmed from CLAUDE.md §SUB 5A), so 5A is the sole source for those.
if project and not pi.project: pi.project = project
if client: pi.client = client
if operator: pi.operator = operator
if location: pi.sensor_location = location
if notes: pi.notes = notes
log.debug(
"a5 metadata: project=%r client=%r operator=%r location=%r",
+31 -10
View File
@@ -240,6 +240,7 @@
let charts = {};
let lastData = null;
let unitInfo = null;
let eventList = []; // populated from /device/events after connect
let currentEventIndex = 0;
function setStatus(msg, cls = '') {
@@ -283,24 +284,45 @@
return;
}
// Populate unit bar
// Populate unit bar from /device/info
document.getElementById('u-serial').textContent = unitInfo.serial || '—';
document.getElementById('u-fw').textContent = unitInfo.firmware_version || '—';
const sr = unitInfo.compliance_config?.sample_rate;
document.getElementById('u-sr').textContent = sr ? `${sr} sps` : '—';
const count = unitInfo.event_count ?? 0;
document.getElementById('u-count').textContent = count;
// Build event chips
// Fetch real event list from /device/events — SUB 08 count is unreliable
setStatus('Fetching event list…', 'loading');
const eventsUrl = `${apiBase}/device/events?host=${encodeURIComponent(devHost)}&tcp_port=${tcpPort}`;
try {
const evResp = await fetch(eventsUrl);
if (!evResp.ok) {
const err = await evResp.json().catch(() => ({ detail: evResp.statusText }));
throw new Error(err.detail || evResp.statusText);
}
const evData = await evResp.json();
eventList = evData.events || [];
} catch (e) {
setStatus(`Error fetching events: ${e.message}`, 'error');
btn.disabled = false;
btn.textContent = 'Reconnect';
return;
}
const count = eventList.length;
document.getElementById('u-count').textContent = count;
// Build event chips with timestamps
const chipsEl = document.getElementById('event-chips');
chipsEl.innerHTML = '';
for (let i = 0; i < count; i++) {
eventList.forEach((ev, i) => {
const chip = document.createElement('button');
chip.className = 'event-chip' + (i === 0 ? ' active' : '');
chip.textContent = `Event ${i}`;
const label = ev.timestamp?.display ?? `Event ${ev.index}`;
chip.textContent = label;
chip.title = ev.record_type || '';
chip.onclick = () => selectEvent(i);
chipsEl.appendChild(chip);
}
});
document.getElementById('unit-bar').style.display = 'flex';
document.getElementById('load-btn').disabled = count === 0;
@@ -324,8 +346,7 @@
c.classList.toggle('active', i === idx);
});
document.getElementById('prev-btn').disabled = idx <= 0;
const count = unitInfo?.event_count ?? 0;
document.getElementById('next-btn').disabled = idx >= count - 1;
document.getElementById('next-btn').disabled = idx >= eventList.length - 1;
loadWaveform();
}
@@ -363,7 +384,7 @@
}
function stepEvent(delta) {
const count = unitInfo?.event_count ?? 0;
const count = eventList.length;
const next = Math.max(0, Math.min(count - 1, currentEventIndex + delta));
selectEvent(next);
}