# Sensor bundles Every sensor configuration the Studio offers is a folder under `apps/studio/config/sensors/` holding `sensor.json` — the processing configuration. `orisys_app_shared.sensors.bundle_catalog` scans that folder and the writable user root beside it, and resolves each candidate through the SDK's `orisys.sensors.resolve_bundle`, so the picker shows what the SDK decided rather than a second opinion computed in the UI. `finger/` also carries `manifest.json`, the bundle descriptor. A descriptor states the profile's identity and its files instead of leaving both to be inferred, which is what lets the bundle keep its meaning when the folder is moved or renamed. ## Picker rows A configuration row in the sensor dialog is **the bundle's name and nothing else**: the descriptor's `display_name` when there is one, otherwise the folder name (`box`, `mini`, `finger`, `finger-mems`, and whatever the operator called their own folder). Everything else — the origin, the resolved root, the `profile_id`, the bundle shape, the SDK's verdict with what it means, the calibration selection and every finding — is written in the details surface directly under the selector, where the sentences fit. One exception: two entries offered together that would otherwise read alike each get a suffix. Across roots that is the origin (`Box (shipped)` beside `Box (user)`); when one root supplies both (for example one `display_name` declared by two folders) the origin cannot separate them and the folder is used instead. Nothing is collapsed or shadowed, and the same tree always produces the same rows. ## The finger descriptor | Field | Value | Why | |---|---|---| | `schema_version` | `1` | The SDK's current descriptor schema (`CURRENT_DESCRIPTOR_SCHEMA`). | | `profile_id` | `orisys.finger.r01` | The processing profile this folder is: finger design revision `r01`, whose unroll map and camera scale the configuration pins. Never a product-family name. | | `display_name` | `Finger` | Shown in the picker. Excluded from the processing fingerprint. | | `product_family` | `finger` | What the operator calls it. `finger-mems` is a different family on a different pipeline. | | `processing_backend` | `uv_unroll` | The pipeline the finger actually runs: `FingerRuntime` builds a UV domain from the export's unroll map, then flow and force. | | `sensor_json` | `sensor.json` | The configuration, still the only place camera, tracker and contact settings live. | | `computational_assets.unroll_map` | `assets/30fps-0703/unroll_map_uv_s0.42.npz` | The map the chain loads. `finger_worker` derives that name for itself from the export folder and `camera.source_scale`, so the descriptor and the chain are checked against each other. Required: without it the profile fails. | | `assets.scene` | `scene/finger-display-1.0.0.oryscene` | The compiled package the 3D pane draws. Visualization only: a missing one never blocks processing. | Every path is relative to the bundle root, and the SDK refuses an absolute path or a `..` escape when it parses the descriptor. `assets/visualization/` is deliberately left out of `assets`: it is offline compiler input, the packaged build strips it, and nothing at runtime reads it. `sensor.json` declares `"type": "single_camera"` while the descriptor says `uv_unroll`. With a descriptor present the descriptor decides, and it is the truthful one — the finger path never builds a single-camera processor. Do not "correct" `type`: it is part of the hashed configuration (only `name`, `version` and `display_name` are skipped), so editing it would re-identify every recorded finger pass and change no number. ## Compatibility keys `name`, `assets_dir` and `viz_pose` are kept beside the descriptor fields — the shape this bundle had before it carried a descriptor. They are relative too, so they do not weaken rename safety, and they keep readers that never learned about the descriptor working: | Key | Reader | |---|---| | `assets_dir` | `orisys/configs.resolve_sensor_bundle` → `resolve_sensor_asset_dir`, which `orisys.finger.exports` consults for a bundled export; `acquisition/geometry.py::describe_sensor_geometry`, the device-geometry check the 3D pane runs; `build/studio/build.ps1`, which checks the packaged copy; `orisys-sdk/examples/gui/viewer3d/presets.py`. | | `viz_pose` | `orisys-sdk/examples/viz/mesh_camera.py`. | | `name` | Nothing reads it today; it is kept because it belongs to the legacy shape. | Dropping a key means migrating those readers first, not tidying up the JSON. ## Verification ### Multi-point force calibration Finger sessions accept SDK schema 3 files through the existing calibration-file picker and operator-confirmed SN. The SDK checks processing settings, geometry and coordinate-map identity; a mismatch blocks loading rather than falling back to raw values. Keep the JSON and its declared NPZ artifacts together. The file format and supported models are defined in the [SDK calibration contract](../../../../orisys-sdk/docs/calibration-model-contract.zh.md). After starting, remove all load and select **Unloaded zero**. Confirm that the next 30 processed frames are unloaded. Contact, invalid readings or tracker reset cancel an unfinished measurement. Zero values belong to this session; tracker reset and model replacement require a new zero. Model replacement is applied between frames on the SDK HHD thread. The rail reports per-axis zero, contact, coverage and range states; an unavailable newton value stays unavailable while raw measurements remain accessible. Selecting a tracker reference alone never implies zero load. For bare-video recomputation, explicitly confirm the unloaded 30-frame interval starting at the selected reference frame. This choice changes the analysis cache identity; a short or invalid interval fails the pass. Leave the option unchecked for legacy calibration or when no unloaded interval has been verified. Recording format 1.5 appends `calibration_status` and `calibration_reference` JSON columns to `forces.csv`; existing columns keep their positions. Raw validity and per-axis model validity are distinct. Recordings store each schema 3 JSON with its artifacts in a directory identified by the SDK model fingerprint, so recorded calibration remains loadable without the original folder. Older recordings remain readable and carry no invented zero or model-state information. `apps/studio/tests/test_sensor_catalog_root.py` asserts the descriptor's fields, that every declared path is relative and present, that the declared map is the one the running chain derives for itself (the SDK's `scaled_map_cache_path`, so `_s0.42.npz` and never a neighbouring `_s0.4.npz`), and that a copy of the bundle under another folder name keeps its `profile_id`, its processing fingerprint and all of its resolved paths inside the copy. The descriptor grammar itself is owned by the SDK: [orisys-sdk/docs/sensor-bundles.md](../../../../orisys-sdk/docs/sensor-bundles.md).