core: Add per-device IR calibration model and store
Calibration records how a given IR camera delivers illuminated frames so
capture can be optimized per device. It is purely an optimization and
capability layer: `capture_ir_face` already works without it (capture a
burst, keep the brightest), so a missing/unmatched/corrupt record just
falls back to the safe mode-agnostic burst — never an error. That's why
this store, unlike the enrollment store, needs no HMAC.
# Device identity (card + bus)
Records are keyed by `CameraIdentity { card, bus }`. Neither field alone
is a usable key: `/dev/videoN` renumbers across reboots, the card name
isn't unique across identical models, and the bus (port topology)
changes when a device is replugged elsewhere. `resolve_calibration`
matches leniently, returning a typed `CalibrationMatch`:
- `Exact` — card + bus both match.
- `MovedPort` — card matches, bus differs, and exactly one connected
camera has that card name, so the device just moved ports: safe to
adopt and refresh the stored bus.
- `Ambiguous` — card matches but several connected cameras share that
name, so we can't guess which: the caller should ask the user.
- `None` — no record; fall back to mode-agnostic capture.
This mirrors the decisions-in-core / rendering-in-CLI shape of
`camera::select::IrCameraResolution`.
# Store
`CalibrationStore` persists all records in one small CBOR file (the set
is tiny and the matching API wants it whole). Writes are atomic
(temp-file + fsync + rename), matching the enrollment store's crash
discipline. `upsert` replaces by exact identity; `replace_by_card` drops
a stale-bus record when a device moved ports.
# CameraInfo.bus
`CameraInfo` gains a `bus: Option<String>`, filled by `enumerate()` from
the V4L2 bus info and left `None` for mock/synthetic sources and the
by-path `open()` path. This is the input `CameraIdentity::from_info`
reads. Calibration shares the `v4l2` feature gate since it depends on the
backend's `IrCaptureMode`.