feat: the export writes an MDF 3.30 file, so a window loads in ScryLab
A third format beside `.npy` and `.csv`, written by hand in `src/export.cpp` -- MF4 is HDF5 and would mean a library, MF3 is a handful of fixed blocks. The layout is the asammdf 8.8.27 reader's own field list, byte-diffed against a known-good 3.30 file it wrote; the shape is the one ScryLab's data-source contract asks for: one channel group, record-interleaved, the master as the x axis, and the unit in the channel's conversion block. So in MDF the master is `abs_seconds`, named `time` -- the clock that is the same in every display mode -- and the plotted axis goes out as an ordinary channel, `time_relative`. `ExportTable::Column` gained the CONFIG's unit, which only this writer spells out; the `.npy`/`.csv` bytes are unchanged, and `write_export()` now picks the writer for both the dialog and the probe.
This commit is contained in:
1 parent
761615c2fe
commit
908286ee89
10 files changed
+505
-28
No files matched your search
@@ -52,6 +52,40 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
|
||||
- **The export is collected at click time, not in the file-dialog callback.** The live
|
||||
view keeps scrolling while the chooser is up, so a late collection would write a
|
||||
window the user never saw. The writer is picked by the chosen path's extension.
|
||||
- **The MDF export is MDF 3.30 written by hand, and its layout is the reader's, not the
|
||||
standard's prose.** No dependency is added: MF4 is HDF5 and would need a library, while MF3 is a
|
||||
handful of fixed-size blocks, so `write_mdf()` spells them out. The field list comes from
|
||||
`asammdf` 8.8.27 (`blocks/v2_v3_blocks.py`, `blocks/mdf_v3.py`, which parses MF3 by explicit
|
||||
offsets) and was confirmed byte for byte against a known-good 3.30 file asammdf wrote (one
|
||||
group, a `time` master and two `V` channels). The shape, in file order: the **ID block** is 64
|
||||
bytes with *no id/length header* (`"MDF 3.30"`, byte order, version `330`) — a writer that
|
||||
emits `ID` + size + links at offset 0 produces something no reader accepts, since the first real
|
||||
block starts at 64; the **HD block** is 208 (164 common + 44 of the 3.2-era time fields) and
|
||||
points at the data group; a **TX** comment follows it; then the **samples**; the **DG** is 28; a
|
||||
**CC** (conversion, 46 — a `NONE` conversion is the common fields alone) then a **CN** (channel,
|
||||
228) precede each channel; and the **CG** (30) is last. Address fields are absolute file offsets
|
||||
written *forward*, so the whole address map is computed before a byte is written, and every
|
||||
block length is checked after it is built: a field at the wrong width does not fail the write, it
|
||||
leaves the reader in the middle of the next block.
|
||||
- **The data group's data address holds the raw records, with no `DT` header** — which is what
|
||||
asammdf's *writer* does and what its *reader* assumes (`DataBlock` is documented as "a pseudo
|
||||
block not defined by the MDF 3 standard"). This is the one place the file follows the reader
|
||||
rather than a narrow reading of the standard, and it is the assumption ScryLab compatibility
|
||||
rests on: a file with a `DT` block there would have its header read as sample bytes.
|
||||
- **In MDF the master is `abs_seconds`, named `time`, and the relative axis is a channel
|
||||
`time_relative`** — the same two numbers the `.npy`/`.csv` table holds, in the other roles. A
|
||||
master off the relative column is 0 at a different sample in each display mode, so it would put a
|
||||
live export and a held event's export on two timelines, which is the one thing the second column
|
||||
exists to avoid; both carry the unit `s`, and the signal columns follow in the `.npy`/`.csv`
|
||||
order. **The unit** is the client's CONFIG unit (a math channel's own), and it is written into
|
||||
the CC block because that is the only place MDF 3 keeps one — and the place ScryLab reads a
|
||||
signal's unit from. `ExportTable::Column` gained the field for it; the two older writers ignore
|
||||
it, and their bytes are unchanged. **The names** are sanitised and de-duplicated against each
|
||||
other *and* against the two reserved names before being cut to 31 bytes (the CN name field is 32
|
||||
with its NUL): ScryLab imports only the **first** occurrence of a channel name, so a cut made
|
||||
before de-duplication could silently drop a signal. `write_export()` is the one place the
|
||||
extension picks the writer (`.csv`, `.mf3`/`.mdf`, anything else `.npy`), and both the dialog and
|
||||
the self-check's export probe come through it so the two cannot offer different formats.
|
||||
- **The chooser runs before `ScopeModel` exists, and `--server` is what decides.** `Config` carries
|
||||
`server_specified`, set only by the `--server` branch, and `main()` asks only when it is false —
|
||||
so declining costs no buffer and no socket, and `ScopeModel`, `Acquisition`, `RowSet` and
|
||||
|
||||
Reference in new issue
Block a user