fix: add STRT invalid detction, ach server passes config for get events,
This commit is contained in:
@@ -191,6 +191,18 @@ rectime_seconds = int(round((total_samples - pretrig_samples) / sample_rate))
|
||||
`_decode_a5_waveform` uses `sample_rate=1024` as a default; the server overrides with
|
||||
`compliance_config.sample_rate` when available. 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
|
||||
- When STRT layout is invalid, `_decode_a5_waveform` falls back to pretrig = 0.25 × sr
|
||||
- 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.
|
||||
|
||||
**Sanity check — pretrig_samples must be less than total_samples:**
|
||||
If `pretrig_samples >= total_samples` the STRT parse is invalid. Possible causes:
|
||||
DLE-stuffed `0x10` byte within prev_key4 or key4 shifted field offsets, or a different
|
||||
|
||||
@@ -371,6 +371,7 @@ class AchSession:
|
||||
full_waveform=True,
|
||||
stop_after_index=stop_idx,
|
||||
skip_waveform_for_keys=seen_keys if seen_keys else None,
|
||||
compliance_config=device_info.compliance_config if device_info else None,
|
||||
)
|
||||
|
||||
# Filter to events whose keys we haven't saved before.
|
||||
|
||||
+67
-4
@@ -448,7 +448,7 @@ class MiniMateClient:
|
||||
proto.confirm_erase_all()
|
||||
log.info("delete_all_events: erase confirmed — device memory cleared")
|
||||
|
||||
def get_events(self, full_waveform: bool = False, debug: bool = False, stop_after_index: Optional[int] = None, skip_waveform_for_keys: Optional[set] = None) -> list[Event]:
|
||||
def get_events(self, full_waveform: bool = False, debug: bool = False, stop_after_index: Optional[int] = None, skip_waveform_for_keys: Optional[set] = None, compliance_config: Optional["ComplianceConfig"] = None) -> list[Event]:
|
||||
"""
|
||||
Download all stored events from the device using the confirmed
|
||||
1E → 0A → 0C → 5A → 1F event-iterator protocol.
|
||||
@@ -479,6 +479,7 @@ class MiniMateClient:
|
||||
ProtocolError: on unrecoverable communication failure.
|
||||
"""
|
||||
proto = self._require_proto()
|
||||
_compliance_config = compliance_config # passed through to _decode_a5_waveform
|
||||
|
||||
log.info("get_events: requesting first event (SUB 1E)")
|
||||
try:
|
||||
@@ -608,7 +609,7 @@ class MiniMateClient:
|
||||
if a5_frames:
|
||||
a5_ok = True
|
||||
_decode_a5_metadata_into(a5_frames, ev)
|
||||
_decode_a5_waveform(a5_frames, ev)
|
||||
_decode_a5_waveform(a5_frames, ev, compliance_config=_compliance_config)
|
||||
log.info(
|
||||
"get_events: 5A decoded %d sample-sets",
|
||||
len((ev.raw_samples or {}).get("Tran", [])),
|
||||
@@ -1311,6 +1312,7 @@ def _decode_a5_metadata_into(frames_data: list[bytes], event: Event) -> None:
|
||||
def _decode_a5_waveform(
|
||||
frames_data: list[bytes],
|
||||
event: Event,
|
||||
compliance_config: Optional["ComplianceConfig"] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Decode the raw 4-channel ADC waveform from a complete set of SUB 5A
|
||||
@@ -1397,15 +1399,19 @@ def _decode_a5_waveform(
|
||||
|
||||
# Sanity check: pretrig must be less than total_samples.
|
||||
# If not, the STRT layout is suspect (DLE-stuffing shift, different record variant, etc.).
|
||||
# Log the raw bytes for diagnosis and clamp pretrig to 0 so the viewer renders.
|
||||
# Log the raw bytes for diagnosis and clamp pretrig to 0 for now — will try to derive
|
||||
# a better value from the compliance config after the full waveform is decoded.
|
||||
_strt_invalid = False
|
||||
if pretrig_samples >= total_samples:
|
||||
log.warning(
|
||||
"_decode_a5_waveform: pretrig_samples=%d >= total_samples=%d — "
|
||||
"STRT layout suspect. Raw strt[0:21]: %s "
|
||||
"Clamping pretrig to 0 for rendering.",
|
||||
"Will attempt to derive pretrig from compliance config after decode.",
|
||||
pretrig_samples, total_samples, strt[0:21].hex(' '),
|
||||
)
|
||||
pretrig_samples = 0
|
||||
total_samples = 0 # also invalid; will be filled from decoded count below
|
||||
_strt_invalid = True
|
||||
|
||||
# strt[18] is a record-mode/type byte, NOT rectime in seconds.
|
||||
# Confirmed from analysis of 4-9-26 ACH capture (15 distinct events):
|
||||
@@ -1534,6 +1540,63 @@ def _decode_a5_waveform(
|
||||
"Mic": mic,
|
||||
}
|
||||
|
||||
# ── Correct STRT-invalid pretrig/total from decoded count + compliance config ──
|
||||
# When the STRT layout was suspect (pretrig >= total_samples), both values were
|
||||
# zeroed out earlier. Now that we know the actual decoded sample count, use it
|
||||
# together with the compliance config record_time to derive a meaningful pretrig.
|
||||
#
|
||||
# Formula:
|
||||
# post_trig = record_time (s) × sample_rate (sps)
|
||||
# pretrig = decoded_samples − post_trig
|
||||
#
|
||||
# This gives the pre-trigger window length, which correctly places t=0 in the
|
||||
# waveform. If compliance_config is not available, leave pretrig=0 (viewer shows
|
||||
# full waveform starting at t=0 — better than a crash or garbage).
|
||||
n_decoded = len(tran)
|
||||
if _strt_invalid:
|
||||
if compliance_config is not None:
|
||||
cc_sr = compliance_config.sample_rate or 1024
|
||||
cc_rt = compliance_config.record_time
|
||||
# Pre-trigger time is a separate device setting from Record Time.
|
||||
# Blastware documentation confirms the compliance monitoring standard
|
||||
# is 0.25 seconds pre-trigger ("the default Time Scale is -0.25 to 1
|
||||
# second — this negative number accounts for the pre-trigger set for
|
||||
# compliance monitoring").
|
||||
# The pre-trigger field has not yet been located in the raw compliance
|
||||
# config bytes; 0.25 s is used as the best-known default until it is
|
||||
# decoded. TODO: locate pretrig_time in ComplianceConfig bytes.
|
||||
_PRETRIG_SECONDS_DEFAULT = 0.25
|
||||
derived_pretrig = int(round(_PRETRIG_SECONDS_DEFAULT * cc_sr))
|
||||
if cc_rt and cc_rt > 0:
|
||||
post_trig_samples = int(round(cc_rt * cc_sr))
|
||||
# Clip total to pretrig + post_trig so the viewer doesn't show the
|
||||
# zero-padded tail frames the device appends beyond the record window.
|
||||
event.total_samples = derived_pretrig + post_trig_samples
|
||||
event.pretrig_samples = derived_pretrig
|
||||
event.rectime_seconds = int(round(cc_rt))
|
||||
log.info(
|
||||
"_decode_a5_waveform: STRT was invalid — using pretrig=%d "
|
||||
"(%.2fs default) from compliance standard; record_time=%.1fs "
|
||||
"sr=%d sps decoded=%d samples display_total=%d",
|
||||
derived_pretrig, _PRETRIG_SECONDS_DEFAULT,
|
||||
cc_rt, cc_sr, n_decoded, event.total_samples,
|
||||
)
|
||||
else:
|
||||
event.total_samples = n_decoded
|
||||
event.pretrig_samples = derived_pretrig
|
||||
log.warning(
|
||||
"_decode_a5_waveform: STRT invalid, compliance config missing "
|
||||
"record_time — pretrig=%d (%.2fs default), total=decoded %d",
|
||||
derived_pretrig, _PRETRIG_SECONDS_DEFAULT, n_decoded,
|
||||
)
|
||||
else:
|
||||
event.total_samples = n_decoded
|
||||
log.warning(
|
||||
"_decode_a5_waveform: STRT invalid, no compliance config available "
|
||||
"— pretrig left as 0, total set to decoded count %d",
|
||||
n_decoded,
|
||||
)
|
||||
|
||||
|
||||
def _extract_record_type(data: bytes) -> Optional[str]:
|
||||
"""
|
||||
|
||||
+5
-1
@@ -662,7 +662,11 @@ def device_event_waveform(
|
||||
with _build_client(port, baud, host, tcp_port, timeout=120.0) as client:
|
||||
info = client.connect()
|
||||
# stop_after_index avoids downloading events beyond the one requested.
|
||||
events = client.get_events(full_waveform=True, stop_after_index=index)
|
||||
events = client.get_events(
|
||||
full_waveform=True,
|
||||
stop_after_index=index,
|
||||
compliance_config=info.compliance_config if info else None,
|
||||
)
|
||||
matching = [ev for ev in events if ev.index == index]
|
||||
return matching[0] if matching else None, info
|
||||
ev, info = _run_with_retry(_do, is_tcp=_is_tcp(host))
|
||||
|
||||
@@ -483,9 +483,14 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Build time axis (ms)
|
||||
const times = Array.from({ length: decoded }, (_, i) =>
|
||||
((i - pretrig) / sr * 1000).toFixed(2)
|
||||
// Clip to total_samples to exclude zero-padding the device appends beyond
|
||||
// the configured record window. total = pretrig + post_trig (e.g. 256+3072=3328).
|
||||
// decoded may be larger (e.g. 4495) due to trailing zero-padded bulk-stream frames.
|
||||
const displayCount = (total > 0 && total < decoded) ? total : decoded;
|
||||
|
||||
// Build time axis in seconds (matching Blastware event report layout).
|
||||
const times = Array.from({ length: displayCount }, (_, i) =>
|
||||
((i - pretrig) / sr).toFixed(3)
|
||||
);
|
||||
|
||||
// Show charts area
|
||||
@@ -517,11 +522,16 @@
|
||||
const isGeo = ch !== 'Mic';
|
||||
let plotSamples, peakLabel, yUnit, tooltipFmt, tickFmt;
|
||||
|
||||
// Clip channel samples to displayCount (same as time axis)
|
||||
const clippedSamples = samples.length > displayCount
|
||||
? samples.slice(0, displayCount)
|
||||
: samples;
|
||||
|
||||
if (isGeo) {
|
||||
// Geo channels: counts × (range / 32767) → in/s
|
||||
// Scale factor for the waveform shape (may need calibration per unit)
|
||||
const scale = geoRange / 32767;
|
||||
plotSamples = samples.map(c => c * scale);
|
||||
plotSamples = clippedSamples.map(c => c * scale);
|
||||
|
||||
// Use the device-computed 0C record peak for the label (authoritative).
|
||||
// The raw-sample-computed peak can be inflated by frame-boundary artifacts.
|
||||
@@ -535,11 +545,11 @@
|
||||
tickFmt = v => v.toFixed(4);
|
||||
} else {
|
||||
// Mic: derive psi/count scale from the 0C peak value, display as psi; show dBL in header
|
||||
const peakCounts = Math.max(...samples.map(Math.abs));
|
||||
const peakCounts = Math.max(...clippedSamples.map(Math.abs));
|
||||
const micScale = (micPeakPsi !== null && peakCounts > 0)
|
||||
? Math.abs(micPeakPsi) / peakCounts
|
||||
: 1.0;
|
||||
plotSamples = samples.map(c => c * micScale);
|
||||
plotSamples = clippedSamples.map(c => c * micScale);
|
||||
const peakPsi = Math.max(...plotSamples.map(Math.abs));
|
||||
const peakDbl = peakPsi > 0 ? 20 * Math.log10(peakPsi / DBL_REF_PSI) : -Infinity;
|
||||
peakLabel = `${peakDbl.toFixed(1)} dBL (${peakPsi.toExponential(3)} psi)`;
|
||||
@@ -597,7 +607,7 @@
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
callbacks: {
|
||||
title: items => `t = ${items[0].label} ms`,
|
||||
title: items => `t = ${items[0].label} s`,
|
||||
label: item => tooltipFmt(item.raw),
|
||||
},
|
||||
},
|
||||
@@ -609,7 +619,7 @@
|
||||
color: '#484f58',
|
||||
maxTicksLimit: 10,
|
||||
maxRotation: 0,
|
||||
callback: (val, i) => renderTimes[i] + ' ms',
|
||||
callback: (val, i) => renderTimes[i] + ' s',
|
||||
},
|
||||
grid: { color: '#21262d' },
|
||||
},
|
||||
@@ -647,7 +657,7 @@
|
||||
const xAxis = chart.scales.x;
|
||||
const yAxis = chart.scales.y;
|
||||
|
||||
// Find index of t=0
|
||||
// Find index of the trigger point (t ≥ 0.000 s)
|
||||
const zeroIdx = renderTimes.findIndex(t => parseFloat(t) >= 0);
|
||||
if (zeroIdx < 0) return;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user