feat(call-home): Implement Auto Call Home configuration management
- Added `CallHomeConfig` model to represent the Auto Call Home settings. - Introduced methods in `MiniMateClient` for reading (`get_call_home_config`) and writing (`set_call_home_config`) the call home configuration. - Updated `MiniMateProtocol` with new commands for call home operations (SUB 0x2C for read, SUB 0x7E for write, and SUB 0x7F for confirm). - Created API endpoints for retrieving and updating call home settings in the server. - Enhanced the web interface with a new "Call Home" tab for user interaction with call home settings. - Implemented JavaScript functions for reading and writing call home configurations from the web app.
This commit is contained in:
@@ -35,6 +35,7 @@ from typing import Optional
|
||||
|
||||
from .framing import S3Frame
|
||||
from .models import (
|
||||
CallHomeConfig,
|
||||
ComplianceConfig,
|
||||
DeviceInfo,
|
||||
Event,
|
||||
@@ -956,6 +957,93 @@ class MiniMateClient:
|
||||
notes=notes,
|
||||
)
|
||||
|
||||
# ── Call home config ──────────────────────────────────────────────────────
|
||||
|
||||
def get_call_home_config(self) -> CallHomeConfig:
|
||||
"""
|
||||
Read the auto call home (ACH) configuration from the device.
|
||||
|
||||
Sends SUB 0x2C (two-step read) and decodes the raw 125-byte payload
|
||||
into a CallHomeConfig object.
|
||||
|
||||
Returns:
|
||||
CallHomeConfig with all confirmed fields populated.
|
||||
|
||||
Raises:
|
||||
RuntimeError: if not connected.
|
||||
ProtocolError: on timeout or wrong response SUB.
|
||||
"""
|
||||
proto = self._require_proto()
|
||||
raw = proto.read_call_home_config()
|
||||
return _decode_call_home_config(raw)
|
||||
|
||||
def set_call_home_config(
|
||||
self,
|
||||
*,
|
||||
auto_call_home_enabled: Optional[bool] = None,
|
||||
after_event_recorded: Optional[bool] = None,
|
||||
at_specified_times: Optional[bool] = None,
|
||||
time1_enabled: Optional[bool] = None,
|
||||
time1_hour: Optional[int] = None,
|
||||
time1_min: Optional[int] = None,
|
||||
time2_enabled: Optional[bool] = None,
|
||||
time2_hour: Optional[int] = None,
|
||||
time2_min: Optional[int] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Read the current call home config, apply any supplied changes, and
|
||||
write the updated config back to the device.
|
||||
|
||||
Only non-None arguments are modified. All other bytes are round-tripped
|
||||
verbatim from the device.
|
||||
|
||||
Configurable fields
|
||||
-------------------
|
||||
auto_call_home_enabled : bool — master enable for ACH
|
||||
after_event_recorded : bool — call home after each triggered event
|
||||
at_specified_times : bool — call home at scheduled times
|
||||
time1_enabled : bool — enable time slot 1
|
||||
time1_hour : int — hour for time slot 1 (0-23)
|
||||
time1_min : int — minute for time slot 1 (0-59)
|
||||
time2_enabled : bool — enable time slot 2
|
||||
time2_hour : int — hour for time slot 2 (0-23)
|
||||
time2_min : int — minute for time slot 2 (0-59)
|
||||
|
||||
Write sequence (confirmed from 4-20-26 call home settings captures):
|
||||
SUB 0x2C (read, 2-step) → 125-byte raw payload
|
||||
patch fields in-place
|
||||
SUB 0x7E (write, 127-byte payload) → ack 0x81
|
||||
SUB 0x7F (confirm) → ack 0x80
|
||||
|
||||
Raises:
|
||||
RuntimeError: if not connected.
|
||||
ProtocolError: if any read or write step fails.
|
||||
"""
|
||||
proto = self._require_proto()
|
||||
|
||||
# 1. Read current config
|
||||
log.info("set_call_home_config: reading current config (SUB 0x2C)")
|
||||
raw = proto.read_call_home_config()
|
||||
|
||||
# 2. Patch fields and build write payload
|
||||
write_data = _encode_call_home_config(
|
||||
raw,
|
||||
auto_call_home_enabled=auto_call_home_enabled,
|
||||
after_event_recorded=after_event_recorded,
|
||||
at_specified_times=at_specified_times,
|
||||
time1_enabled=time1_enabled,
|
||||
time1_hour=time1_hour,
|
||||
time1_min=time1_min,
|
||||
time2_enabled=time2_enabled,
|
||||
time2_hour=time2_hour,
|
||||
time2_min=time2_min,
|
||||
)
|
||||
|
||||
# 3. Write back
|
||||
log.info("set_call_home_config: writing updated config (SUB 0x7E + 0x7F)")
|
||||
proto.write_call_home_config(write_data)
|
||||
log.info("set_call_home_config: complete")
|
||||
|
||||
def poll(self) -> None:
|
||||
"""
|
||||
Perform just the POLL startup handshake — no config reads.
|
||||
@@ -2232,3 +2320,160 @@ def _decode_monitor_status(data: bytes) -> MonitorStatus:
|
||||
memory_total=memory_total,
|
||||
memory_free=memory_free,
|
||||
)
|
||||
|
||||
|
||||
def _decode_call_home_config(raw: bytes) -> CallHomeConfig:
|
||||
"""
|
||||
Decode the raw 125-byte call home config payload into a CallHomeConfig.
|
||||
|
||||
*raw* is data[11:] from the SUB 0xD3 data response frame.
|
||||
|
||||
Field offsets (confirmed from 4-20-26 captures, all 11 BW+S3 pairs):
|
||||
[5] auto_call_home_enabled (0x00=off, 0x01=on)
|
||||
[6:46] dial_string 40-byte null-padded ASCII
|
||||
[87] after_event_recorded (0x01=on, 0x00=off)
|
||||
[91] at_specified_times (0x01=on, 0x00=off)
|
||||
[93] time1_enabled (0x01=on, 0x00=off)
|
||||
[95] time2_enabled (0x01=on, 0x00=off)
|
||||
[101] time1_hour uint8 decimal 0-23
|
||||
[102] time1_min uint8 decimal 0-59
|
||||
[105] time2_hour uint8 decimal 0-23
|
||||
[106] time2_min uint8 decimal 0-59
|
||||
[117:119] 10 03 = DLE-escaped num_retries=3 (logical value = 0x03)
|
||||
[120] time_between_retries_sec (shifted +1 from logical by DLE prefix)
|
||||
[122] wait_for_connection_sec
|
||||
[124] warm_up_time_sec
|
||||
|
||||
The DLE-escaped ETX at raw[117:119] = b'\\x10\\x03' means the logical value
|
||||
0x03 (3 retries) is stored there. The S3FrameParser keeps both bytes verbatim.
|
||||
Subsequent fields are at logical_offset + 1 in the raw byte array.
|
||||
"""
|
||||
cfg = CallHomeConfig(raw=raw)
|
||||
|
||||
if len(raw) < 10:
|
||||
return cfg
|
||||
|
||||
# Simple boolean and string fields — direct reads, no DLE complications
|
||||
if len(raw) > 5:
|
||||
cfg.auto_call_home_enabled = bool(raw[5])
|
||||
if len(raw) >= 46:
|
||||
ds = raw[6:46]
|
||||
cfg.dial_string = ds.split(b"\x00", 1)[0].decode("ascii", errors="replace") or None
|
||||
if len(raw) > 87:
|
||||
cfg.after_event_recorded = bool(raw[87])
|
||||
if len(raw) > 91:
|
||||
cfg.at_specified_times = bool(raw[91])
|
||||
if len(raw) > 93:
|
||||
cfg.time1_enabled = bool(raw[93])
|
||||
if len(raw) > 95:
|
||||
cfg.time2_enabled = bool(raw[95])
|
||||
if len(raw) > 102:
|
||||
cfg.time1_hour = raw[101]
|
||||
cfg.time1_min = raw[102]
|
||||
if len(raw) > 106:
|
||||
cfg.time2_hour = raw[105]
|
||||
cfg.time2_min = raw[106]
|
||||
|
||||
# num_retries: raw[117]=0x10 (DLE prefix), raw[118]=0x03 (value)
|
||||
# Subsequent fields shift by +1 from logical positions.
|
||||
if len(raw) > 118 and raw[117] == 0x10:
|
||||
cfg.num_retries = raw[118] # 0x03 = 3
|
||||
if len(raw) > 120:
|
||||
cfg.time_between_retries_sec = raw[120] # logical 119, shifted to 120
|
||||
if len(raw) > 122:
|
||||
cfg.wait_for_connection_sec = raw[122] # logical 121, shifted to 122
|
||||
if len(raw) > 124:
|
||||
cfg.warm_up_time_sec = raw[124] # logical 123, shifted to 124
|
||||
elif len(raw) > 117:
|
||||
# Fallback: no DLE prefix (num_retries is not 0x03)
|
||||
cfg.num_retries = raw[117]
|
||||
if len(raw) > 119:
|
||||
cfg.time_between_retries_sec = raw[119]
|
||||
if len(raw) > 121:
|
||||
cfg.wait_for_connection_sec = raw[121]
|
||||
if len(raw) > 123:
|
||||
cfg.warm_up_time_sec = raw[123]
|
||||
|
||||
log.debug(
|
||||
"_decode_call_home_config: enabled=%s dial=%r after_event=%s at_times=%s "
|
||||
"t1=%s %02d:%02d t2=%s %02d:%02d retries=%s gap=%s wait=%s warmup=%s",
|
||||
cfg.auto_call_home_enabled, cfg.dial_string,
|
||||
cfg.after_event_recorded, cfg.at_specified_times,
|
||||
cfg.time1_enabled, cfg.time1_hour or 0, cfg.time1_min or 0,
|
||||
cfg.time2_enabled, cfg.time2_hour or 0, cfg.time2_min or 0,
|
||||
cfg.num_retries, cfg.time_between_retries_sec,
|
||||
cfg.wait_for_connection_sec, cfg.warm_up_time_sec,
|
||||
)
|
||||
return cfg
|
||||
|
||||
|
||||
def _encode_call_home_config(
|
||||
raw: bytes,
|
||||
*,
|
||||
auto_call_home_enabled: Optional[bool] = None,
|
||||
after_event_recorded: Optional[bool] = None,
|
||||
at_specified_times: Optional[bool] = None,
|
||||
time1_enabled: Optional[bool] = None,
|
||||
time1_hour: Optional[int] = None,
|
||||
time1_min: Optional[int] = None,
|
||||
time2_enabled: Optional[bool] = None,
|
||||
time2_hour: Optional[int] = None,
|
||||
time2_min: Optional[int] = None,
|
||||
) -> bytes:
|
||||
"""
|
||||
Patch specific fields in the 125-byte raw call home payload and return
|
||||
the 127-byte write payload (raw + b'\\x00\\x00' footer).
|
||||
|
||||
Only non-None arguments are modified. All other bytes including the
|
||||
DLE-escaped \\x10\\x03 at [117:119] are preserved verbatim for round-trip.
|
||||
|
||||
The write payload footer (2 trailing zeros) matches Blastware's confirmed
|
||||
write frame format from the 4-20-26 captures.
|
||||
|
||||
CAUTION: hour and minute values must not equal 0x03 (3) — such values would
|
||||
require DLE-escaping that this encoder does not implement. Values 0x03 in
|
||||
hour/minute slots are rejected with ValueError.
|
||||
"""
|
||||
if len(raw) < 107:
|
||||
raise ValueError(
|
||||
f"call home raw payload too short: {len(raw)} bytes (need ≥107)"
|
||||
)
|
||||
buf = bytearray(raw) # 125 bytes
|
||||
|
||||
def _set_bool(offset: int, value: Optional[bool]) -> None:
|
||||
if value is not None:
|
||||
buf[offset] = 0x01 if value else 0x00
|
||||
|
||||
def _set_uint8(offset: int, value: Optional[int], name: str) -> None:
|
||||
if value is None:
|
||||
return
|
||||
if value == 0x03:
|
||||
raise ValueError(
|
||||
f"{name}={value} (0x03) requires DLE escaping — "
|
||||
"not supported by this encoder; avoid using 3 for hour/minute fields"
|
||||
)
|
||||
buf[offset] = value & 0xFF
|
||||
|
||||
_set_bool(5, auto_call_home_enabled)
|
||||
_set_bool(87, after_event_recorded)
|
||||
_set_bool(91, at_specified_times)
|
||||
_set_bool(93, time1_enabled)
|
||||
_set_bool(95, time2_enabled)
|
||||
_set_uint8(101, time1_hour, "time1_hour")
|
||||
_set_uint8(102, time1_min, "time1_min")
|
||||
_set_uint8(105, time2_hour, "time2_hour")
|
||||
_set_uint8(106, time2_min, "time2_min")
|
||||
# num_retries, time_between_retries_sec, wait_for_connection_sec, warm_up_time_sec
|
||||
# are not writable via this encoder — they're preserved verbatim including the
|
||||
# DLE-escaped 0x03 at [117:119].
|
||||
|
||||
log.debug(
|
||||
"_encode_call_home_config: patched fields: "
|
||||
"enabled=%s after_event=%s at_times=%s "
|
||||
"t1=%s %s:%s t2=%s %s:%s",
|
||||
auto_call_home_enabled, after_event_recorded, at_specified_times,
|
||||
time1_enabled, time1_hour, time1_min,
|
||||
time2_enabled, time2_hour, time2_min,
|
||||
)
|
||||
|
||||
return bytes(buf) + b"\x00\x00" # append 2-byte footer (confirmed BW pattern)
|
||||
|
||||
@@ -378,6 +378,78 @@ class ComplianceConfig:
|
||||
notes: Optional[str] = None # extended notes / additional info
|
||||
|
||||
|
||||
# ── Call Home Config ──────────────────────────────────────────────────────────
|
||||
|
||||
@dataclass
|
||||
class CallHomeConfig:
|
||||
"""
|
||||
Auto Call Home (ACH) configuration from SUB 0x2C (response 0xD3).
|
||||
|
||||
Read with a standard two-step protocol (probe offset=0x00, data offset=0x7C).
|
||||
Written via SUB 0x7E (write, 127-byte payload) + SUB 0x7F (confirm).
|
||||
|
||||
Confirmed from 4-20-26 call home settings captures (11 BW + S3 capture pairs).
|
||||
|
||||
Raw payload layout (data[11:] from S3 response, 125 bytes):
|
||||
[0] 0x00 header byte
|
||||
[1] 0x7C = 124 inner length (= offset for SUB 0x7E write - 2)
|
||||
[2] 0xDC constant
|
||||
[3:5] 0x00 0x00 padding
|
||||
[5] auto_call_home_enabled (0x00=off, 0x01=on) ✅
|
||||
[6:46] dial_string 40-byte null-padded ASCII ✅
|
||||
[46:87] auto_answer_raw AT command strings (not decoded) ✅ present
|
||||
[87] after_event_recorded (0x01=on, 0x00=off) ✅
|
||||
[91] at_specified_times (0x01=on, 0x00=off) ✅
|
||||
[93] time1_enabled (0x01=on, 0x00=off) ✅
|
||||
[95] time2_enabled (0x01=on, 0x00=off) ✅
|
||||
[101] time1_hour uint8 decimal 0-23 ✅
|
||||
[102] time1_min uint8 decimal 0-59 ✅
|
||||
[105] time2_hour uint8 decimal 0-23 ✅
|
||||
[106] time2_min uint8 decimal 0-59 ✅
|
||||
[117] DLE prefix (0x10) ┐ DLE-escaped num_retries=3 (0x03)
|
||||
[118] 0x03 ┘ device stores/returns 0x03 DLE-escaped ✅
|
||||
[120] time_between_retries_sec uint8 (= 0x0F = 15 s default) ✅
|
||||
[122] wait_for_connection_sec uint8 (= 0x3C = 60 s default) ✅
|
||||
[124] warm_up_time_sec uint8 (= 0x3C = 60 s default) ✅
|
||||
|
||||
Write payload = raw 125 bytes + b'\\x00\\x00' (2 trailing zeros) = 127 bytes.
|
||||
Offset for SUB 0x7E: data[1] + 2 = 0x7C + 2 = 0x7E = 126.
|
||||
|
||||
Note on DLE-escaped 0x03: The device's S3 response DLE-escapes ETX (0x03)
|
||||
bytes as \\x10\\x03. The S3FrameParser preserves both bytes in frame.data.
|
||||
Subsequent fields after offset 117 are therefore at raw_offset = logical+1.
|
||||
The raw payload must be round-tripped verbatim in write; do NOT reapply DLE
|
||||
destuffing or stripping.
|
||||
"""
|
||||
raw: Optional[bytes] = None # raw 125-byte read payload (for round-trip write)
|
||||
|
||||
# ── Main enable ──────────────────────────────────────────────────────────
|
||||
auto_call_home_enabled: Optional[bool] = None # raw[5] ✅
|
||||
|
||||
# ── Dial string ──────────────────────────────────────────────────────────
|
||||
dial_string: Optional[str] = None # raw[6:46] 40-byte null-padded ASCII ✅
|
||||
|
||||
# ── When to call ─────────────────────────────────────────────────────────
|
||||
after_event_recorded: Optional[bool] = None # raw[87] ✅
|
||||
at_specified_times: Optional[bool] = None # raw[91] ✅
|
||||
|
||||
# ── Time slot 1 ──────────────────────────────────────────────────────────
|
||||
time1_enabled: Optional[bool] = None # raw[93] ✅
|
||||
time1_hour: Optional[int] = None # raw[101] 0-23 ✅
|
||||
time1_min: Optional[int] = None # raw[102] 0-59 ✅
|
||||
|
||||
# ── Time slot 2 ──────────────────────────────────────────────────────────
|
||||
time2_enabled: Optional[bool] = None # raw[95] ✅
|
||||
time2_hour: Optional[int] = None # raw[105] 0-23 ✅
|
||||
time2_min: Optional[int] = None # raw[106] 0-59 ✅
|
||||
|
||||
# ── Retry / timeout settings (read-only; not writable via set_call_home_config) ──
|
||||
num_retries: Optional[int] = None # raw[117:119]=10 03 → value 3 ✅
|
||||
time_between_retries_sec: Optional[int] = None # raw[120] (shifted +1 by DLE) ✅
|
||||
wait_for_connection_sec: Optional[int] = None # raw[122] ✅
|
||||
warm_up_time_sec: Optional[int] = None # raw[124] ✅
|
||||
|
||||
|
||||
# ── Event ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -65,6 +65,7 @@ SUB_WAVEFORM_HEADER = 0x0A
|
||||
SUB_WAVEFORM_RECORD = 0x0C
|
||||
SUB_BULK_WAVEFORM = 0x5A
|
||||
SUB_COMPLIANCE = 0x1A
|
||||
SUB_CALL_HOME = 0x2C # Call home config read → response 0xD3 ✅
|
||||
SUB_UNKNOWN_2E = 0x2E
|
||||
|
||||
# Write command SUBs (= Read SUB + 0x60, confirmed from BW captures 3-11-26)
|
||||
@@ -78,6 +79,10 @@ SUB_WRITE_CONFIRM_C = 0x74 # Confirm C — sent after 69 ✅
|
||||
SUB_TRIGGER_CONFIG_WRITE = 0x82 # Write trigger config (0x22 + 0x60) ✅
|
||||
SUB_TRIGGER_CONFIRM = 0x83 # Confirm trigger write ✅
|
||||
|
||||
# Call home write SUBs (confirmed from 4-20-26 call home settings captures)
|
||||
SUB_CALL_HOME_WRITE = 0x7E # Write call home config → response 0x81 ✅
|
||||
SUB_CALL_HOME_CONFIRM = 0x7F # Confirm call home write → response 0x80 ✅
|
||||
|
||||
# Monitoring control SUBs (confirmed from 4-8-26/2ndtry BW TX capture)
|
||||
SUB_START_MONITORING = 0x96 # Start monitoring → response 0x69 ✅
|
||||
SUB_STOP_MONITORING = 0x97 # Stop monitoring → response 0x68 ✅
|
||||
@@ -109,6 +114,7 @@ DATA_LENGTHS: dict[int, int] = {
|
||||
# SUB_WAVEFORM_HEADER (0x0A) is VARIABLE — length read from probe response
|
||||
# data[4]. Do NOT add it here; use read_waveform_header() instead. ✅
|
||||
SUB_WAVEFORM_RECORD: 0xD2, # 210-byte waveform/histogram record ✅
|
||||
SUB_CALL_HOME: 0x7C, # 124-byte call home config ✅ (confirmed 4-20-26)
|
||||
SUB_UNKNOWN_2E: 0x1A, # 26 bytes, purpose TBD 🔶
|
||||
0x09: 0xCA, # 202 bytes, purpose TBD 🔶
|
||||
# SUB_COMPLIANCE (0x1A) uses a multi-step sequence with a 2090-byte total;
|
||||
@@ -1087,6 +1093,89 @@ class MiniMateProtocol:
|
||||
self._send(frame)
|
||||
return self.recv_write_ack(expected_sub=rsp_sub)
|
||||
|
||||
# ── Call home config (SUBs 0x2C / 0x7E / 0x7F) ──────────────────────────
|
||||
|
||||
def read_call_home_config(self) -> bytes:
|
||||
"""
|
||||
Read the auto call home configuration (SUB 0x2C → response 0xD3).
|
||||
|
||||
Standard two-step read: probe (offset=0x00) then data (offset=0x7C=124).
|
||||
Returns the raw 125-byte payload (data[11:] of the data response).
|
||||
|
||||
Confirmed from 4-20-26 call home settings capture:
|
||||
- Probe response: data[4]=0x7C (confirms data length = 124)
|
||||
- Data response: 136 bytes total (11-byte echo header + 125 bytes payload)
|
||||
- Payload[0:3] = 0x00 0x7C 0xDC (header: zero, inner-length, constant)
|
||||
- Payload[5] = auto_call_home_enabled
|
||||
- Payload[6:46] = dial_string (40-byte null-padded ASCII "RADIO RING")
|
||||
|
||||
Returns:
|
||||
Raw 125-byte call home config payload (data[11:]).
|
||||
Suitable for round-trip write (append \\x00\\x00 → 127-byte write payload).
|
||||
|
||||
Raises:
|
||||
ProtocolError: on timeout or wrong response SUB.
|
||||
"""
|
||||
rsp_sub = _expected_rsp_sub(SUB_CALL_HOME) # 0xFF - 0x2C = 0xD3
|
||||
length = DATA_LENGTHS[SUB_CALL_HOME] # 0x7C = 124
|
||||
|
||||
log.debug("read_call_home_config: 0x2C probe")
|
||||
self._send(build_bw_frame(SUB_CALL_HOME, 0))
|
||||
self._recv_one(expected_sub=rsp_sub)
|
||||
|
||||
log.debug("read_call_home_config: 0x2C data request offset=0x%02X", length)
|
||||
self._send(build_bw_frame(SUB_CALL_HOME, length))
|
||||
data_rsp = self._recv_one(expected_sub=rsp_sub)
|
||||
|
||||
payload = data_rsp.data[11:]
|
||||
log.debug("read_call_home_config: received %d payload bytes", len(payload))
|
||||
return payload
|
||||
|
||||
def write_call_home_config(self, data: bytes) -> None:
|
||||
"""
|
||||
Write the auto call home configuration (SUB 0x7E → 0x7F confirm).
|
||||
|
||||
Write sequence (confirmed from 4-20-26 call home settings captures):
|
||||
SUB 0x7E write 127-byte payload → device acks SUB 0x81
|
||||
SUB 0x7F confirm (no data) → device acks SUB 0x80
|
||||
|
||||
The 127-byte write payload = 125-byte read payload + b'\\x00\\x00'.
|
||||
The offset field = data[1] + 2 = 0x7C + 2 = 0x7E = 126.
|
||||
|
||||
Write frame format: build_bw_write_frame (minimal DLE stuffing — only
|
||||
BW_CMD is doubled; all other bytes are RAW). The \\x10\\x03 sequence
|
||||
within the payload is preserved as-is (device interprets DLE+ETX as the
|
||||
literal value 0x03 per the inner-frame terminator convention).
|
||||
|
||||
Args:
|
||||
data: 127-byte write payload (read payload + \\x00\\x00 footer).
|
||||
Must start with [0x00][0x7C][...] (standard header).
|
||||
|
||||
Raises:
|
||||
ValueError: if data is not exactly 127 bytes or lacks expected header.
|
||||
ProtocolError: on timeout or wrong response SUB.
|
||||
"""
|
||||
if len(data) < 2:
|
||||
raise ValueError(f"call home write payload must be at least 2 bytes, got {len(data)}")
|
||||
rsp_sub_write = _expected_rsp_sub(SUB_CALL_HOME_WRITE) # 0xFF - 0x7E = 0x81
|
||||
rsp_sub_confirm = _expected_rsp_sub(SUB_CALL_HOME_CONFIRM) # 0xFF - 0x7F = 0x80
|
||||
|
||||
# Offset formula: data[1] + 2 (same pattern as other single-chunk writes)
|
||||
offset = data[1] + 2 # 0x7C + 2 = 0x7E = 126
|
||||
frame = build_bw_write_frame(SUB_CALL_HOME_WRITE, data, offset=offset)
|
||||
log.debug(
|
||||
"write_call_home_config: %d bytes data[1]=0x%02X offset=0x%04X",
|
||||
len(data), data[1], offset,
|
||||
)
|
||||
self._send(frame)
|
||||
self.recv_write_ack(expected_sub=rsp_sub_write)
|
||||
log.debug("write_call_home_config: write acked; sending confirm 0x7F")
|
||||
|
||||
confirm_frame = build_bw_write_frame(SUB_CALL_HOME_CONFIRM, b"")
|
||||
self._send(confirm_frame)
|
||||
self.recv_write_ack(expected_sub=rsp_sub_confirm)
|
||||
log.debug("write_call_home_config: confirm acked — done")
|
||||
|
||||
# ── Monitoring ────────────────────────────────────────────────────────────
|
||||
|
||||
def read_monitor_status(self) -> S3Frame:
|
||||
|
||||
Reference in New Issue
Block a user