client: fix setup_name; add diagnostic scan for record_time/sample_rate
- setup_name was broken: _find_string_after(b"Standard Recording Setup") returned what comes AFTER the string (i.e. "Project:"), not the name itself. Fixed by searching for the first long (>=8 char) ASCII string in cfg[40:250] with _find_first_string(). - record_time offset 0x28 was wrong (that location holds "(L)", a unit label string). Disabled for now to avoid returning garbage; correct offset will be determined from _cfg_diagnostic() output. - Added _cfg_diagnostic(): logs all strings and all plausible float32/uint16 values across the full cfg so record_time and sample_rate offsets can be pinpointed from a single device run. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -580,40 +580,52 @@ def _decode_compliance_config_into(data: bytes, info: DeviceInfo) -> None:
|
||||
"""
|
||||
Decode a 2090-byte SUB 1A (COMPLIANCE_CONFIG) response into a ComplianceConfig.
|
||||
|
||||
The *data* argument is the raw response bytes from read_compliance_config().
|
||||
The *data* argument is the raw bytes returned by read_compliance_config()
|
||||
(frames B+C+D concatenated, echo headers stripped).
|
||||
|
||||
Extracts (per §7.6):
|
||||
- record_time: float32 BE at offset +0x28
|
||||
- trigger_level / alarm_level per-channel: IEEE 754 BE floats
|
||||
- project strings: "Project:", "Client:", "User Name:", "Seis Loc:", "Extended Notes"
|
||||
- sample_rate: NOT YET FOUND ❓
|
||||
Confirmed field locations (BE11529 with 3-step read, 2126 bytes):
|
||||
- cfg[89] = setup_name: first long ASCII string in cfg[40:250]
|
||||
- cfg[??] = record_time: float32 BE — offset TBD (scan below)
|
||||
- cfg[??] = sample_rate: uint16 or float — offset TBD (scan below)
|
||||
- "Project:" needle → project string
|
||||
- "Client:" needle → client string
|
||||
- "User Name:" needle → operator string
|
||||
- "Seis Loc:" needle → sensor_location string
|
||||
- "Extended Notes" needle → notes string
|
||||
|
||||
Modifies info.compliance_config in-place.
|
||||
"""
|
||||
if not data or len(data) < 0x28:
|
||||
if not data or len(data) < 40:
|
||||
log.warning("compliance config payload too short (%d bytes)", len(data))
|
||||
return
|
||||
|
||||
config = ComplianceConfig(raw=data)
|
||||
|
||||
# ── Record Time (✅ CONFIRMED at §7.6.1) ──────────────────────────────────
|
||||
# ── Diagnostic: scan full cfg for strings and plausible floats ─────────────
|
||||
# Logged at WARNING so they appear without debug mode. Remove once field
|
||||
# offsets are confirmed for both BE11529 and BE18189.
|
||||
_cfg_diagnostic(data)
|
||||
|
||||
# ── Setup name ────────────────────────────────────────────────────────────
|
||||
# The setup_name IS the string itself — it is NOT a label followed by a value.
|
||||
# It appears as the first long (>=8 char) ASCII string in cfg[40:250].
|
||||
# The preceding bytes vary by device (cfg[88]=0x01 on BE11529); the string
|
||||
# itself is null-terminated.
|
||||
try:
|
||||
# Record time is at offset +0x28 within the data payload (NOT from frame start)
|
||||
# This is the second page of the paged read response.
|
||||
if len(data) > 0x28 + 4:
|
||||
record_time = struct.unpack_from(">f", data, 0x28)[0]
|
||||
config.record_time = record_time
|
||||
log.debug("compliance_config: record_time = %.1f sec", record_time)
|
||||
except struct.error:
|
||||
log.warning("compliance_config: failed to unpack record_time at offset 0x28")
|
||||
setup_name = _find_first_string(data, start=40, end=250, min_len=8)
|
||||
config.setup_name = setup_name
|
||||
if setup_name:
|
||||
log.debug("compliance_config: setup_name = %r", setup_name)
|
||||
except Exception as exc:
|
||||
log.warning("compliance_config: setup_name extraction failed: %s", exc)
|
||||
|
||||
# ── Per-channel trigger/alarm levels (✅ CONFIRMED at §7.6) ─────────────────
|
||||
# Layout (per §7.6): [padding2][max_range][padding][trigger]["in.\0"][alarm]["/s\0\0"][flag][label]
|
||||
# Exact byte offsets relative to label require detailed field mapping from actual captures.
|
||||
# For now, we skip extraction — this section will be populated once we have precise offsets.
|
||||
# TODO: Capture E5 response and map exact trigger/alarm float positions
|
||||
# ── Record Time (⚠️ OFFSET UNCONFIRMED — waiting on diagnostic output) ──
|
||||
# Previous guess of 0x28 was wrong (that offset holds "(L)" string).
|
||||
# The correct offset will be clear once _cfg_diagnostic identifies it.
|
||||
# Temporarily disabled to avoid returning a garbage value.
|
||||
config.record_time = None # TODO: set once offset confirmed from diagnostic
|
||||
|
||||
# ── Project strings (from E5 / SUB 71 payload) ────────────────────────────
|
||||
# ── Project strings ───────────────────────────────────────────────────────
|
||||
try:
|
||||
def _find_string_after(needle: bytes, max_len: int = 64) -> Optional[str]:
|
||||
pos = data.find(needle)
|
||||
@@ -630,7 +642,6 @@ def _decode_compliance_config_into(data: bytes, info: DeviceInfo) -> None:
|
||||
s = data[value_start:end].decode("ascii", errors="replace").strip()
|
||||
return s or None
|
||||
|
||||
config.setup_name = _find_string_after(b"Standard Recording Setup")
|
||||
config.project = _find_string_after(b"Project:")
|
||||
config.client = _find_string_after(b"Client:")
|
||||
config.operator = _find_string_after(b"User Name:")
|
||||
@@ -644,10 +655,82 @@ def _decode_compliance_config_into(data: bytes, info: DeviceInfo) -> None:
|
||||
except Exception as exc:
|
||||
log.warning("compliance_config: project string extraction failed: %s", exc)
|
||||
|
||||
# ── Sample rate (NOT YET FOUND ❓) ────────────────────────────────────────
|
||||
# The sample rate (1024 sps standard, 2048 sps fast) is not yet located in the
|
||||
# protocol docs. It may be encoded in mystery flags at offset +0x12 in the .set
|
||||
# file format, or it may require a separate capture analysis. For now, leave as None.
|
||||
config.sample_rate = None
|
||||
# ── Sample rate (⚠️ OFFSET UNCONFIRMED — waiting on diagnostic output) ────
|
||||
config.sample_rate = None # TODO: set once offset confirmed from diagnostic
|
||||
|
||||
info.compliance_config = config
|
||||
|
||||
|
||||
def _find_first_string(data: bytes, start: int, end: int, min_len: int) -> Optional[str]:
|
||||
"""
|
||||
Return the first null-terminated printable ASCII string of length >= min_len
|
||||
found in data[start:end].
|
||||
"""
|
||||
i = start
|
||||
end = min(end, len(data))
|
||||
while i < end:
|
||||
if 0x20 <= data[i] < 0x7F:
|
||||
j = i
|
||||
while j < len(data) and 0x20 <= data[j] < 0x7F:
|
||||
j += 1
|
||||
if j - i >= min_len:
|
||||
return data[i:j].decode("ascii", errors="replace").strip()
|
||||
i = j + 1
|
||||
else:
|
||||
i += 1
|
||||
return None
|
||||
|
||||
|
||||
def _cfg_diagnostic(data: bytes) -> None:
|
||||
"""
|
||||
Log all strings and plausible float32 BE values found in the full compliance
|
||||
config data. Used to map field offsets for record_time and sample_rate.
|
||||
Remove once offsets are confirmed.
|
||||
"""
|
||||
import struct as _struct
|
||||
|
||||
# ── All printable ASCII strings >= 4 chars ────────────────────────────────
|
||||
strings_found = []
|
||||
i = 0
|
||||
while i < len(data):
|
||||
if 0x20 <= data[i] < 0x7F:
|
||||
j = i
|
||||
while j < len(data) and 0x20 <= data[j] < 0x7F:
|
||||
j += 1
|
||||
if j - i >= 4:
|
||||
s = data[i:j].decode("ascii", errors="replace")
|
||||
strings_found.append((i, s))
|
||||
i = j
|
||||
else:
|
||||
i += 1
|
||||
|
||||
log.warning("cfg_diag: %d strings found:", len(strings_found))
|
||||
for off, s in strings_found[:40]: # cap at 40 to avoid log spam
|
||||
log.warning(" cfg[%04d/0x%04X] str %r", off, off, s[:60])
|
||||
|
||||
# ── Float32 BE values in plausible physical ranges ─────────────────────────
|
||||
log.warning("cfg_diag: float32_BE scan (plausible record_time / sample_rate / trigger):")
|
||||
for i in range(len(data) - 3):
|
||||
try:
|
||||
v = _struct.unpack_from(">f", data, i)[0]
|
||||
except Exception:
|
||||
continue
|
||||
if v != v or v == float("inf") or v == float("-inf"):
|
||||
continue
|
||||
tag = ""
|
||||
if 0.5 <= v <= 30.0: tag = "RECORD_TIME?"
|
||||
elif 200 <= v <= 8192: tag = "SAMPLE_RATE?"
|
||||
elif 0.003 <= v <= 5.0: tag = "TRIGGER/ALARM?"
|
||||
if tag:
|
||||
log.warning(" cfg[%04d/0x%04X] f32_BE=%10.4f %s", i, i, v, tag)
|
||||
|
||||
# ── uint16 BE/LE scan for known sample rates ──────────────────────────────
|
||||
known_rates = {256, 512, 1024, 2048, 4096}
|
||||
log.warning("cfg_diag: uint16 scan for sample rates %s:", known_rates)
|
||||
for i in range(len(data) - 1):
|
||||
u16_le = _struct.unpack_from("<H", data, i)[0]
|
||||
u16_be = _struct.unpack_from(">H", data, i)[0]
|
||||
if u16_le in known_rates:
|
||||
log.warning(" cfg[%04d/0x%04X] uint16_LE=%d", i, i, u16_le)
|
||||
if u16_be in known_rates and u16_be != u16_le:
|
||||
log.warning(" cfg[%04d/0x%04X] uint16_BE=%d", i, i, u16_be)
|
||||
|
||||
Reference in New Issue
Block a user