v0.12.6 #10
@@ -1096,7 +1096,7 @@ body) because writing a dial string may require DLE escaping for embedded contro
|
|||||||
|
|
||||||
- **Database** — SQLite store for events + monitor log entries; dedup by key; queryable
|
- **Database** — SQLite store for events + monitor log entries; dedup by key; queryable
|
||||||
- **Histograms** — decode histogram-mode A5 data (noise floor tracking)
|
- **Histograms** — decode histogram-mode A5 data (noise floor tracking)
|
||||||
- **Blastware-compatible file output** — `write_n00()` and `write_mlg()` implemented (v0.12.3+). `write_n00` verified byte-perfect vs M529LIY6.N00. Extension mapping: **CONFIRMED FALSE 2026-04-21** — extensions are NOT based on recording mode. A continuous-mode event produced `.EI0`, not `.9T0`. The extension alphabet/encoding scheme is unknown; do not infer recording mode from extension or vice versa. Observed extensions: `.N00`, `.9T0`, `.EI0`, `.490`, `.5K0`, `.980`, `.ML0` — mapping to recording mode × sample rate × other settings is unknown. Filename format: `<prefix_letter><serial3><4-char-base36-stem><ext>`
|
- **Blastware-compatible file output** — `write_blastware_file()` and `write_mlg()` implemented. `blastware_filename()` generates correct Blastware filenames (AB0 for direct, AB0W/AB0H for ACH). **Confirmed working for Continuous mode events (2026-04-23):** SFM-generated file opens in Blastware, shows correct PPV/waveform/timestamp. File is ~200 bytes shorter than BW (missing last ADC tail slice) — all measurements correct. Histogram+Continuous mode deferred (5A stream for those events embeds histogram interval records that create spurious STRT markers in the body). Extension mapping: **CONFIRMED FALSE 2026-04-21** — extensions encode timestamp (AB0T for ACH, AB0 for direct), NOT recording mode. Filename format: `<prefix_letter><serial3><4-char-base36-stem><ext>`
|
||||||
|
|
||||||
**Serial encoding (CONFIRMED 2026-04-22):** `prefix_letter = chr(ord('B') + floor(serial_numeric / 1000))`, `serial3 = f"{serial_numeric % 1000:03d}"`. Examples: BE6907→H907, BE11529→M529, BE14036→P036, BE17353→S353, BE18003→T003. The prefix letter encodes the production generation (batch of 1000 units).
|
**Serial encoding (CONFIRMED 2026-04-22):** `prefix_letter = chr(ord('B') + floor(serial_numeric / 1000))`, `serial3 = f"{serial_numeric % 1000:03d}"`. Examples: BE6907→H907, BE11529→M529, BE14036→P036, BE17353→S353, BE18003→T003. The prefix letter encodes the production generation (batch of 1000 units).
|
||||||
|
|
||||||
|
|||||||
@@ -1248,9 +1248,25 @@ Two critical differences from `build_bw_frame`:
|
|||||||
> for all keys encountered on this device.
|
> for all keys encountered on this device.
|
||||||
|
|
||||||
The `stop_after_metadata=True` flag causes the loop to stop as soon as `b"Project:"` is
|
The `stop_after_metadata=True` flag causes the loop to stop as soon as `b"Project:"` is
|
||||||
found in the accumulated A5 frame data, typically after 7–9 chunks. A termination frame
|
found in the accumulated A5 frame data, typically after 4–9 chunks. A termination frame
|
||||||
is always sent before returning.
|
is always sent before returning.
|
||||||
|
|
||||||
|
**IMPORTANT — one extra chunk required after "Project:" for valid file footer (confirmed 2026-04-23):**
|
||||||
|
When writing a Blastware-compatible waveform file, stopping immediately at "Project:" and
|
||||||
|
sending termination produces an empty termination response with no footer bytes (`0e 08`
|
||||||
|
marker missing). Blastware downloads exactly **one more chunk** after finding "Project:"
|
||||||
|
before sending termination — that extra chunk primes the device to return valid footer
|
||||||
|
bytes (monitoring start/stop timestamps) in the termination response.
|
||||||
|
|
||||||
|
`read_bulk_waveform_stream(stop_after_metadata=True)` implements this: after the "Project:"
|
||||||
|
chunk is received, one additional chunk is requested before breaking. The termination
|
||||||
|
response (`include_terminator=True`) then contains the correct `0e 08` footer.
|
||||||
|
|
||||||
|
**do NOT use `full_waveform=True` for Blastware file writing** — for events with long
|
||||||
|
post-event silence (35 chunks), the silence chunks contain embedded device-internal
|
||||||
|
pointer structures that produce spurious STRT markers in the file body. Blastware only
|
||||||
|
downloads 4–5 chunks (metadata + one signal chunk) regardless of event length.
|
||||||
|
|
||||||
#### 7.8.3 A5 Frame Layout
|
#### 7.8.3 A5 Frame Layout
|
||||||
|
|
||||||
Each A5 response frame contains a chunk of raw bulk data. Frame 7 of the stream carries the
|
Each A5 response frame contains a chunk of raw bulk data. Frame 7 of the stream carries the
|
||||||
|
|||||||
@@ -449,7 +449,7 @@ class MiniMateClient:
|
|||||||
proto.confirm_erase_all()
|
proto.confirm_erase_all()
|
||||||
log.info("delete_all_events: erase confirmed — device memory cleared")
|
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, extra_chunks_after_metadata: int = 1) -> list[Event]:
|
||||||
"""
|
"""
|
||||||
Download all stored events from the device using the confirmed
|
Download all stored events from the device using the confirmed
|
||||||
1E → 0A → 0C → 5A → 1F event-iterator protocol.
|
1E → 0A → 0C → 5A → 1F event-iterator protocol.
|
||||||
@@ -623,6 +623,7 @@ class MiniMateClient:
|
|||||||
a5_frames = proto.read_bulk_waveform_stream(
|
a5_frames = proto.read_bulk_waveform_stream(
|
||||||
cur_key, stop_after_metadata=True,
|
cur_key, stop_after_metadata=True,
|
||||||
include_terminator=True,
|
include_terminator=True,
|
||||||
|
extra_chunks_after_metadata=extra_chunks_after_metadata,
|
||||||
)
|
)
|
||||||
if a5_frames:
|
if a5_frames:
|
||||||
a5_ok = True
|
a5_ok = True
|
||||||
|
|||||||
@@ -527,6 +527,7 @@ class MiniMateProtocol:
|
|||||||
stop_after_metadata: bool = True,
|
stop_after_metadata: bool = True,
|
||||||
max_chunks: int = 32,
|
max_chunks: int = 32,
|
||||||
include_terminator: bool = False,
|
include_terminator: bool = False,
|
||||||
|
extra_chunks_after_metadata: int = 1,
|
||||||
) -> list[S3Frame]:
|
) -> list[S3Frame]:
|
||||||
"""
|
"""
|
||||||
Download the SUB 5A (BULK_WAVEFORM_STREAM) A5 frames for one event.
|
Download the SUB 5A (BULK_WAVEFORM_STREAM) A5 frames for one event.
|
||||||
@@ -651,7 +652,9 @@ class MiniMateProtocol:
|
|||||||
# and primes the device to return a valid footer in the termination
|
# and primes the device to return a valid footer in the termination
|
||||||
# response. Without it, termination returns an empty ack with no
|
# response. Without it, termination returns an empty ack with no
|
||||||
# footer bytes (confirmed 2026-04-23 from HxD comparison).
|
# footer bytes (confirmed 2026-04-23 from HxD comparison).
|
||||||
log.debug("5A A5[%d] metadata found — fetching one more chunk then stopping", chunk_num)
|
log.debug("5A A5[%d] metadata found — fetching %d more chunk(s) then stopping",
|
||||||
|
chunk_num, extra_chunks_after_metadata)
|
||||||
|
for _extra_n in range(extra_chunks_after_metadata):
|
||||||
chunk_num += 1
|
chunk_num += 1
|
||||||
counter = chunk_num * _BULK_COUNTER_STEP
|
counter = chunk_num * _BULK_COUNTER_STEP
|
||||||
params = bulk_waveform_params(key4, counter)
|
params = bulk_waveform_params(key4, counter)
|
||||||
@@ -666,7 +669,8 @@ class MiniMateProtocol:
|
|||||||
return frames_data
|
return frames_data
|
||||||
frames_data.append(extra)
|
frames_data.append(extra)
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
log.debug("5A extra chunk timed out — end of stream")
|
log.debug("5A extra chunk %d timed out — end of stream", _extra_n + 1)
|
||||||
|
break
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
log.warning(
|
log.warning(
|
||||||
|
|||||||
+16
-7
@@ -885,13 +885,22 @@ def device_event_blastware_file(
|
|||||||
def _do():
|
def _do():
|
||||||
with _build_client(port, baud, host, tcp_port, timeout=120.0) as client:
|
with _build_client(port, baud, host, tcp_port, timeout=120.0) as client:
|
||||||
info = client.connect()
|
info = client.connect()
|
||||||
# Use full_waveform=False (stop_after_metadata=True) — stops when
|
# Calculate extra ADC chunks to download after finding "Project:".
|
||||||
# "Project:" is found in the 5A stream. Content is byte-identical to
|
# BW downloads ~2 extra chunks per second of record time.
|
||||||
# BW for Continuous/Single-Shot events; our file is slightly shorter
|
# Without enough extra chunks the termination response contains no
|
||||||
# (~286 bytes of extra ADC signal BW includes past the metadata).
|
# footer bytes and Blastware rejects the file.
|
||||||
# full_waveform=True corrupts the body: silence chunks past the event
|
rectime = 1.0
|
||||||
# contain device-internal pointers that embed extra STRT records.
|
try:
|
||||||
events = client.get_events(full_waveform=False, stop_after_index=index)
|
rectime = float(info.compliance_config.record_time or 1.0)
|
||||||
|
except (AttributeError, TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
extra_chunks = max(1, round(rectime * 2))
|
||||||
|
log.info("blastware_file: rectime=%.1fs → extra_chunks=%d", rectime, extra_chunks)
|
||||||
|
events = client.get_events(
|
||||||
|
full_waveform=False,
|
||||||
|
stop_after_index=index,
|
||||||
|
extra_chunks_after_metadata=extra_chunks,
|
||||||
|
)
|
||||||
matching = [ev for ev in events if ev.index == index]
|
matching = [ev for ev in events if ev.index == index]
|
||||||
return matching[0] if matching else None, info
|
return matching[0] if matching else None, info
|
||||||
ev, info = _run_with_retry(_do, is_tcp=_is_tcp(host))
|
ev, info = _run_with_retry(_do, is_tcp=_is_tcp(host))
|
||||||
|
|||||||
Reference in New Issue
Block a user