Compare commits
4 Commits
6a42facf02
...
6eecd0c1d1
| Author | SHA1 | Date | |
|---|---|---|---|
| 6eecd0c1d1 | |||
| 870a10365e | |||
| b2d10fd689 | |||
| ce44852383 |
+46
-1
@@ -121,9 +121,10 @@ class MiniMateClient:
|
|||||||
2. SUB 15 — serial number
|
2. SUB 15 — serial number
|
||||||
3. SUB 01 — full config block (firmware, model strings)
|
3. SUB 01 — full config block (firmware, model strings)
|
||||||
4. SUB 1A — compliance config (record time, trigger/alarm levels, project strings)
|
4. SUB 1A — compliance config (record time, trigger/alarm levels, project strings)
|
||||||
|
5. SUB 08 — event index (stored event count)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Populated DeviceInfo with compliance_config cached.
|
Populated DeviceInfo with compliance_config and event_count cached.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ProtocolError: on any communication failure.
|
ProtocolError: on any communication failure.
|
||||||
@@ -151,6 +152,14 @@ class MiniMateClient:
|
|||||||
except ProtocolError as exc:
|
except ProtocolError as exc:
|
||||||
log.warning("connect: compliance config read failed: %s — continuing", 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)
|
log.info("connect: %s", device_info)
|
||||||
return device_info
|
return device_info
|
||||||
|
|
||||||
@@ -682,6 +691,42 @@ def _decode_compliance_config_into(data: bytes, info: DeviceInfo) -> None:
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
log.warning("compliance_config: project string extraction failed: %s", 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
|
info.compliance_config = config
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -183,6 +183,9 @@ class DeviceInfo:
|
|||||||
# ── From SUB 1A (COMPLIANCE_CONFIG_RESPONSE) ──────────────────────────────
|
# ── From SUB 1A (COMPLIANCE_CONFIG_RESPONSE) ──────────────────────────────
|
||||||
compliance_config: Optional["ComplianceConfig"] = None # E5 response, read in connect()
|
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:
|
def __str__(self) -> str:
|
||||||
fw = self.firmware_version or f"?.{self.firmware_minor}"
|
fw = self.firmware_version or f"?.{self.firmware_minor}"
|
||||||
mdl = self.model or "MiniMate Plus"
|
mdl = self.model or "MiniMate Plus"
|
||||||
|
|||||||
@@ -240,6 +240,41 @@ class MiniMateProtocol:
|
|||||||
|
|
||||||
# ── Event download API ────────────────────────────────────────────────────
|
# ── 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]:
|
def read_event_first(self) -> tuple[bytes, bytes]:
|
||||||
"""
|
"""
|
||||||
Send the SUB 1E (EVENT_HEADER) two-step read and return the first
|
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,
|
"dsp_version": info.dsp_version,
|
||||||
"manufacturer": info.manufacturer,
|
"manufacturer": info.manufacturer,
|
||||||
"model": info.model,
|
"model": info.model,
|
||||||
|
"event_count": info.event_count,
|
||||||
"compliance_config": _serialise_compliance_config(info.compliance_config),
|
"compliance_config": _serialise_compliance_config(info.compliance_config),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,6 +316,13 @@ def device_events(
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise HTTPException(status_code=500, detail=f"Device error: {exc}") from 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 {
|
return {
|
||||||
"device": _serialise_device_info(info),
|
"device": _serialise_device_info(info),
|
||||||
"event_count": len(events),
|
"event_count": len(events),
|
||||||
|
|||||||
Reference in New Issue
Block a user