# Calibration sequence format (`.oryseq`) A calibration sequence is a small JSON document: the **profile** that generated the points, plus the **points** themselves. It is written by `导出序列…` in the Sequence tab and read by `导入序列…`. Why JSON and not a binary container: a sequence is at most a few hundred points of text. It is read by a human, diffed in review, and emailed to whoever runs the bench. Binary would buy nothing and cost all three. (The *acquired data* of a run is a different problem — per-point time series are large and will need a sidecar; that decision belongs to workflow P8, not here.) ## Conventions The format follows this repository's existing documents rather than inventing a convention. Where it matches `session.json` and `force_calibration.json`, that is deliberate: | Convention | Value | |---|---| | Version field | `"format_version": "1.0"` — a dotted **string**, compared as a tuple | | Identity | `"kind": "autocal.sequence"` — checked on import | | Extension | `.oryseq` by convention; import accepts any `.json` | | Encoding | UTF-8, `indent=2`, `ensure_ascii=False`, trailing newline | | Write | temp file then `Path.replace` — a half-written sequence that still parses looks like a design | ## The document ```json { "format_version": "1.0", "kind": "autocal.sequence", "created_at": "2026-09-20T10:31:00+00:00", "name": "5 点灵敏度扫描", "note": "", "versions": { "app": "0.1.0", "shared": "0.1.0", "shared_api": "0.1" }, "device": { "sensor_type": "box", "config_ref": "…\\config\\sensors\\box\\sensor.json", "geometry_id": "finger-1.0.0", "probe_tip_set": true }, "frame": { "targets": "platform", "length_unit": "mm", "units_per_mm": 10.0 }, "profile": { "mode": "flat", "pitch_mm": 2.0, "margin_mm": 1.0, "radius_mm": 0.0, "clear_height_mm": 5.0, "approach_speed_mm_s": 2.0, "press_speed_mm_s": 0.5, "press_depth_mm": 0.2, "dwell_s": 0.5, "settle_window_s": 0.5, "repeats": 3, "order": "serpentine" }, "points": [ { "id": 1, "target_mm": { "x": 0.0, "y": 0.0, "z": 12.4 }, "sensor_mm": { "x": 0.0, "y": 0.0, "z": 0.0 }, "clearance_mm": 5.0, "enabled": true, "note": "" } ], "summary": { "count": 12, "enabled": 12, "min_mm": { "x": 0.0, "y": 0.0, "z": 12.4 }, "max_mm": { "x": 10.0, "y": 4.0, "z": 12.4 }, "span_mm": { "x": 10.0, "y": 4.0, "z": 0.0 }, "pitch_mm": 2.0, "estimated_duration_s": 380.0 } } ``` ## Fields ### `frame` — required | Field | Meaning | |---|---| | `targets` | the frame `target_mm` is expressed in; today always `"platform"` | | `length_unit` | `"mm"`; anything else is refused | | `units_per_mm` | the pulse equivalent the points were built with | A sequence whose numbers do not say whether they are millimetres or user units is a sequence that will eventually be read wrong. `units_per_mm` is recorded so a file from a differently-configured axis is detectable rather than silently believed. ### `profile` — required The twelve fields the Sequence tab edits, and nothing else. A field absent from the file is **kept at its current value and reported**, never silently defaulted to another bench's number. ### `points` — required, may be empty | Field | Meaning | |---|---| | `id` | 1-based, stable within the file; used for run progress and resume | | `target_mm` | where the axes go — required, with all three of `x`, `y`, `z` | | `sensor_mm` | where that point lands on the sensor; optional but written | | `clearance_mm` | the standoff for this point | | `enabled` | false = designed but deliberately skipped | | `note` | free text | Both positions are stored on purpose. The platform target is what the run uses; the sensor position is what makes the point checkable against the surface. Deriving one from the other on every load would silently change what the points mean the moment a sensor is re-fixtured — and a re-fixture is exactly when a stored transform should be *noticed*, not recomputed from. ### `summary` — derived, not authoritative Present so a human reading the file sees the point count and travel span without counting rows, and so a diff is legible. It is **recomputed on load**; a mismatch is reported as a warning and the recomputed values win. That is how a hand-edited document announces itself instead of quietly disagreeing with its own contents. `estimated_duration_s` assumes a constant traverse rate (`sequence_io.ASSUMED_TRAVERSE_MM_S`) and ignores acceleration. It is a planning figure and is labelled an estimate wherever it is shown. ### `device` — a reference, not a requirement Records which sensor the sequence was designed for. Importing a sequence for another sensor is **allowed and warned about**, because re-fixturing deliberately is a normal thing to do. ## Import rules Every rejection names the specific problem — `invalid file` tells an operator nothing. | Condition | Behaviour | |---|---| | not JSON | refused, with line and column | | top level not an object | refused | | `kind` missing or different | refused, naming what was found | | `format_version` missing or not a string | refused | | `format_version` newer than this build | refused, naming both versions | | `frame` missing, or `length_unit` not `mm` | refused | | `profile` not an object | refused | | a profile key absent | imported; the current value is kept and the key is reported | | `points` absent | imported as profile-only, and reported | | a point without `target_mm.x/y/z` | refused, naming the point and the axis | | `summary` disagreeing with `points` | imported; recomputed, and reported | | current design is unsaved | the operator is asked before it is replaced | Version strings are compared as tuples, never as strings: `"1.10"` is newer than `"1.9"`, and a string comparison would make a newer file look older and load it as if it were supported. ## Compatibility rules - **Adding an optional key** does not change `format_version`. - **Adding a required key, or changing a field's meaning or unit**, does. - A reader must refuse a document whose version it does not support rather than interpreting the parts it recognises — a sequence half-applied is worse than one rejected. - `1.x` readers are expected to accept `1.y` for `y <= x` on the same major version and report anything they did not use. ## Where the pieces live | Concern | Module | |---|---| | The document, validation, the derived summary | `apps/autocal/src/orisys_autocal/sequence_io.py` | | The design screen and its controls | `apps/autocal/src/orisys_autocal/ui/sequence_panel.py` | | The file dialogs and the unsaved-work guard | `apps/autocal/src/orisys_autocal/ui/window.py` | | Why a sequence is an artifact (P8) | `apps/autocal/docs/workflow.md` |