"""Blastware sensor self-check waveform decode (minimateplus.sensor_check). Reverse-engineered 2026-09-15 against 7 BE12844 (MiniMate Plus) oracle events. After the main waveform record-chain and the trailing metadata / per-channel calibration records, a series-3 binary carries four length-prefixed records tagged 0x3c-0x3f: the sensor self-check traces the unit records when it pulses each sensor before monitoring (Blastware draws these as the little waveforms in the "Sensor Check" strip on the right of the Event Report). * 0x3c / 0x3d / 0x3e = Tran / Vert / Long geophone ring-downs. * 0x3f = MicL, a pulse train at the mic self-test frequency. The self-check injects a fixed pulse, so the response is near-identical across events — asserted here as an invariant shape (damped one-sided ring-down for the geophones, a multi-pulse train for the mic). """ from pathlib import Path import numpy as np from minimateplus.sensor_check import decode_sensor_check FIXDIR = Path(__file__).parent / "fixtures" / "fft-oracle-2026-09-14" EVENTS = sorted(p.name for p in FIXDIR.iterdir()) # 7 BE12844 event binaries def _decode(name): return decode_sensor_check((FIXDIR / name).read_bytes()) def test_all_four_channels_present(): for name in EVENTS: sc = _decode(name) assert set(sc) == {"Tran", "Vert", "Long", "MicL"}, name def test_geo_channels_are_damped_ringdowns(): # Each geophone self-check is a large one-sided deflection (~-990 raw) that # rings back and damps toward a settled value well above the trough. for name in EVENTS: sc = _decode(name) for ch in ("Tran", "Vert", "Long"): tr = np.asarray(sc[ch], dtype=float) assert 240 <= len(tr) <= 260, f"{name}:{ch} n={len(tr)}" assert abs(tr[:3].mean()) < 50, f"{name}:{ch} starts off-baseline" assert tr.min() < -800, f"{name}:{ch} min {tr.min()}" assert tr.max() < 60, f"{name}:{ch} unexpected positive swing {tr.max()}" # damped: settles between the trough and zero, well above the trough assert tr.min() < tr[-1] < 0, f"{name}:{ch} end {tr[-1]} not between trough and 0" assert abs(tr[-1]) < 0.6 * abs(tr.min()), f"{name}:{ch} not damped, end {tr[-1]}" def test_mic_channel_is_a_pulse_train(): for name in EVENTS: tr = np.asarray(_decode(name)["MicL"], dtype=float) assert 235 <= len(tr) <= 255, f"{name} mic n={len(tr)}" # larger dynamic range than the geo ring-down, and swings both ways assert tr.min() < -1500, f"{name} mic min {tr.min()}" assert tr.max() > 100, f"{name} mic max {tr.max()}" # multiple pulses: several deep local minima deep = (tr[1:-1] < tr[:-2]) & (tr[1:-1] < tr[2:]) & (tr[1:-1] < -800) assert int(deep.sum()) >= 4, f"{name} mic pulses {int(deep.sum())}" def test_returns_empty_when_no_sensor_check_block(): assert decode_sensor_check(b"not a blastware file") == {} assert decode_sensor_check(b"") == {}