fix: clarify event handling in waveform viewer
This commit is contained in:
@@ -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 |
|
||||
| 3–4 | 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 | |
|
||||
| 4–5 | 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
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user