feat: decode waveform record timestamp, record type, and Peak Vector Sum

Confirmed 2026-04-01 against Blastware event report for BE11529 thump
event ("00:28:12 April 1, 2026", PVS 3.906 in/s).

models.py:
- Timestamp.from_waveform_record(): decode 9-byte format from 0C record
  bytes[0-8]: [day][sub_code][month][year:2BE][?][hour][min][sec]
- Timestamp: add hour/minute/second optional fields; __str__ includes
  time when available
- PeakValues: add peak_vector_sum field (confirmed fixed offset 87)

client.py:
- _decode_waveform_record_into: add timestamp decode from bytes[0:9]
- _extract_record_type: decode byte[1] (sub_code), not ASCII string
  search; 0x10 → "Waveform", histogram TBD
- _extract_peak_floats: add PVS from offset 87 (IEEE 754 BE float32)
  = √(T²+V²+L²) at max instantaneous vector moment

sfm/server.py:
- _serialise_timestamp: add hour/minute/second/day fields to JSON
- _serialise_peak_values: add peak_vector_sum to JSON

docs: update §7.7.5 and §8 with confirmed 9-byte timestamp layout,
PVS field, and byte[1] record type encoding; update command table;
close resolved open questions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Brian Harrison
2026-04-01 00:53:34 -04:00
parent f74992f4e5
commit 4944974f6e
4 changed files with 257 additions and 73 deletions

View File

