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:
2026-04-20 18:23:48 -04:00
parent 7bdd7c92f2
commit 3fb24e1895
8 changed files with 1081 additions and 8 deletions
+118
View File
@@ -106,6 +106,7 @@
| 2026-04-17 | §7.6.2, §14 | **RESOLVED — Float 6.206053 at channel_label+28 is the ADC-to-velocity scale factor.** Confirmed from Series III Interface Handbook §4.5 formula: `Range (×1) = 1.61133 V / Sensitivity (V/unit)`. For the standard Instantel geophone at Normal range (10.000 in/s): Sensitivity = 1.61133 / 10 = 0.161133 V/(in/s). The stored value is the **inverse sensitivity** = 1/0.161133 = **6.206053 (in/s)/V**. Cross-check: 1.61133 V × 6.206053 = 10.000 in/s ✅. The firmware uses it as: `PPV (in/s) = ADC_voltage (V) × 6.206053`. Value is identical on all Instantel standard geophones — it is a hardware/firmware constant, NOT a user-configurable setting. Do NOT write this field. Open question §14 item "Max Geo Range float 6.2061" is now **RESOLVED**. |
| 2026-04-20 | §7.6.4 (NEW), §7.9, Appendix B | **CONFIRMED — Recording Mode byte location.** Three targeted captures (4-20-26) confirmed `recording_mode` at **`cfg[5]`** in the SUB 71 write payload (3-chunk compliance write). Method: single Blastware session, one initial E5 config pull, then three sequential "Send to unit" writes changing Recording Mode only. Diff of SUB 71 chunk-1 payloads: only `cfg[5]` and `cfg[1024]` changed; `cfg[1024]` delta exactly equals `cfg[5]` delta (chunk running checksum). In the E5 read response (sub-frame 1, page=0x0010), the field is at **`data[17]`** (= **anchor 4** from the 10-byte anchor), one position earlier than in the write payload due to an extra `0x10` byte at `data[18]` present only in the read format. Enum: `0x00`=Single Shot, `0x01`=Continuous, `0x03`=Histogram, `0x04`=Histogram+Continuous. `0x02` value not yet observed. See §7.6.4 for full details. |
| 2026-04-20 | §7.6.2, §7.9, Appendix B | **CONFIRMED — Geophone maximum range / sensitivity selector byte location.** Two targeted captures (4-20-26, geo sensitivity folder): one at Normal 10.000 in/s, one at Sensitive 1.250 in/s. E5 read payload diff: exactly 3 bytes differ at channel_label+33 for Tran/Vert/Long. Values: `0x00`=Normal 10.000 in/s, `0x01`=Sensitive 1.250 in/s. Same offset applies to the SUB 71 write payload (which is the same 2126-byte E5-format buffer round-tripped verbatim). **`channel_label+20` reads `0x01` in ALL captures regardless of range setting — it is NOT this field.** Previous hypothesis (uint8 at Tran+20, 0x01=Normal) was WRONG. Stored as `geo_range` in `ComplianceConfig`. Encoded to all three geo channel blocks (Tran/Vert/Long) at label+33. |
| 2026-04-20 | §5.1, §5.3, §7.12 (NEW) | **NEW — Auto Call Home config protocol confirmed from 4-20-26 call home settings captures.** SUB 0x2C (Call Home Config READ, response 0xD3, data offset 0x7C=124) and SUB 0x7E/0x7F (WRITE + CONFIRM, response 0x81/0x80) confirmed. Write payload = read payload (125 bytes) + `\x00\x00` (127 bytes total). **DLE-escaped ETX at raw[117:119]:** the device returns logical value 0x03 (num_retries=3) as `\x10\x03` on the wire — S3FrameParser preserves both bytes as two literals, causing a +1 byte shift for all subsequent fields. Write frame sends these bytes verbatim (device interprets `\x10\x03` as literal value 3). Field map confirmed from 10-frame BW TX diff. See §7.12 for full layout. |
---
@@ -267,6 +268,7 @@ Step 4 — Device sends actual data payload:
| `1C` | **MONITOR STATUS READ** | Two-step read, data offset 0x2C (44 bytes). `section[1] == 0x10` → monitoring; `0x00` → idle (CONFIRMED 2026-04-09, 100% accuracy on 144 frames). Payload length: 4647 bytes IDLE, 4849 bytes MONITORING. `frame.data` has checksum stripped — no trailing byte to skip. Battery/memory at end: `section[-10:-8]` = battery×100 (uint16 BE), `section[-8:-4]` = memory_total (uint32 BE), `section[-4:]` = memory_free (uint32 BE). | ✅ CONFIRMED 2026-04-09 |
| `96` | **START MONITORING** | Single write frame, no data payload. Transitions unit from idle to monitoring mode (after optional on-device sensor check ~40 s). | ✅ CONFIRMED 2026-04-08 |
| `97` | **STOP MONITORING** | Single write frame, no data payload. Stops monitoring, unit returns to idle. | ✅ CONFIRMED 2026-04-08 |
| `2C` | **CALL HOME CONFIG READ** | Two-step read, data offset 0x7C (124 bytes + 1-byte DLE artefact = 125 raw bytes). Returns Auto Call Home configuration: enable flag, dial string, scheduled call times, retry settings, modem timing. Response SUB = 0xD3. **DLE note:** logical value 0x03 (num_retries) is returned as `\x10\x03` on the wire, which S3FrameParser preserves as two literal bytes — this shifts all subsequent field positions by +1. See §7.12 for full field map. | ✅ CONFIRMED 2026-04-20 |
| `A3` | **ERASE ALL BEGIN** | Single frame, token=0xFE at params[7]. Initiates device memory erase. Must be followed by 0x1C probe+data + 0x06 probe+data + 0xA2 to complete. Standard `build_bw_frame` (not write-format). Response ack SUB = 0x5C. | ✅ CONFIRMED 2026-04-11 |
| `A2` | **ERASE ALL CONFIRM** | Single frame, token=0xFE at params[7]. Commits the erase initiated by 0xA3. After this ack (SUB 0x5D), device memory is cleared and the event counter resets to `0x01110000`. | ✅ CONFIRMED 2026-04-11 |
@@ -296,6 +298,7 @@ All requests use CMD byte `0x02`. All responses use CMD byte `0x10 0x02` (which,
| `98` | `67` | ✅ CONFIRMED 2026-04-08 |
| `96` | `69` | ✅ CONFIRMED 2026-04-08 |
| `97` | `68` | ✅ CONFIRMED 2026-04-08 |
| `2C` | `D3` | ✅ CONFIRMED 2026-04-20 |
| `A3` | `5C` | ✅ CONFIRMED 2026-04-11 |
| `A2` | `5D` | ✅ CONFIRMED 2026-04-11 |
@@ -317,6 +320,8 @@ Write commands are initiated by Blastware (`BW->S3`) and use SUB bytes in the `0
| `72` | **WRITE CONFIRM A** | Short frame, no data. Likely commit/confirm step after `71`. | `8D` | ✅ CONFIRMED |
| `73` | **WRITE CONFIRM B** | Short frame, no data. | `8C` | ✅ CONFIRMED |
| `74` | **WRITE CONFIRM C** | Short frame, no data. | `8B` | ✅ CONFIRMED |
| `7E` | **CALL HOME CONFIG WRITE** | Writes Auto Call Home configuration (127 bytes: 125-byte read payload + `\x00\x00`). Offset = data[1]+2 = 0x7E. Write format (DLE-aware checksum, only BW_CMD `0x10` doubled on wire). Response SUB = 0x81. Must be followed by SUB 0x7F confirm. | `81` | ✅ CONFIRMED 2026-04-20 |
| `7F` | **CALL HOME WRITE CONFIRM** | Short frame, no data. Commits call home config write from SUB 0x7E. Response SUB = 0x80. | `80` | ✅ CONFIRMED 2026-04-20 |
| `82` | **TRIGGER CONFIG WRITE** | Writes trigger config block (0x1C bytes, mirrors SUB `1C` read). | `7D` | ✅ CONFIRMED |
| `83` | **TRIGGER WRITE CONFIRM** | Short frame, no data. Likely commit step after `82`. | `7C` | ✅ CONFIRMED |
@@ -330,6 +335,8 @@ Write commands are initiated by Blastware (`BW->S3`) and use SUB bytes in the `0
| `72` | `8D` |
| `73` | `8C` |
| `74` | `8B` |
| `7E` | `81` |
| `7F` | `80` |
| `82` | `7D` |
| `83` | `7C` |
@@ -1522,6 +1529,117 @@ and applies this heuristic on every call-home.
---
### 7.12 Auto Call Home Config (SUB 0x2C / 0x7E / 0x7F) — ✅ CONFIRMED 2026-04-20
> Confirmed from `bridges/captures/4-20-26/call home settings/` — 10 BW TX write frames
> diffed against the S3 read payload. Accessible in Blastware via Remote Access → Setup Unit.
#### 7.12.1 Read Protocol — SUB 0x2C → Response 0xD3
Standard two-step read:
| Step | Offset | Purpose |
|---|---|---|
| Probe | `0x0000` | Get ack (no data returned) |
| Data | `0x007C` (124) | Receive 125-byte raw payload |
`DATA_LENGTHS[SUB_CALL_HOME] = 0x7C`
The raw payload is accessed as `data_rsp.data[11:]` — this is 125 bytes (not 124) because
the device returns logical value 0x03 (num_retries=3) as the two-byte wire sequence
`\x10\x03`. S3FrameParser is in `STATE_IN_FRAME` when it sees `0x10`, transitions to
`STATE_AFTER_DLE`, and then on `0x03` (ETX qualifier) it would normally end the frame —
but in the `_IN_FRAME_DLE` state it instead appends **both** the `0x10` and the `0x03`
literally to the payload. The result: `raw[117] = 0x10`, `raw[118] = 0x03`, and all
subsequent fields are shifted +1 from their logical positions.
#### 7.12.2 Raw Payload Field Map (125 bytes, from `data_rsp.data[11:]`)
> All offsets are into the 125-byte raw array. Offsets ≥ 119 are shifted +1 from logical
> due to the DLE-escaped 0x03 at raw[117:119].
| Raw Offset | Field | Type | Notes |
|---|---|---|---|
| `[5]` | `auto_call_home_enabled` | uint8 | `0x00` = disabled, `0x01` = enabled |
| `[6:46]` | `dial_string` | ASCII | 40-byte null-padded, e.g. `"12345"` or phone number |
| `[87]` | `after_event_recorded` | uint8 | `0x00` = off, `0x01` = on |
| `[91]` | `at_specified_times` | uint8 | `0x00` = off, `0x01` = on |
| `[93]` | `time1_enabled` | uint8 | `0x00` = off, `0x01` = on |
| `[101]` | `time1_hour` | uint8 | 023 |
| `[102]` | `time1_min` | uint8 | 059 |
| `[95]` | `time2_enabled` | uint8 | `0x00` = off, `0x01` = on |
| `[105]` | `time2_hour` | uint8 | 023 |
| `[106]` | `time2_min` | uint8 | 059 |
| `[117]` | DLE prefix `0x10` | — | Part of `\x10\x03` wire encoding for num_retries value 3 |
| `[118]` | `num_retries` (value = 3) | uint8 | Logical value 0x03; check `raw[117] == 0x10` to detect DLE prefix |
| `[120]` | `time_between_retries_sec` | uint8 | Shift +1 from logical 119 |
| `[122]` | `wait_for_connection_sec` | uint8 | Shift +1 from logical 121 |
| `[124]` | `warm_up_time_sec` | uint8 | Shift +1 from logical 123 |
**Unconfirmed fields** (offsets not yet mapped from captures):
- Time slots 3 and 4 (if they exist — Blastware UI only shows 2 time slots in observed sessions)
- `modem_power_relay_enabled` (bool)
- `storage_mode` (call home trigger on all events vs. triggered only?)
#### 7.12.3 DLE-Escaped 0x03 — Critical Detail
The `\x10\x03` sequence at raw[117:119] is **not** a DLE stuffing artifact in the usual
sense. Standard DLE stuffing escapes `\x10``\x10\x10`. But here the device is encoding
the integer value `3` in a position where the byte `\x03` would be indistinguishable from
the frame ETX terminator. The device therefore sends `\x10\x03` (DLE + ETX = "inner-frame
terminator" in S3 inner-frame syntax). S3FrameParser correctly handles this: in
`STATE_AFTER_DLE`, seeing `\x03` (ETX) while **inside** an outer frame causes it to
append both `\x10` and `\x03` as literal bytes rather than ending the frame. The outer
frame only terminates on a **bare** `\x03` (without the DLE prefix).
The write frame sends these bytes verbatim — the device accepts `\x10\x03` in the write
payload and interprets it as the value 3. No transformation is needed in
`_encode_call_home_config()`.
**Limitation:** Any field that needs to encode the value `3` (0x03) requires this DLE
prefix. The current encoder raises `ValueError` if any hour or minute field equals 3,
since the encoder does not yet implement DLE-prefixed writes for arbitrary field positions.
In practice, 3:00 AM / 3 minutes past are unlikely scheduled call times.
#### 7.12.4 Write Protocol — SUB 0x7E → 0x7F
Write format (same as other write commands — only BW_CMD `0x10` doubled on wire;
all other bytes written raw; DLE-aware checksum):
| Step | SUB | Payload | Offset | Response |
|---|---|---|---|---|
| Data write | `0x7E` | 127 bytes (125-byte read payload + `\x00\x00`) | `data[1]+2 = 0x7E` (126) | `0x81` |
| Confirm | `0x7F` | empty | `0x00` | `0x80` |
**Write payload construction:**
```python
write_payload = bytearray(raw_125_bytes)
write_payload.append(0x00)
write_payload.append(0x00)
# patch fields in-place, then pass bytes(write_payload) to build_bw_write_frame
```
**Offset formula:** `write_payload[1] = 0x7C` (same as DATA_LENGTH).
`offset = write_payload[1] + 2 = 0x7C + 2 = 0x7E = 126`.
This follows the identical pattern as SUB 0x68 (event index write) and SUB 0x69 (waveform write).
**No preceding 0x2C read required** — Blastware sends SUB 0x7E directly using cached
state. The `seismo-relay` implementation always reads first (`get_call_home_config()`)
before writing for safety.
#### 7.12.5 Implementation Notes
- `MiniMateProtocol.read_call_home_config()` — standard two-step read; returns `data_rsp.data[11:]` (125 bytes raw)
- `MiniMateProtocol.write_call_home_config(data)` — sends SUB 0x7E (127-byte payload) then SUB 0x7F confirm
- `MiniMateClient.get_call_home_config()``CallHomeConfig` dataclass
- `MiniMateClient.set_call_home_config(...)` — reads current config, patches via `_encode_call_home_config()`, writes back
- `_decode_call_home_config(raw)` — handles DLE prefix detection at raw[117]
- `_encode_call_home_config(raw, ...)` — patches in-place, appends 2 trailing zeros; raises `ValueError` if any hour/min == 3
- REST API: `GET /device/call_home` and `POST /device/call_home` in `sfm/server.py`
- Web UI: "Call Home" tab in `sfm/sfm_webapp.html`
---
## 8. Timestamp Format
Two timestamp wire formats are used: