Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6eecd0c1d1 | |||
| 870a10365e | |||
| b2d10fd689 | |||
| ce44852383 |
+46
-1
@@ -121,9 +121,10 @@ class MiniMateClient:
|
||||
2. SUB 15 — serial number
|
||||
3. SUB 01 — full config block (firmware, model strings)
|
||||
4. SUB 1A — compliance config (record time, trigger/alarm levels, project strings)
|
||||
5. SUB 08 — event index (stored event count)
|
||||
|
||||
Returns:
|
||||
Populated DeviceInfo with compliance_config cached.
|
||||
Populated DeviceInfo with compliance_config and event_count cached.
|
||||
|
||||
Raises:
|
||||
ProtocolError: on any communication failure.
|
||||
@@ -151,6 +152,14 @@ class MiniMateClient:
|
||||
except ProtocolError as exc:
|
||||
log.warning("connect: compliance config read failed: %s — continuing", exc)
|
||||
|
||||
log.info("connect: reading event index (SUB 08)")
|
||||
try:
|
||||
idx_raw = proto.read_event_index()
|
||||
device_info.event_count = _decode_event_count(idx_raw)
|
||||
log.info("connect: device has %d stored event(s)", device_info.event_count)
|
||||
except ProtocolError as exc:
|
||||
log.warning("connect: event index read failed: %s — continuing", exc)
|
||||
|
||||
log.info("connect: %s", device_info)
|
||||
return device_info
|
||||
|
||||
@@ -682,6 +691,42 @@ def _decode_compliance_config_into(data: bytes, info: DeviceInfo) -> None:
|
||||
except Exception as exc:
|
||||
log.warning("compliance_config: project string extraction failed: %s", exc)
|
||||
|
||||
# ── Channel block: trigger_level_geo, alarm_level_geo, max_range_geo ─────
|
||||
# The channel block is only present in the full cfg (frame D delivered,
|
||||
# ~2126 bytes). Per §7.6, each channel record ends with the label string:
|
||||
# [00 00][max_range f32][00 00][trigger f32]["in.\0"][alarm f32]["/s\0\0"][00 01][label]
|
||||
# Relative offsets from the "Tran" label position:
|
||||
# trigger = float32_BE at label - 18
|
||||
# alarm = float32_BE at label - 10
|
||||
# max_range = float32_BE at label - 24
|
||||
# Validated by checking unit strings "in.\0" at label-14 and "/s\0\0" at label-6.
|
||||
# "Tran2" at a later position won't match because its surrounding bytes differ.
|
||||
try:
|
||||
tran_pos = data.find(b"Tran", 1000)
|
||||
if (
|
||||
tran_pos >= 24
|
||||
and data[tran_pos + 4 : tran_pos + 5] != b"2" # not "Tran2"
|
||||
and data[tran_pos - 14 : tran_pos - 10] == b"in.\x00"
|
||||
and data[tran_pos - 6 : tran_pos - 2 ] == b"/s\x00\x00"
|
||||
):
|
||||
config.trigger_level_geo = struct.unpack_from(">f", data, tran_pos - 18)[0]
|
||||
config.alarm_level_geo = struct.unpack_from(">f", data, tran_pos - 10)[0]
|
||||
config.max_range_geo = struct.unpack_from(">f", data, tran_pos - 24)[0]
|
||||
log.debug(
|
||||
"compliance_config: trigger=%.4f alarm=%.4f max_range=%.4f in/s",
|
||||
config.trigger_level_geo, config.alarm_level_geo, config.max_range_geo,
|
||||
)
|
||||
elif tran_pos >= 0:
|
||||
log.debug(
|
||||
"compliance_config: 'Tran' at %d but unit strings absent "
|
||||
"— channel block not yet in cfg (frame D duplicate?)",
|
||||
tran_pos,
|
||||
)
|
||||
else:
|
||||
log.debug("compliance_config: channel block not present in cfg (len=%d)", len(data))
|
||||
except Exception as exc:
|
||||
log.warning("compliance_config: channel block extraction failed: %s", exc)
|
||||
|
||||
info.compliance_config = config
|
||||
|
||||
|
||||
|
||||
@@ -183,6 +183,9 @@ class DeviceInfo:
|
||||
# ── From SUB 1A (COMPLIANCE_CONFIG_RESPONSE) ──────────────────────────────
|
||||
compliance_config: Optional["ComplianceConfig"] = None # E5 response, read in connect()
|
||||
|
||||
# ── From SUB 08 (EVENT_INDEX_RESPONSE) ────────────────────────────────────
|
||||
event_count: Optional[int] = None # stored event count from F7 response 🔶
|
||||
|
||||
def __str__(self) -> str:
|
||||
fw = self.firmware_version or f"?.{self.firmware_minor}"
|
||||
mdl = self.model or "MiniMate Plus"
|
||||
|
||||
@@ -240,6 +240,41 @@ class MiniMateProtocol:
|
||||
|
||||
# ── Event download API ────────────────────────────────────────────────────
|
||||
|
||||
def read_event_index(self) -> bytes:
|
||||
"""
|
||||
Send the SUB 08 (EVENT_INDEX) two-step read and return the raw 88-byte
|
||||
(0x58) index block.
|
||||
|
||||
The index block contains:
|
||||
+0x00 (3 bytes): total index size or record count — purpose partially
|
||||
decoded; byte [3] may be a high byte of event count.
|
||||
+0x03 (4 bytes): stored event count as uint32 BE ❓ (inferred from
|
||||
captures; see §7.4 in protocol reference)
|
||||
+0x07 onwards: 6-byte event timestamps (see §8), one per event
|
||||
|
||||
Caller is responsible for parsing the returned bytes.
|
||||
|
||||
Returns:
|
||||
Raw 88-byte data section (data[11:11+0x58]).
|
||||
|
||||
Raises:
|
||||
ProtocolError: on timeout, bad checksum, or wrong response SUB.
|
||||
"""
|
||||
rsp_sub = _expected_rsp_sub(SUB_EVENT_INDEX)
|
||||
length = DATA_LENGTHS[SUB_EVENT_INDEX] # 0x58
|
||||
|
||||
log.debug("read_event_index: 08 probe")
|
||||
self._send(build_bw_frame(SUB_EVENT_INDEX, 0))
|
||||
self._recv_one(expected_sub=rsp_sub)
|
||||
|
||||
log.debug("read_event_index: 08 data request offset=0x%02X", length)
|
||||
self._send(build_bw_frame(SUB_EVENT_INDEX, length))
|
||||
data_rsp = self._recv_one(expected_sub=rsp_sub)
|
||||
|
||||
raw = data_rsp.data[11 : 11 + length]
|
||||
log.debug("read_event_index: got %d bytes", len(raw))
|
||||
return raw
|
||||
|
||||
def read_event_first(self) -> tuple[bytes, bytes]:
|
||||
"""
|
||||
Send the SUB 1E (EVENT_HEADER) two-step read and return the first
|
||||
|
||||
@@ -145,6 +145,7 @@ def _serialise_device_info(info: DeviceInfo) -> dict:
|
||||
"dsp_version": info.dsp_version,
|
||||
"manufacturer": info.manufacturer,
|
||||
"model": info.model,
|
||||
"event_count": info.event_count,
|
||||
"compliance_config": _serialise_compliance_config(info.compliance_config),
|
||||
}
|
||||
|
||||
@@ -315,6 +316,13 @@ def device_events(
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=500, detail=f"Device error: {exc}") from exc
|
||||
|
||||
# Fill sample_rate from compliance config where the event record doesn't supply it.
|
||||
# sample_rate is a device-level setting, not stored per-event in the waveform record.
|
||||
if info.compliance_config and info.compliance_config.sample_rate:
|
||||
for ev in events:
|
||||
if ev.sample_rate is None:
|
||||
ev.sample_rate = info.compliance_config.sample_rate
|
||||
|
||||
return {
|
||||
"device": _serialise_device_info(info),
|
||||
"event_count": len(events),
|
||||
|
||||
Reference in New Issue
Block a user