@@ -63,6 +63,10 @@
| 2026-03-31 | §14.2 | **CORRECTED — Sierra Wireless RV50/RV55 sends `RING`/`CONNECT` over TCP to caller even with Quiet Mode enabled.** Quiet Mode suppresses these only on the serial port (protecting the MiniMate). TCP client still receives `\r\nRING\r\n\r\nCONNECT\r\n` prefixed before the first S3 frame bytes. Parser handles correctly by scanning for DLE+STX (`0x10 0x02`) and discarding prefix bytes. Previous note "no CONNECT string" described Raven X ENQ-disable behaviour; RV50/RV55 differ. |
| 2026-03-31 | §7.3 | **NEW — Calibration date field confirmed** at Full Config (SUB FE) destuffed payload offsets 0x530x57. Two-unit comparison: BE18189 (calibrated 2023) has `07 E7` at 0x560x57; BE11529 (calibrated 2025) has `07 E9`. Bytes 0x560x57 = uint16 BE calibration year ✅ CONFIRMED. Adjacent bytes at 0x530x55 likely encode month/day (both units show `0x10` at offset 0x54 = BCD October; 0x53 and 0x55 differ between units). Full date layout 🔶 INFERRED — pending third-unit capture or recalibration diff. Resolves open question. |
| 2026-03-31 | §9 | **CONFIRMED via Console cold-start capture**`"Operating System"` (16 B: `4f 70 65 72 61 74 69 6e 67 20 53 79 73 74 65 6d`) arrives as first TCP bytes on cold-connect before unit enters DLE-framed mode. `TcpTransport` + retry logic handles gracefully: first attempt times out waiting for SUB A4; second connect (after unit fully booted) succeeds. |
| 2026-04-01 | §7.7.5, §8 | **CONFIRMED — Full waveform record (0C) timestamp layout** cross-referenced against Blastware event report for BE11529 thump event ("00:28:12 April 1, 2026"). 9-byte format at bytes[08]: `[day][sub_code][month][year:2 BE][unknown][hour][min][sec]`. All fields verified. Sub_code `0x10` = Waveform (continuous/single-shot). **Previous 7-byte format doc was wrong** — replaced with confirmed 9-byte layout. |
| 2026-04-01 | §7.7.5 | **CONFIRMED — Record type** encoded in byte[1] (sub_code), not as ASCII string. `0x10` = Waveform ✅. Histogram sub_code not yet captured. ASCII string search approach removed. |
| 2026-04-01 | §7.7.5, §14 | **CONFIRMED — Per-channel PPV** at label+6 (✅ all four channels), cross-referenced vs Blastware: Tran=0.420, Vert=3.870, Long=0.495 in/s. **CONFIRMED — Peak Vector Sum** at fixed offset 87 = 3.906 in/s ✅ matches Blastware "Peak Vector Sum". Is √(Tran²+Vert²+Long²) at max instantaneous vector moment, not vector sum of per-channel peaks. Open question "offset 87 purpose" closed. |
| 2026-04-01 | §8 | **RESOLVED — §8 unknown byte at offset 3.** Field is confirmed absent in the 9-byte waveform record format (no such field). The 6-byte event-index format has a separator byte at [3] whose purpose remains ❓ but is no longer actively blocking anything. |
---
@@ -211,7 +215,7 @@ Step 4 — Device sends actual data payload:
| `1C` | **TRIGGER CONFIG READ** | Requests trigger settings block (0x2C bytes). | ✅ CONFIRMED |
| `1E` | **EVENT HEADER READ** | Gets the first waveform key (4-byte opaque record address). All-zero params; key returned at data[11:15]. | ✅ CONFIRMED 2026-03-31 |
| `0A` | **WAVEFORM HEADER READ** | Checks record type for a given waveform key. Variable DATA_LENGTH: 0x30=full bin, 0x26=partial bin. Key at params[4..7]. | ✅ CONFIRMED 2026-03-31 |
| `0C` | **FULL WAVEFORM RECORD** | Downloads 210-byte waveform/histogram record. Contains record type, PPV floats (at channel label+6), project strings, 7-byte timestamp. Key at params[4..7], DATA_LENGTH=0xD2. | ✅ CONFIRMED 2026-03-31 |
| `0C` | **FULL WAVEFORM RECORD** | Downloads 210-byte waveform/histogram record. 9-byte timestamp at bytes[08]; record sub_code at byte[1] (0x10=Waveform); PPV floats at channel label+6; Peak Vector Sum float at offset 87; project strings. Key at params[4..7], DATA_LENGTH=0xD2. | ✅ CONFIRMED 2026-04-01 |
| `1F` | **EVENT ADVANCE** | Advances to next waveform key. Token byte at params[6]: 0x00=browse (one step), 0xFE=download (skip partial bins). Returns next key at data[11:15]; zeros = no more events. | ✅ CONFIRMED 2026-03-31 |
| `5A` | **BULK WAVEFORM STREAM** | Initiates bulk download of raw ADC sample data, keyed by waveform key. Large multi-page transfer. | ✅ CONFIRMED |
| `24` | **WAVEFORM PAGE A?** | Paged waveform read, possibly channel group A. | 🔶 INFERRED |
@@ -775,26 +779,46 @@ Actual data lengths:
#### 7.7.5 Waveform Record Layout (210 bytes, SUB F3 → response F3)
> ✅ **Updated 2026-04-01** — Full timestamp layout confirmed against Blastware
> event report (BE11529 thump event, "00:28:12 April 1, 2026"). Record type
> encoding corrected (byte[1], not ASCII string search). Peak Vector Sum field
> confirmed at fixed offset 87.
The 210-byte record (`data_rsp.data[11:11+0xD2]`) contains:
**Record type string** (search at variable offset):
- `"Histogram"` — histogram mode recording
- `"Waveform"` — single-shot waveform recording
**Timestamp** (7-byte format, confirmed from 3-31-26 capture):
**Header / Timestamp** (9 bytes at offsets 08, ✅ CONFIRMED 2026-04-01):
```
byte 0: 0x09 (magic/type marker)
bytes 12: year (uint16 big-endian)
byte 3: 0x00
byte 4: hour
byte 5: minute
byte 6: second
byte[0]: day (uint8)
byte[1]: sub_code 0x10 = Waveform (continuous/single-shot) ✅
histogram code not yet captured ❓
byte[2]: month (uint8)
bytes[34]: year (uint16 big-endian)
byte[5]: unknown (0x00 in all observed samples ❓)
byte[6]: hour (uint8)
byte[7]: minute (uint8)
byte[8]: second (uint8)
```
> ❓ Month and day are not present in the waveform record timestamp.
> Month/day may appear in the event index (SUB F7) or a separate header
> field not yet confirmed.
**Peak particle velocity floats** (✅ CONFIRMED 2026-03-31):
Thump event raw bytes (2026-04-01 00:28:12):
```
01 10 04 07 ea 00 00 1c 0c
↑ ↑ ↑ ↑──↑ ↑ ↑ ↑ ↑
d=1 sub m=4 y=2026 ? h=0 m=28 s=12
```
Cross-referenced against the `.MLG` file for the same event, which stores an
8-byte timestamp at two offsets (trigger time and end time):
```
MLG format: [day:1][month:1][year:2 LE][?:1][hour:1][min:1][sec:1]
01 04 ea 07 00 00 1c 0c → trigger at April 1, 2026 00:28:12
01 04 ea 07 00 00 1c 0f → end time April 1, 2026 00:28:15 (3.0 s record time ✅)
```
**Record type** — encoded in `byte[1]` (sub_code), NOT as an ASCII string:
- `0x10``"Waveform"` (continuous / single-shot mode) ✅
- histogram sub_code: not yet confirmed — capture a histogram event with `debug=true`
**Peak particle velocity floats** (✅ CONFIRMED 2026-03-31, re-confirmed 2026-04-01):
Channel labels `"Tran"`, `"Vert"`, `"Long"`, `"MicL"` are embedded as
ASCII strings at variable offsets within the record. The PPV float for
@@ -803,26 +827,42 @@ each channel is at `label_offset + 6` (IEEE 754 big-endian float32).
The floats are **NOT 4-byte aligned** — Tran, Long, and MicL all fall at
non-aligned offsets. The previous heuristic step-4 scanner missed all three.
Example from 3-31-26 capture:
Confirmed offsets from thump event (2026-04-01, cross-referenced vs Blastware):
```
"Tran" at offset N → float at N+6 = 0.0916 in/s
"Vert" at offset M → float at M+6 = 0.0907 in/s
"Long" at offset P → float at P+6 = 0.0605 in/s
"MicL" at offset Q → float at Q+6 = 0.000145 psi
"Tran" at offset 99 → float at 105 = 0x3ED70A2D = 0.420 in/s ✅ Blastware: 0.420
"Vert" at offset 114 → float at 120 = 0x4077AE01 = 3.870 in/s ✅ Blastware: 3.870
"Long" at offset 129 → float at 135 = 0x3EFD7090 = 0.495 in/s ✅ Blastware: 0.495
"MicL" at offset 144 → float at 150 = 0x3985114E = 0.000254 psi
```
Channel labels are separated by inner-frame bytes `10 03` (DLE ETX),
preserved as literal data by `S3FrameParser`.
**Peak Vector Sum** (✅ CONFIRMED 2026-04-01):
```
Offset 87: IEEE 754 big-endian float32
= √(Tran² + Vert² + Long²) at the sample instant of maximum
combined geo motion
NOT the vector sum of the three per-channel peaks (those may
occur at different sample times)
Thump event: 0x4079F6C5 = 3.906 in/s ✅ matches Blastware "Peak Vector Sum: 3.906 in/s"
Near-ambient: 0x3C75C28F = 0.015 in/s (histogram event, near-zero ambient)
```
**Project strings** — ASCII label-value pairs (search for label, read null-terminated value):
```
"Project:" → project description
"Client:" → client name ✅ offset confirmed
"User Name:" → operator / user
"Seis Loc:" → sensor location
"Project:" → project description (present in 0C record ✅)
"Client:" → client name (NOT in 0C; comes from compliance config SUB 1A/E5 ❓)
"User Name:" → operator / user (NOT confirmed in 0C ❓)
"Seis Loc:" → sensor location (NOT confirmed in 0C ❓)
"Extended Notes"→ notes field
```
> ❓ **Clarification needed:** Only "Project:" has been confirmed in the 210-byte 0C record.
> "Client:", "User Name:", and "Seis Loc:" appear in the Blastware event report but their
> source in the protocol (0C vs SUB 1A/E5 compliance config) is not yet confirmed.
---
#### 7.7.6 Complete Download Loop (Python pseudocode)
@@ -854,28 +894,54 @@ return events
---
## 8. Timestamp Format
Two timestamp wire formats are used:
### 8.1 6-byte format (event index / 1E header)
> 🔶 **Updated 2026-02-26** — Year field resolved. Confidence upgraded.
Timestamps are 6-byte sequences appearing in event headers and waveform keys.
Appears in event index blocks. Time-of-day fields (hour/min/sec) are absent.
**Observed example:**
```
01 07 CB 00 06 1E
```
**Decoded:**
| Byte(s) | Value | Meaning | Certainty |
|---|---|---|---|
| `01` | 1 | Record validity / type flag | 🔶 INFERRED |
| `07 CB` | 1995 | Year — 16-bit big-endian integer | ✅ CONFIRMED — 2026-02-26 |
| `00` | 0 | Unknown — possibly hours, minutes, or padding | ❓ SPECULATIVE |
| `00` | 0 | Unknown separator | ❓ |
| `06` | 6 | Month (June) | ✅ CONFIRMED |
| `1E` | 30 | Day (0x1E = 30 decimal) | ✅ CONFIRMED |
> ✅ **2026-02-26 — CONFIRMED:** The year 1995 is the **MiniMate Plus factory default RTC date**, which the device reverts to whenever the internal battery is disconnected or the real-time clock loses power. Any event timestamped around 1995 means the clock was not set. This is known device behavior, not an encoding anomaly.
> ❓ **Still unknown:** The `00` byte at offset 3. Likely encodes time-of-day (hours or minutes). Needs a capture with a precisely known event time to decode.
### 8.2 9-byte format (Full Waveform Record / SUB 0C, bytes 08)
> ✅ **CONFIRMED 2026-04-01** — Cross-referenced against Blastware event report
> for BE11529 thump event: "00:28:12 April 1, 2026".
Full date + time, including a sub_code byte that encodes the recording mode.
**Observed example (thump event, 2026-04-01):**
```
01 10 04 07 ea 00 00 1c 0c
```
| Byte(s) | Value | Meaning | Certainty |
|---|---|---|---|
| `01` | 1 | Day | ✅ |
| `10` | 0x10 | Sub_code: `0x10` = Waveform (continuous mode) | ✅ / histogram code ❓ |
| `04` | 4 | Month (April) | ✅ |
| `07 ea` | 2026 | Year — 16-bit big-endian integer | ✅ |
| `00` | 0 | Unknown separator | ❓ |
| `00` | 0 | Hour | ✅ |
| `1c` | 28 | Minute | ✅ |
| `0c` | 12 | Second | ✅ |
The `.MLG` file for the same event stores the timestamp in a different binary
representation (little-endian year, no sub_code byte), confirming the waveform
record and the saved file use distinct serialisation formats.
---
@@ -1258,7 +1324,7 @@ The `.bin` files produced by `s3_bridge` are **not raw wire bytes**. The logger
| Question | Priority | Added | Notes |
|---|---|---|---|
| Byte at timestamp offset 3 — hours, minutes, or padding? | MEDIUM | 2026-02-26 | |
| Timestamp 6-byte format byte[3] — purpose of the separator `0x00` byte | LOW | 2026-02-26 | Not blocking; 9-byte waveform record format (§8.2) fully confirmed without this byte. |
| `trail[0]` in serial number response — unit-specific byte, derivation unknown. `trail[1]` resolved as firmware minor version. | MEDIUM | 2026-02-26 | |
| Full channel ID mapping in SUB `5A` stream (01/02/03/04 → which sensor?) | MEDIUM | 2026-02-26 | |
| Exact byte boundaries of project string fields in SUB `71` write frame — padding rules unconfirmed | MEDIUM | 2026-02-26 | |

