From a84a46e9d47d6f8b395f161cc668284486b531c0 Mon Sep 17 00:00:00 2001 From: serversdown Date: Sun, 20 Sep 2026 18:21:07 +0000 Subject: [PATCH] fix(decode): stamp waveform events with the event time, not the session start MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit read_blastware_file built ev.timestamp from footer ts1, which for a WAVEFORM is the monitoring-session start (a unit arming at 06:00 stamps 06:00 on every event that day) — so every waveform's time was hours off (vomit-list #3, "~4.5 h off"). The event time is footer ts2 (the recording stop); BW's displayed Date/Time is the trigger = ts2 - record duration. Root cause proven against the BE12844 oracle set: 5 of 7 events decoded to the identical 06:00:13 (the shared session start); ts2 gives distinct plausible event times (N844LQHB ts2 = 10:33:32, BW trigger 10:33:29 = ts2 - 3.0 s rectime). * read_blastware_file now uses ts2 for waveforms (discriminated by which codec decoded the body, not the filename — save_imported_bw passes a tmp name). Histograms keep ts1 (the ~24 h window start, which IS the event time). * Binary-only decode can't get the exact trigger: the STRT record-time byte is a misparsed record-type marker (0x46=70), so ts2 (the stop, ~record duration after the trigger) is the best estimate. A paired BW report carries the exact trigger — apply_report_to_event now overlays event.timestamp from report.event_datetime, matching the existing build-path override (line ~441). Tests: waveform → ts2, histogram → ts1 unchanged, report → exact trigger. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01YDXjZCr4RqT2U3QvMDhgzf --- minimateplus/event_file_io.py | 37 ++++++++++++++++++- tests/fixtures/ts-fix/K441LKZU.C30H | Bin 0 -> 47586 bytes tests/test_event_timestamp.py | 53 ++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 tests/fixtures/ts-fix/K441LKZU.C30H create mode 100644 tests/test_event_timestamp.py diff --git a/minimateplus/event_file_io.py b/minimateplus/event_file_io.py index 188df85..082e7ee 100644 --- a/minimateplus/event_file_io.py +++ b/minimateplus/event_file_io.py @@ -296,6 +296,16 @@ def apply_report_to_event(event: Event, report: BwAsciiReport) -> None: event.sample_rate = report.sample_rate_sps if report.record_time_s is not None: event.rectime_seconds = report.record_time_s + # The report's event_datetime is Blastware's exact trigger time (parsed + # from Event Time + Event Date). Prefer it over the binary footer's stop + # time so a report-paired import matches BW to the second. + edt = report.event_datetime + if edt is not None: + event.timestamp = Timestamp( + raw=b"", flag=0x10, + year=edt.year, unknown_byte=0, month=edt.month, day=edt.day, + hour=edt.hour, minute=edt.minute, second=edt.second, + ) def apply_bw_report_dict_to_event(event: Event, bw_report: dict) -> None: @@ -917,6 +927,10 @@ def read_blastware_file(path: Union[str, Path]) -> Event: # rest of the event (timestamp, waveform_key, project strings) is # still recoverable and useful. decoded = decode_waveform_v2(body) + # Discriminator for the timestamp logic below: a waveform (trigger) event + # vs a histogram window. Keyed on the codec, not the filename — the + # save_imported_bw path passes a tmp ".bw" name whose extension lies. + is_waveform_body = decoded is not None if decoded is None: decoded = decode_histogram_body(body) if decoded is None: @@ -948,7 +962,28 @@ def read_blastware_file(path: Union[str, Path]) -> Event: ev.total_samples = strt_fields.get("total_samples") ev.pretrig_samples = strt_fields.get("pretrig_samples") - if ts1 is not None: + # Event timestamp. The footer's two timestamps mean different things by + # record type: + # * Waveform: ts1 = the monitoring-SESSION start (shared across every + # event that day — a unit arming at 06:00 stamps 06:00 on all of them), + # ts2 = THIS event's recording STOP. ts2 is the correct binary-only + # estimate of the event time; BW's displayed Date/Time is the trigger = + # ts2 - record duration (~3 s), but the record-duration byte in the STRT + # record is a misparsed record-type marker here (see the strt-build + # note), so the exact trigger comes from the paired BW report's + # event_datetime — apply_report_to_event() overrides with it when a + # report is present. (Stamping ts1 showed the session start, hours off.) + # * Histogram / undecodable: ts1 = the window start, which IS the event + # time — keep it. + # Discriminate by ``is_waveform_body`` (the codec), not the filename. + if is_waveform_body and ts2 is not None: + ev.timestamp = Timestamp( + raw=footer[10:18], + flag=0x10, + year=ts2.year, unknown_byte=0, month=ts2.month, day=ts2.day, + hour=ts2.hour, minute=ts2.minute, second=ts2.second, + ) + elif ts1 is not None: ev.timestamp = Timestamp( raw=footer[2:10], flag=0x10, diff --git a/tests/fixtures/ts-fix/K441LKZU.C30H b/tests/fixtures/ts-fix/K441LKZU.C30H new file mode 100644 index 0000000000000000000000000000000000000000..36ef2e1c252c2c48d11393abbe4d57ec8d6cfd27 GIT binary patch literal 47586 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 A7XSbN literal 0 HcmV?d00001 diff --git a/tests/test_event_timestamp.py b/tests/test_event_timestamp.py new file mode 100644 index 0000000..06ef82f --- /dev/null +++ b/tests/test_event_timestamp.py @@ -0,0 +1,53 @@ +"""Event timestamp decode — waveform trigger/stop vs histogram window start. + +The Blastware footer holds two timestamps: ts1 = footer[2:10], ts2 = footer[10:18]. +Their meaning depends on record type: + + * Waveform: ts1 is the monitoring-SESSION start (e.g. 06:00 for a unit that + arms at 06:00 daily — shared across every event that day), and ts2 is THIS + event's recording STOP. read_blastware_file used to stamp events with ts1 → + every waveform showed the session start (~4.5 h off). Binary-only, the best + estimate is ts2 (the stop); the exact trigger BW displays (= ts2 - record + duration) comes from the paired report's event_datetime, since the binary + STRT record-time byte is a misparsed record-type marker. + * Histogram: ts1/ts2 are the ~24 h window [start, stop]; the event time is the + window start = ts1 (unchanged). +""" +import datetime +from pathlib import Path + +from minimateplus.event_file_io import read_blastware_file, apply_report_to_event +from minimateplus.bw_ascii_report import BwAsciiReport +from minimateplus.models import Event + +FIX = Path(__file__).parent / "fixtures" +WAVEFORM = FIX / "fft-oracle-2026-09-14" / "N844LQHB.ZT0W" # footer ts2 = 2026-08-25 10:33:32 +HISTOGRAM = FIX / "ts-fix" / "K441LKZU.C30H" # window start 2026-05-10 19:04:50 + + +def _tuple(ts): + return (ts.year, ts.month, ts.day, ts.hour, ts.minute, ts.second) + + +def test_waveform_timestamp_is_event_stop_not_session_start(): + ev = read_blastware_file(WAVEFORM) + # ts2 (the event's recording stop), NOT the 06:00:13 monitoring-session + # start the old decode used. + assert _tuple(ev.timestamp) == (2026, 8, 25, 10, 33, 32), _tuple(ev.timestamp) + + +def test_histogram_timestamp_is_window_start_unchanged(): + ev = read_blastware_file(HISTOGRAM) + # Histogram event time = the window start (ts1); must NOT get the waveform + # ts2 treatment (that would land ~24 h off). + assert _tuple(ev.timestamp) == (2026, 5, 10, 19, 4, 50), _tuple(ev.timestamp) + + +def test_report_event_datetime_overrides_to_exact_trigger(): + # A paired BW report carries the exact trigger time; applying it must + # override the binary footer's stop time so the event matches BW exactly. + ev = read_blastware_file(WAVEFORM) + assert _tuple(ev.timestamp) == (2026, 8, 25, 10, 33, 32) # stop, pre-report + apply_report_to_event(ev, BwAsciiReport( + event_datetime=datetime.datetime(2026, 8, 25, 10, 33, 29))) + assert _tuple(ev.timestamp) == (2026, 8, 25, 10, 33, 29) # exact BW trigger