15be9eafdbc7eef122566cf09061feb6666c340b
4
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
d7d72cadf8 |
feat(micromate): the event chain -- walk, records, download, and a self-check
Step 4 of docs/micromate_client_spec.md. MicromateEventRef plus list_events(), iter_events(), download_event(), get_event() and decode_error(). 14 new tests; 112 micromate tests total. The load-bearing test replays THOR's captured six-event download session through iter_events() + get_event() and asserts EVERY BYTE WE EMIT MATCHES THOR'S, in order -- 74 frames -- while decoding all six events and cross-checking each waveform's peak vector sum against the float the device computed itself. Two design decisions worth recording: 1. iter_events() EXISTS BECAUSE THOR INTERLEAVES. Its captured order is 0x93 -> 1E -> 0C -> 5A*n -> 0x93 -> 1F -> 0C -> 5A*n, downloading each event before advancing the chain. list_events() walks to the end first, which is fine for browsing but leaves the device cursor parked past the event a later download addresses. 0x5A is key-addressed so it very probably does not care -- but nothing observed says either way, so the interleaved path is the one offered for downloads, and it is the one the replay test exercises. 2. get_event(verify=True) RECOMPUTES THE PEAK VECTOR SUM from the decoded samples and compares it against the device's own 0x0C float. Two independent computations over the same samples, so a disagreement means our decode is wrong. Agreement on the bench events is 0.000%. Cheap insurance in a codebase whose decode failures have historically been silent -- unhandled block tags shorten a channel and nothing raises. It is a decode-correctness check, NOT a truncation detector: a channel cut after its peak still yields the right PVS, and the docstring says so. A test corrupts a stored peak to prove the check actually fires. MicromateEventRef.uid is SERIAL:key, because the key alone is ambiguous across units, and .filename generates THOR's name (<serial>_<YYYYMMDDHHMMSS>.IDFW/H) -- returning None rather than guessing when the record type is unknown, since read_idf_file() dispatches on exactly that suffix. Recorded as a CANDIDATE, not used: 0x06 content[0:4] looks like the EVENT COUNT -- 6 on a unit holding 6 events, zeros on an empty one, and THOR reads it BEFORE the walk then downloads exactly six events without ever reading the chain sentinel. If it holds it lets a caller decide whether to walk at all, which over cellular is the useful part. Two samples on one unit, and content[4:8] reads 9 unexplained, so list_events() still walks to the sentinel: slower by one round trip and correct on evidence rather than inference. Full suite: 465 passed, 16 pre-existing failures unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ru8Lg9HkkYvX9VWWo65SmL |
||
|
|
f09b7dcaaf |
feat(micromate): client layer -- connect, state, setups; read-only
Step 3 of docs/micromate_client_spec.md: micromate/client.py, two models in micromate/models.py, 26 offline tests. Every response constant in the tests is a real captured data section from UM12947. Field offsets were measured rather than taken from the spec, which turned up one general rule and one trap: THE RESPONSE SHAPE. Every response carries an 11-byte prefix and content starts at data[11]. One rule, every command. THE TRAP: data[0] is the content length & 0xFF, with no high byte anywhere in the prefix. It is therefore correct for every response under 256 bytes -- most of them -- and then reports 44 for a 2,092-byte setup block, 30 for a 286-byte monitor-log record, and 0 for a 1,024-byte download chunk. 182 of 251 captured responses agree with a naive read; the 69 that disagree are exactly the ones >= 256 bytes. That is the third length in this protocol read too narrow, after payload[9]-vs-payload[8:10] in the probe response. The client takes content as data[11:] and lets the frame's own length bound it -- nothing needs the declared length, since the frame already knows how long it is. Also measured: - POLL content[3] is 0x50, printable as "P", immediately before "Instantel". A generic printable-run scan therefore returns "PInstantel" -- it caught a test, not a unit. Vendor comes from a fixed offset; the model is found by searching for "MM/", which is structural rather than positional and so survives the Thor line's shorter "MM/ISEE/S". - The setup walk terminates on an EMPTY NAME, not an error: 23 responses, 22 names, factory.MMB first through TEST1.mmb last. - The 0x1C clock has an unidentified byte at content[6]; the hour is at content[7]. The protocol reference's 0x1C section already had this right and names the byte -- its one-line summary in the divergences list reads as six contiguous fields and is the version not to trust. Re-verified against three captures: 19:12:25, 19:13:34 and 01:14:05 against filenames stamped 19:12:14, 19:12:14 and 01:14:03. - Battery and memory are read FORWARD from content start, never backward from the end. This block is 4 bytes longer on the Thor line; the Series III from-the-end offsets give a 11.0BD unit 577.92 V. A test appends the four trailing bytes and asserts the forward offsets survive. connect() is narrower than the spec asked. The spec said to mirror Thor's POLL -> SERIAL -> 0x49 -> POLL "because it is known-good"; measurement showed that is Thor's connection check (3 of 8 sessions) and its fourth frame repeats its first. So connect() sends the three reads that gather something, and 0x01 is not read at all -- Thor never reads it, its layout is unmapped, and firmware_line comes free from any response's flags byte. If a unit ever refuses the next command after a cold connect, put the fourth POLL back and record it. A dead clock battery yields device_time=None rather than failing the whole state read; an unreadable active setup yields active_setup=None rather than failing connect. Both are real device states. Still verified only against 11.0CB and only over USB. The BD offsets follow from the extra bytes being trailing, which is documented but not something this code has seen. Full suite unchanged at 16 pre-existing failures; 445 passed, up 26. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ru8Lg9HkkYvX9VWWo65SmL |
||
|
|
1ed86244d0 | fix(thor-events): add parallel field for mic psi. Now shows mic in dbl and psi. (psi for charts) | ||
|
|
ecc935482b |
seismo-relay v0.19.0 — device-family separation + micromate/ package
Tighten the Series III / Series IV boundary so UI and storage dispatch
on a clean signal instead of sniffing filenames or applying magnitude
heuristics.
Phase 1 — events.device_family column ("series3" | "series4"):
self-applying migration with filename-based backfill of existing rows
(1,132 backfilled on prod 2026-05-20); plumbed through every import
path (BW endpoint, IDF endpoint, ACH server, BW CLI, sidecar
backfill); UPSERT preserves via COALESCE; UI dispatches on it.
Phase 2 — extract micromate/ package alongside minimateplus/:
native IdfEvent / IdfReport / IdfPeaks / IdfProjectInfo /
IdfSensorCheck (mic in dB(L), not pseudo-psi); moved
idf_ascii_report.py from sfm/ to micromate/; refactored
save_imported_idf to use IdfEvent and bridge to minimateplus.Event at
the SQL-insert boundary; idf_file.py stub for the future binary codec.
Phase 3 prep — docs/idf_protocol_reference.md captures the two
observed Thor binary header signatures (1,012 newer-firmware files vs
2 old files whose layout is byte-for-byte BW-STRT-compatible), file-size
hints suggesting int8 sample encoding, open questions in dependency
order, and a concrete first-session plan for cracking the codec.
Also rolled in the v0.18.1 hotfixes that motivated this work:
- idf_ascii_report parser now handles "<0.005 in/s" (below-threshold)
and "N/A" markers without leaving raw strings in numeric DB columns.
- sfm_webapp.html: defensive _ppvFmt / mic formatter so future
data-shape drift can't kill the whole events table render.
All 1,014 example-data sidecars round-trip through the new package.
See CHANGELOG.md for full notes.
|