View File

@@ -394,25 +394,30 @@ def _decode_waveform_record_into(data: bytes, event: Event) -> None:
The *data* argument is the raw record bytes returned by
MiniMateProtocol.read_waveform_record() — i.e. data_rsp.data[11:11+0xD2].
Extracts:
- record_type: "Histogram" or "Waveform" (string search) 🔶
- peak_values: label-based float32 lookup (confirmed ✅)
- project_info: "Project:", "Client:", etc. string search ✅
Timestamp in the waveform record:
7-byte format: [0x09][year:2 BE][0x00][hour][minute][second]
Month and day come from a separate source (not yet fully mapped ❓).
For now we leave event.timestamp as None.
Extracts (all ✅ confirmed 2026-04-01 against Blastware event report):
- timestamp: 9-byte format at bytes [0:9]
- record_type: sub_code at byte[1] (0x10 = "Waveform")
- peak_values: label-based float32 at label+6 for Tran/Vert/Long/MicL
- peak_vector_sum: IEEE 754 BE float at offset 87
- project_info: "Project:", "Client:", etc. string search
Modifies event in-place.
"""
# ── Timestamp ─────────────────────────────────────────────────────────────
# 9-byte format: [day][sub_code][month][year:2 BE][unknown][hour][min][sec]
try:
event.timestamp = Timestamp.from_waveform_record(data)
except Exception as exc:
log.warning("waveform record timestamp decode failed: %s", exc)
# ── Record type ───────────────────────────────────────────────────────────
# Decoded from byte[1] (sub_code), not from ASCII string search
try:
event.record_type = _extract_record_type(data)
except Exception as exc:
log.warning("waveform record type decode failed: %s", exc)
# ── Peak values ───────────────────────────────────────────────────────────
# ── Peak values (per-channel PPV + Peak Vector Sum) ───────────────────────
try:
peak_values = _extract_peak_floats(data)
if peak_values:
@@ -431,14 +436,24 @@ def _decode_waveform_record_into(data: bytes, event: Event) -> None:
def _extract_record_type(data: bytes) -> Optional[str]:
"""
Search the waveform record for a record-type indicator string.
Decode the recording mode from byte[1] of the 210-byte waveform record.
Confirmed types from 3-31-26 capture: "Histogram", "Waveform".
Returns the first match, or None if neither is found.
Byte[1] is the sub-record code that immediately follows the day byte in the
9-byte timestamp header at the start of each waveform record:
[day:1] [sub_code:1] [month:1] [year:2 BE] ...
Confirmed codes (✅ 2026-04-01):
0x10 → "Waveform" (continuous / single-shot mode)
Histogram mode code is not yet confirmed — a histogram event must be
captured with debug=true to identify it. Returns None for unknown codes.
"""
for rtype in (b"Histogram", b"Waveform"):
if data.find(rtype) >= 0:
return rtype.decode()
if len(data) < 2:
return None
code = data[1]
if code == 0x10:
return "Waveform"
# TODO: add histogram sub_code once a histogram event is captured with debug=true
return None
@@ -486,11 +501,23 @@ def _extract_peak_floats(data: bytes) -> Optional[PeakValues]:
if not vals:
return None
# ── Peak Vector Sum — fixed offset 87 (✅ confirmed 2026-04-01) ───────────
# = √(Tran² + Vert² + Long²) at the sample instant of maximum combined geo
# motion, NOT the vector sum of the three per-channel peak values (which may
# occur at different times). Matches Blastware "Peak Vector Sum" exactly.
pvs: Optional[float] = None
if len(data) > 91:
try:
pvs = struct.unpack_from(">f", data, 87)[0]
except struct.error:
pass
return PeakValues(
tran=vals.get("tran"),
vert=vals.get("vert"),
long=vals.get("long_"),
micl=vals.get("micl"),
peak_vector_sum=pvs,
)

View File

@@ -24,30 +24,52 @@ from typing import Optional
@dataclass
class Timestamp:
"""
6-byte event timestamp decoded from the MiniMate Plus wire format.
Event timestamp decoded from the MiniMate Plus wire format.
Wire layout: [flag:1] [year:2 BE] [unknown:1] [month:1] [day:1]
Two source formats exist:
1. 6-byte format (from event index / 1E header — not yet decoded in client):
[flag:1] [year:2 BE] [unknown:1] [month:1] [day:1]
Use Timestamp.from_bytes().
2. 9-byte format (from Full Waveform Record / 0C, bytes 08) ✅ CONFIRMED:
[day:1] [sub_code:1] [month:1] [year:2 BE] [unknown:1] [hour:1] [min:1] [sec:1]
Use Timestamp.from_waveform_record().
Confirmed 2026-04-01 against Blastware event report (BE11529 thump event):
raw bytes: 01 10 04 07 ea 00 00 1c 0c
→ day=1, sub_code=0x10 (Waveform mode), month=4, year=2026,
hour=0, minute=28, second=12 ← matches Blastware "00:28:12 April 1, 2026"
The sub_code at byte[1] is the record-mode indicator:
0x10 → Waveform (continuous / single-shot) ✅
other → Histogram (code not yet captured ❓)
The year 1995 is the device's factory-default RTC date — it appears
whenever the battery has been disconnected. Treat 1995 as "clock not set".
"""
raw: bytes # raw 6-byte sequence for round-tripping
flag: int # byte 0 — validity/type flag (usually 0x01) 🔶
year: int # bytes 12 big-endian uint16
unknown_byte: int # byte 3 — likely hours/minutes
month: int # byte 4
day: int # byte 5
raw: bytes # raw bytes for round-tripping
flag: int # byte 0 of 6-byte format, or sub_code from 9-byte format
year: int # ✅
unknown_byte: int # separator byte (purpose unclear)
month: int # ✅
day: int # ✅
# Time fields — populated only from the 9-byte waveform-record format
hour: Optional[int] = None # ✅ (waveform record format)
minute: Optional[int] = None # ✅ (waveform record format)
second: Optional[int] = None # ✅ (waveform record format)
@classmethod
def from_bytes(cls, data: bytes) -> "Timestamp":
"""
Decode a 6-byte timestamp sequence.
Decode a 6-byte timestamp (6-byte event-index format).
Args:
data: exactly 6 bytes from the device payload.
Returns:
Decoded Timestamp.
Decoded Timestamp (no time fields).
Raises:
ValueError: if data is not exactly 6 bytes.
@@ -68,6 +90,55 @@ class Timestamp:
day=day,
)
@classmethod
def from_waveform_record(cls, data: bytes) -> "Timestamp":
"""
Decode a 9-byte timestamp from the first bytes of a 210-byte waveform
record (SUB 0C / Full Waveform Record response).
Wire layout (✅ CONFIRMED 2026-04-01 against Blastware event report):
byte[0]: day (uint8)
byte[1]: sub_code / mode flag (0x10 = Waveform mode) 🔶
byte[2]: month (uint8)
bytes[34]: year (big-endian uint16)
byte[5]: unknown (0x00 in all observed samples ❓)
byte[6]: hour (uint8)
byte[7]: minute (uint8)
byte[8]: second (uint8)
Args:
data: at least 9 bytes; only the first 9 are consumed.
Returns:
Decoded Timestamp with hour/minute/second populated.
Raises:
ValueError: if data is fewer than 9 bytes.
"""
if len(data) < 9:
raise ValueError(
f"Waveform record timestamp requires at least 9 bytes, got {len(data)}"
)
day = data[0]
sub_code = data[1] # 0x10 = Waveform; histogram code not yet confirmed
month = data[2]
year = struct.unpack_from(">H", data, 3)[0]
unknown_byte = data[5]
hour = data[6]
minute = data[7]
second = data[8]
return cls(
raw=bytes(data[:9]),
flag=sub_code,
year=year,
unknown_byte=unknown_byte,
month=month,
day=day,
hour=hour,
minute=minute,
second=second,
)
@property
def clock_set(self) -> bool:
"""False when year == 1995 (factory default / battery-lost state)."""
@@ -76,7 +147,10 @@ class Timestamp:
def __str__(self) -> str:
if not self.clock_set:
return f"CLOCK_NOT_SET ({self.year}-{self.month:02d}-{self.day:02d})"
return f"{self.year}-{self.month:02d}-{self.day:02d}"
date_str = f"{self.year}-{self.month:02d}-{self.day:02d}"
if self.hour is not None:
return f"{date_str} {self.hour:02d}:{self.minute:02d}:{self.second:02d}"
return date_str
# ── Device identity ───────────────────────────────────────────────────────────
@@ -136,15 +210,28 @@ class ChannelConfig:
@dataclass
class PeakValues:
"""
Per-channel peak particle velocity / pressure for a single event.
Per-channel peak particle velocity / pressure for a single event, plus the
scalar Peak Vector Sum.
Extracted from the Full Waveform Record (SUB F3), stored as IEEE 754
big-endian floats in the device's native units (in/s / psi).
Extracted from the Full Waveform Record (SUB F3 / 0C response), stored as
IEEE 754 big-endian floats in the device's native units (in/s / psi).
Per-channel PPV location (✅ CONFIRMED 2026-04-01):
Found by searching for the 4-byte channel label string ("Tran", "Vert",
"Long", "MicL") and reading the float at label_offset + 6.
Peak Vector Sum (✅ CONFIRMED 2026-04-01):
Fixed offset 87 in the 210-byte record.
= √(Tran² + Vert² + Long²) at the sample instant of maximum combined
geo motion. NOT the vector sum of the three per-channel peak values
(those may occur at different times).
Matches Blastware's "Peak Vector Sum" display exactly.
"""
tran: Optional[float] = None # Transverse PPV (in/s) ✅
vert: Optional[float] = None # Vertical PPV (in/s) ✅
long: Optional[float] = None # Longitudinal PPV (in/s) ✅
micl: Optional[float] = None # Air overpressure (psi) 🔶 (units uncertain)
peak_vector_sum: Optional[float] = None # Scalar geo PVS (in/s) ✅
# ── Project / operator metadata ───────────────────────────────────────────────

View File

@@ -86,6 +86,9 @@ def _serialise_timestamp(ts: Optional[Timestamp]) -> Optional[dict]:
"year": ts.year,
"month": ts.month,
"day": ts.day,
"hour": ts.hour,
"minute": ts.minute,
"second": ts.second,
"clock_set": ts.clock_set,
"display": str(ts),
}
@@ -99,6 +102,7 @@ def _serialise_peak_values(pv: Optional[PeakValues]) -> Optional[dict]:
"vert_in_s": pv.vert,
"long_in_s": pv.long,
"micl_psi": pv.micl,
"peak_vector_sum": pv.peak_vector_sum,
}