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:
|
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
|
"Extended Notes"→ notes
|
||||||
```
|
```
|
||||||
|
|
||||||
These strings are **NOT** present in the 210-byte SUB 0C waveform record. They reflect
|
**IMPORTANT — 5A "Project:" is session-start config, NOT per-event (confirmed 2026-04-05):**
|
||||||
the setup at record time, not the current device config — this is why we fetch them from
|
The "Project:" string in the A5 frame 7 payload reflects the compliance setup from when
|
||||||
5A instead of backfilling from the current compliance config.
|
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,
|
`stop_after_metadata=True` (default) stops the 5A loop as soon as `b"Project:"` appears,
|
||||||
then sends the termination frame.
|
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 0C — Waveform Record (210 bytes = data[11:11+0xD2])
|
||||||
|
|
||||||
|
**sub_code=0x10 (Waveform single-shot) — 9-byte timestamp header:**
|
||||||
|
|
||||||
| Offset | Field | Type |
|
| Offset | Field | Type |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 0 | day | uint8 |
|
| 0 | day | uint8 |
|
||||||
| 1 | sub_code | uint8 (`0x10` = Waveform single-shot, `0x03` = Waveform continuous) |
|
| 1 | sub_code | uint8 (`0x10`) |
|
||||||
| 2 | month | uint8 |
|
| 2 | month | uint8 |
|
||||||
| 3–4 | year | uint16 BE |
|
| 3–4 | year | uint16 BE |
|
||||||
| 5 | unknown | uint8 (always 0) |
|
| 5 | unknown | uint8 (always 0) |
|
||||||
| 6 | hour | uint8 |
|
| 6 | hour | uint8 |
|
||||||
| 7 | minute | uint8 |
|
| 7 | minute | uint8 |
|
||||||
| 8 | second | 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 |
|
| 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-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"Seis Loc:" at data[735]
|
||||||
b"Extended Notes" at data[774]
|
b"Extended Notes" at data[774]
|
||||||
|
|
||||||
All frames are concatenated for a single-pass needle search. Fields already
|
All frames are concatenated for a single-pass needle search.
|
||||||
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.).
|
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.
|
Modifies event in-place.
|
||||||
"""
|
"""
|
||||||
@@ -709,13 +714,17 @@ def _decode_a5_metadata_into(frames_data: list[bytes], event: Event) -> None:
|
|||||||
event.project_info = ProjectInfo()
|
event.project_info = ProjectInfo()
|
||||||
|
|
||||||
pi = event.project_info
|
pi = event.project_info
|
||||||
# Overwrite with A5 values — they are event-time authoritative.
|
# "project" comes from 0C (per-event, set during _decode_waveform_record_into).
|
||||||
# 0C waveform record only carried "Project:"; A5 carries the full set.
|
# 5A returns session-start compliance config — its "project" value is NOT
|
||||||
if project: pi.project = project
|
# per-event authoritative. Only use the 5A project as a fallback if 0C
|
||||||
if client: pi.client = client
|
# didn't supply one.
|
||||||
if operator: pi.operator = operator
|
# client / operator / sensor_location / notes are NOT in the 0C record at all
|
||||||
if location: pi.sensor_location = location
|
# (confirmed from CLAUDE.md §SUB 5A), so 5A is the sole source for those.
|
||||||
if notes: pi.notes = notes
|
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(
|
log.debug(
|
||||||
"a5 metadata: project=%r client=%r operator=%r location=%r",
|
"a5 metadata: project=%r client=%r operator=%r location=%r",
|
||||||
|
|||||||
+31
-10
@@ -240,6 +240,7 @@
|
|||||||
let charts = {};
|
let charts = {};
|
||||||
let lastData = null;
|
let lastData = null;
|
||||||
let unitInfo = null;
|
let unitInfo = null;
|
||||||
|
let eventList = []; // populated from /device/events after connect
|
||||||
let currentEventIndex = 0;
|
let currentEventIndex = 0;
|
||||||
|
|
||||||
function setStatus(msg, cls = '') {
|
function setStatus(msg, cls = '') {
|
||||||
@@ -283,24 +284,45 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Populate unit bar
|
// Populate unit bar from /device/info
|
||||||
document.getElementById('u-serial').textContent = unitInfo.serial || '—';
|
document.getElementById('u-serial').textContent = unitInfo.serial || '—';
|
||||||
document.getElementById('u-fw').textContent = unitInfo.firmware_version || '—';
|
document.getElementById('u-fw').textContent = unitInfo.firmware_version || '—';
|
||||||
const sr = unitInfo.compliance_config?.sample_rate;
|
const sr = unitInfo.compliance_config?.sample_rate;
|
||||||
document.getElementById('u-sr').textContent = sr ? `${sr} sps` : '—';
|
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');
|
const chipsEl = document.getElementById('event-chips');
|
||||||
chipsEl.innerHTML = '';
|
chipsEl.innerHTML = '';
|
||||||
for (let i = 0; i < count; i++) {
|
eventList.forEach((ev, i) => {
|
||||||
const chip = document.createElement('button');
|
const chip = document.createElement('button');
|
||||||
chip.className = 'event-chip' + (i === 0 ? ' active' : '');
|
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);
|
chip.onclick = () => selectEvent(i);
|
||||||
chipsEl.appendChild(chip);
|
chipsEl.appendChild(chip);
|
||||||
}
|
});
|
||||||
|
|
||||||
document.getElementById('unit-bar').style.display = 'flex';
|
document.getElementById('unit-bar').style.display = 'flex';
|
||||||
document.getElementById('load-btn').disabled = count === 0;
|
document.getElementById('load-btn').disabled = count === 0;
|
||||||
@@ -324,8 +346,7 @@
|
|||||||
c.classList.toggle('active', i === idx);
|
c.classList.toggle('active', i === idx);
|
||||||
});
|
});
|
||||||
document.getElementById('prev-btn').disabled = idx <= 0;
|
document.getElementById('prev-btn').disabled = idx <= 0;
|
||||||
const count = unitInfo?.event_count ?? 0;
|
document.getElementById('next-btn').disabled = idx >= eventList.length - 1;
|
||||||
document.getElementById('next-btn').disabled = idx >= count - 1;
|
|
||||||
loadWaveform();
|
loadWaveform();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,7 +384,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function stepEvent(delta) {
|
function stepEvent(delta) {
|
||||||
const count = unitInfo?.event_count ?? 0;
|
const count = eventList.length;
|
||||||
const next = Math.max(0, Math.min(count - 1, currentEventIndex + delta));
|
const next = Math.max(0, Math.min(count - 1, currentEventIndex + delta));
|
||||||
selectEvent(next);
|
selectEvent(next);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user