@@ -163,6 +163,83 @@ record — 5A remains the sole source for those fields and they are set uncondit
`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.
### SUB 5A — STRT record layout and rectime_seconds (CORRECTED 2026-04-14)
The STRT record is 21 bytes embedded at the start of A5[0] data. Offsets relative to
the `b'STRT'` magic bytes:
```
+0..3 b'STRT' magic
+4..5 flags 0xFF 0xFE (single-shot) or 0xFF 0xFD (continuous)
+6..9 next_key4 ← key of the NEXT stored event (NOT the current event) ← confirmed 2026-04-14
+10..13 prev_key4 ← key of the PREVIOUS stored event ← confirmed 2026-04-14
+14..15 UNKNOWN (values seen: 0xDA63=55907, 0xF38F=62351, 0x5685=22149) — NOT total_samples
+16..17 UNKNOWN (values seen: 0x0122=290, 0x011A=282, 0x00FA=250) — NOT pretrig_samples
+18 uint8 record-MODE byte — NOT rectime in seconds
+19..20 0x00 0x00
```
**CONFIRMED field values (2026-04-14) from 3 desk-thump events, firmware S338.17: **
| Field | What it is | Confirmed |
|---|---|---|
| +4..5 | 0xFFFE single-shot / 0xFFFD continuous | ✅ |
| +6..9 | next_event_key (NOT current) | ✅ 3 events |
| +10..13 | prev_event_key | ✅ 3 events |
| +18 | mode byte: 0x46 ('F') = single-shot, 0x0E = continuous | ✅ |
**CONFIRMED (2026-04-14) — total_samples and pretrig_samples are NOT stored in the STRT record. **
The prior documented offsets (+14..15 for total_samples, +16..17 for pretrig_samples) were
WRONG — confirmed by cross-checking STRT-derived rectime against compliance record_time
(4-14-26): all 4 events give STRT-derived rectime of 21– 61 s vs actual 3.0 s (ratio 7– 20× ).
Extending the STRT dump to 32 bytes confirmed that bytes 21+ are the start of the raw ADC
waveform samples, not more STRT fields. Blastware itself derives total_samples and
pretrig_samples from the compliance config — exactly what our fallback does.
**The compliance-config fallback IS the correct permanent solution, not a workaround. **
`_decode_a5_waveform` uses:
- `pretrig_samples = round(0.25 × sample_rate)` (compliance monitoring standard)
- `total_samples = pretrig_samples + round(record_time × sample_rate)`
**CONFIRMED (2026-04-14) — waveform starts at strt_pos + 21 (no preamble). **
The original `sp + 27` skip (STRT 21B + null-pad 2B + 0xFF-sentinel 4B) was WRONG.
The 6-byte "preamble" in the 4-2-26 blast capture (`00 00 ff ff ff ff` ) was actually the
first ~0.75 sample-sets of quiet pre-trigger ADC data misread as padding. Desk-thump
events show different bytes at positions 21-26 (e.g. `00 10 02 00 ff fc` ) — they are real
ADC readings, not a fixed preamble. The `sp + 27` skip discarded 6 bytes of real waveform
data and misaligned the channel decode for all subsequent frames. Fixed: `wave = w[sp+21:]` .
The +6..9 next_key and +10..13 prev_key fields are confirmed across 4 events including the
first-event-after-erase case (prev_key = self-reference `01110000` ; next_key = device
pre-allocates the predicted next slot even before any second event exists).
**CRITICAL — strt[18] is a record-mode byte, NOT rectime_seconds (confirmed 2026-04-14): **
Analysis of 15 distinct STRT records across the 4-9-26 ACH capture shows:
- `flags=0xFFFE` (single-shot) → `strt[18] = 0x46` ('F') for EVERY event regardless of duration
- `flags=0xFFFD` (continuous) → `strt[18] = 0x0E` for EVERY event regardless of duration
Do NOT use `strt[18]` for rectime.
**Pre-trigger time is separate from record_time (confirmed 2026-04-14): **
Blastware documentation states: "The default Time Scale is -0.25 second to 1 second — this
negative number accounts for the pre-trigger set for compliance monitoring." Therefore:
- `record_time` (3.0 s) is POST-TRIGGER duration only
- Pre-trigger = 0.25 s = 256 samples at 1024 sps (compliance monitoring standard default)
- The pre-trigger field has NOT yet been located in the raw compliance config bytes
- `_decode_a5_waveform` falls back to pretrig = 0.25 × sr from compliance standard
- TODO: locate pretrig_time offset in ComplianceConfig — search around anchor or channel blocks
The device bulk-streams zero-padded frames BEYOND the configured record window. The
viewer clips `displayCount = total_samples = pretrig + post_trig` to exclude this padding.
**Validity checks in `_decode_a5_waveform`: **
Check 1: `pretrig_samples >= total_samples` → invalid (original check).
Check 2: STRT-derived rectime differs from `compliance_config.record_time` by more than 2×
→ invalid. Both failures fall back to the compliance-config derived values.
`_decode_a5_waveform` logs `raw strt[0:21]` at WARNING level on any failure.
Observed once (2026-04-14) with `strt[16:18] = 0x41 0x01` → pretrig=16641 (impossible).
Root cause not yet identified — capture the warning log hex dump to diagnose.
### SUB 5A — end-of-stream signal (confirmed 2026-04-06)
### SUB 5A — end-of-stream signal (confirmed 2026-04-06)
After streaming all waveform chunks, the device sends exactly **1 raw byte ** in response to
After streaming all waveform chunks, the device sends exactly **1 raw byte ** in response to
@@ -189,6 +266,94 @@ silence). Only the initial variable-size chunks contain actual signal.
Removed. Terminator detection is via `page_key == 0x0000` in `read_bulk_waveform_stream` ,
Removed. Terminator detection is via `page_key == 0x0000` in `read_bulk_waveform_stream` ,
not frame index.
not frame index.
### SUB 5A — re-probe at counter=0x1000 (DLE collision, FIXED 2026-04-15)
**Root cause (confirmed from diagnostic output, desk-thump event key=01110000): **
`bulk_waveform_params()` sets `p[3] = (counter >> 8) & 0xFF` . For chunk 4, counter =
`4 * 0x0400 = 0x1000` , so `p[3] = 0x10` — the DLE byte. Because `build_5a_frame` writes
params RAW (no DLE stuffing), the on-wire byte sequence contains a bare `0x10` . The
device DLE-decodes its own receive buffer: `10 00` (the p[3]/p[4] pair) is collapsed to
`00` , so the counter field reads 0 — a probe request. The device re-sends the initial
probe response (containing the STRT record and first waveform bytes).
**Effect: ** In a 36-frame stream for key=01110000, fi=4 was a byte-for-byte duplicate of
fi=0 (same db=1101B, same w[0:32] bytes, STRT present at w[10]). The old code treated it
as a regular chunk, decoded the STRT bytes as int16 samples (producing T=21587="ST",
V=21586="RT"), and shifted the running byte alignment for all subsequent frames.
**Fix (`_decode_a5_waveform`): ** Check for STRT in every frame, not just fi==0. Any
non-fi=0 frame containing STRT is a re-probe — log it and skip (do NOT add to
`all_chunks` ). Regular chunk path is reached only when `w.find(b"STRT") < 0` .
**Note: ** This DLE collision is key-specific. For key=01110000 (`key4[2:4]=0x0000` ),
counter = `chunk_num * 0x0400` , so counter=0x1000 occurs at chunk 4 for every event with
this key. For other keys (e.g. key=0111245a, `key4[2:4]=0x245A` ), the chunk counter
formula `key4[2:4] + n*0x0400` produces different values; the collision only occurs when
the high byte of any counter is 0x10.
### SUB 5A — metadata false-positive (FIXED 2026-04-15)
**Root cause (confirmed from diagnostic output): **
The old metadata-frame test was `b"Project:" in w` (single anchor). For the 36-frame
desk-thump stream, fi=15 had `b"Project:"` at w[93] inside live ADC data — a coincidental
4-byte pattern in the waveform. The frame (134 live sample-sets) was incorrectly skipped.
**Fix: ** Require BOTH `b"Project:" in w` AND `b"Client:" in w` to classify a frame as
metadata. The real metadata frame (fi=6 in the desk-thump stream) contains both strings
as part of the compliance-setup ASCII block; random ADC data is statistically unlikely to
contain both 8-byte sequences.
**Updated check in `_decode_a5_waveform`: **
``` python
elif b " Project: " in w and b " Client: " in w :
log . info ( " _decode_a5_waveform: fi= %d skipped (metadata frame) " , fi )
continue
```
### SUB 5A — 0xFF tail frames beyond record window (confirmed 2026-04-15)
The device bulk-streams flash pages beyond the configured record window. Un-written flash
pages read as 0xFF. Decoded as int16 LE, `0xFF 0xFF = -1` , which maps to ~0 in/s after
the geo scale factor is applied — producing a visible flat-line at the end of the waveform.
**Confirmed from raw capture analysis (desk-thump event key=01110000, 1024 sps,
record_time=3.0 s, ach_inbound_20260412_231505):**
- total_samples = 256 (pretrig) + 3072 (post) = 3328
- A5 stream: 37 frames total; frames 8– 35 are uniform 1036-byte 0xFF data (unwritten flash)
- Actual ADC signal: frames 0 (probe) + 1– 3 + 5 + 7 = 6 data chunks ≈ 815 sample-sets (0.80 s)
- The desk-thump vibration was genuinely short; the remaining ~2.2 s of the record window
is unwritten flash (0xFF), not signal. The flatline is physically correct.
- After 0xFF tail clip: display = min(decoded, 3328) excludes the all-zeros post-record padding
**Fix (two parts): **
1. `_decode_a5_waveform` (Python): already returns `total_samples` alongside
`samples_decoded` ; the field is populated from compliance config:
`total_samples = pretrig_samples + round(record_time * sample_rate)` .
2. `sfm_webapp.html` (`_buildWaveformCharts` ): `display = Math.min(decoded, total_samples)` ;
`times` array and per-channel `samples` are both sliced to `display` length before
plotting — `(channels[ch] || []).slice(0, display)` . Without this, the chart rendered
all `decoded` samples including the 0xFF tail.
### SUB 5A — chunk frame header is 5 bytes, NOT 8 (CORRECTED 2026-04-15)
* * `_decode_a5_waveform` uses `wave = w[5:]` to strip the per-chunk frame header.**
The header is 5 bytes: 2 counter bytes + 3 zero bytes. The code previously used `w[8:]`
(8-byte strip) — this was WRONG and discarded 3 bytes of real waveform data per frame,
misaligning the channel decode for all frames after any non-multiple-of-8 frame.
**Proof — "Standard Recording Setup" cross-frame continuity test (MITM capture 4-11-26): **
The ASCII string `"Standard Recording Setup"` spans the A5[5]→A5[6] frame boundary:
- A5[5].w last 2 bytes: `53 74` = `"St"` (end of "Standard")
- A5[6].w[0:5] = `00 00 00 00 00` (5 null bytes = header)
- A5[6].w[5:] begins with `61 6e 64` = `"and"` (start of "andard Recording Setup…")
Only `header=5` produces the contiguous string `"Standard Recording Setup"` .
`header=6/7/8` all produce broken text (tested exhaustively).
**Also updated: ** The probe frame (fi=0) strip of `w[10:]` is unchanged — the STRT magic
itself is at `w[10]` inside the probe frame, which has a different 10-byte preamble.
### SUB 1E / 1F — event iteration null sentinel and token position (FIXED, do not re-introduce)
### SUB 1E / 1F — event iteration null sentinel and token position (FIXED, do not re-introduce)
**token_params bug (FIXED): ** The token byte was at `params[6]` (wrong). Both 3-31-26 and
**token_params bug (FIXED): ** The token byte was at `params[6]` (wrong). Both 3-31-26 and
@@ -935,6 +1100,32 @@ call-home.
---
---
## Known broken / deferred
### Waveform chart Y-axis scaling (see GitHub issue)
**The waveform chart in the web viewer shows incorrect amplitudes — DO NOT trust the Y-axis values. **
PPV values reported by the system (stored in the DB, shown in event summaries) come from
the **0C waveform record ** computed on-device and are correct. The issue is purely in the
ADC→in/s conversion used to draw the chart.
What we know:
- Decoded ADC peaks from 5A (~30703– 32661 counts) imply ~9× larger PPV than the 0C record reports
(e.g. Vert 5.8 in/s decoded vs 0.690 in/s from 0C for the same event)
- ALL four channels show near-full-scale peaks, which is implausible for a desk-thump event
where Vert should dominate
- The `max_range_geo` value in the compliance config is 6.206053 in/s (not 10.000 or 1.25
as displayed in the Blastware UI) — unclear whether this is an ADC full-scale or something else
- The empirical full-scale implied by 0C data is ~0.736 in/s, not 6.206
- Tried: LE vs BE decode, header=5 vs 8, float32 format — all produce near-full-scale peaks
- Root cause unknown; likely requires a Blastware-generated waveform export for the same
event to reverse-engineer the correct scaling formula
Tracking: **GitHub issue #TODO ** (create before resuming this work)
---
## What's next
## What's next
- **Database** — SQLite store for events + monitor log entries; dedup by key; queryable
- **Database** — SQLite store for events + monitor log entries; dedup by key; queryable
@@ -944,3 +1135,4 @@ call-home.
- Modem manager — push RV50/RV55 configs via Sierra Wireless API
- Modem manager — push RV50/RV55 configs via Sierra Wireless API
- RV55 DCD/DTR issue — newer RV55 firmware doesn't assert DCD by default; units don't
- RV55 DCD/DTR issue — newer RV55 firmware doesn't assert DCD by default; units don't
resume monitoring after call-home disconnect (`--restart-monitoring` flag deferred)
resume monitoring after call-home disconnect (`--restart-monitoring` flag deferred)