Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 15 additions & 8 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,9 @@ The words **MUST**, **MUST NOT**, **SHOULD**, **SHOULD NOT**, and **MAY** are no
| D-CAM-20 | Group capture | `sb/capture-group` fans one request out as independent per-camera capture jobs sharing an adapter-generated `captureGroupId`; the single deferred reply aggregates every member's terminal result | One operator action often needs an evidence set from several cameras. Aggregation fits the shipped single-reply command model plus the D-CAM-10 deferred reply; a core scatter-gather exchange (one request, multiple replies) is not required for v1 and is raised as a core question (§27). |
| D-CAM-21 | Capture thumbnail | Opt-in per capture profile (`thumbnail.size` = `small` 160px / `medium` 320px / `large` 640px, longest edge, aspect preserved, never upscaled); JPEG; carried in the ANNOUNCEMENT only, as native protobuf bytes; never in the durable record | A consumer on the bus can see the picture without fetching the file. It is bounded by the longest edge because cameras are 4:3 and 16:9 and a fixed W×H would distort or letterbox. It is announcement-only because the terminal body IS the committed document — the catalog's `terminal_result`, the metadata sidecar verbatim, and the body group replies embed — and a lossy, derived, disposable preview must not be durably stored per capture (D-CAM-13). It carries NO digest: a thumbnail is a lossy re-encode and a `sha256` beside the artifact's would invite a consumer to believe it is verifiable against it. A thumbnail that cannot be rendered, or will not fit the byte ceiling, is dropped and counted — it never fails a capture. The ceiling is the TRANSPORT's, resolved at startup: the Greengrass IPC client encodes a whole message into a static 10,000-byte buffer, so IPC carries `small` only (6 KiB budget) and a larger configured size is clamped down rather than rejected — the same config ships to Greengrass and to Kubernetes; MQTT carries all three (60 KiB budget, bounded by the library's 64 KiB binary-value cap, not by the broker). And a preview NEVER outranks the result: if an announcement carrying one cannot be published, the result is announced again without it. |
| D-CAM-23 | Standardized southbound error codes | Rename the routing/availability codes to the SOUTHBOUND.md §2.2 family: `INVALID_REQUEST` and `INSTANCE_REQUIRED` → `BAD_ARGS`, `UNKNOWN_INSTANCE` → `NO_SUCH_INSTANCE`, `CAMERA_UNAVAILABLE` → `DEVICE_UNAVAILABLE`. Domain codes (`CAPTURE_*`, `PTZ_*`, `STORAGE_PRESSURE`, `QUEUE_FULL`, …) are unchanged. **Hard rename, no aliases.** | The adapter's routing/availability codes were adapter-local while the rest of the ecosystem standardized on `BAD_ARGS`/`NO_SUCH_INSTANCE`/`DEVICE_UNAVAILABLE`, so a fleet-wide alarm rule keyed on the standard names could not match this adapter. This is a **wire-visible breaking change** for any consumer keying on the old strings; the component is pre-1.0/experimental and a documented breaking change is acceptable where a silent one is not. `INSTANCE_REQUIRED` and `INVALID_REQUEST` both collapse to `BAD_ARGS` because both are argument faults. Consumer grep at implementation time found no matches in `edge-console` or `bottling-company-test` for the old strings. |
| D-CAM-24 | Lifecycle verbs `sb/pause` / `sb/resume`; `repoll` N/A | Serve the standardized `sb/pause` / `sb/resume` lifecycle verbs: they suspend/admit a camera's new capture work (scheduled and commanded), leave in-flight captures to run to completion, refuse new capture work while paused with the stable `INSTANCE_PAUSED` code, are idempotent (`{ paused, changed }`), and surface in `sb/status`. Pause is process-local (a restart begins unpaused) and scoped to capture workload — `sb/reconnect`, PTZ, and read-only verbs are unaffected. **`repoll` is recorded N/A.** | Pause/resume are the standard instance-control family a real adapter serves (SOUTHBOUND.md §2.2). `repoll` triggers an immediate poll cycle for poll-loop adapters; this adapter is capture-on-demand and schedule-driven, not poll-based, so there is no poll cycle to trigger — `sb/capture` / `sb/capture-submit` are the on-demand trigger and schedules drive the periodic work. Adding a redundant `repoll` alias would only duplicate `sb/capture` while needing its own idempotency key, so it is not implemented; the decision is recorded here and in `docs/reference/messaging-interface.md` per the issue. |
| D-CAM-24 | Lifecycle verbs `sb/pause` / `sb/resume`; `repoll` N/A | Serve the standardized `sb/pause` / `sb/resume` lifecycle verbs: they suspend/admit a camera's new capture work (scheduled and commanded), leave in-flight captures to run to completion, refuse new capture work while paused with the standardized top-level `PAUSED` code (SOUTHBOUND.md §2.2), are idempotent (`{ paused, changed }`), and surface in `sb/status`. Pause is process-local (a restart begins unpaused) and scoped to capture workload — `sb/reconnect`, PTZ, and read-only verbs are unaffected. **`repoll` is recorded N/A.** | Pause/resume are the standard instance-control family a real adapter serves (SOUTHBOUND.md §2.2). `repoll` triggers an immediate poll cycle for poll-loop adapters; this adapter is capture-on-demand and schedule-driven, not poll-based, so there is no poll cycle to trigger — `sb/capture` / `sb/capture-submit` are the on-demand trigger and schedules drive the periodic work. Adding a redundant `repoll` alias would only duplicate `sb/capture` while needing its own idempotency key, so it is not implemented; the decision is recorded here and in `docs/reference/messaging-interface.md` per the issue. |
| D-CAM-25 | Operational metric families | `camera_captures` becomes a `(Total, Interval)` counter family accumulated on the capture hooks and drained on the 30 s sampler tick (the interval counter preserves the every-event guarantee — the drain is a reset, not a sample). Add a `CameraCommand` family dimensioned `instance` × `verb` × `result` with `commandRequests`/`commandErrors` pairs and a `commandLatencyMs` sum, its cells defined lazily on first use. `camera_queue` stays a set of sampled gauges. **Recorded cardinality decision:** `camera_captures` / `camera_queue` stay **fleet-scoped** (no per-camera dimension); only the lower-frequency `CameraCommand` carries `instance`. | The two capture families lacked the `(Total, Interval)` pattern the rest of the ecosystem uses, so a fleet dashboard could not read them the same way as the other adapters. Per-camera dimensions on the capture families are deliberately avoided: a 256-camera fleet would mint 256 streams per measure on the highest-frequency families (the same reasoning that kept the fleet-scope originally), and pre-populating `instance × verb × result` for the command family would cold-define 256 × 16 × 2 streams — so `CameraCommand` is defined lazily and only the families whose frequency justifies it carry `instance`. Per-camera capture state remains answerable through `sb/queue-status` and the per-instance connectivity keepalive. |
| D-CAM-26 | Edge-console panel trio | Register `overview` / `signals` / `diagnostics` panels via `register_panel` (order 10/20/30, `scope: "instance"`), bound to the verbs the adapter serves: overview → `sb/status`/`sb/reconnect`/`sb/pause`/`sb/resume`; signals → `sb/list`/`sb/status`/`sb/capture`/`sb/capture-status`; diagnostics → `sb/discover`/`sb/queue-status`. | The baseline advertises a descriptor-driven panel surface to edge-console; registering the trio on the same inbox as the verbs, before the acknowledged subscription begins, advertises the panels atomically with the command surface they drive. |
| D-CAM-26 | Edge-console panel trio | Register `overview` / `signals` / `diagnostics` panels via `register_panel` (order 10/20/30, `scope: "instance"`), bound to the verbs the adapter serves: overview → `sb/status`/`sb/reconnect`/`sb/pause`/`sb/resume`; signals → `sb/list`/`sb/status`/`sb/capture`/`sb/capture-status`; diagnostics → `sb/discover`/`sb/queue-status`. Every panel meets the renderable-descriptor floor: `summary`/`keyValueList` widgets carry `rows`, `commandSummary` widgets carry `verbs`, widgets carry `id`/`title`, and no widget names a `writeVerb`. The signal-adapter widget kinds are absent by design — no `signalGrid` (no `sb/signals`/`sb/read` signal inventory; `cameraRoster`/`captureSurface` are the camera-domain equivalents) and no `treeBrowser` (no hierarchical `sb/browse`; `sb/discover` is an active scan, not a browse). | The baseline advertises a descriptor-driven panel surface to edge-console; registering the trio on the same inbox as the verbs, before the acknowledged subscription begins, advertises the panels atomically with the command surface they drive. |
| D-CAM-27 | Committed `Cargo.lock`; `data-types.md` N/A | Commit `Cargo.lock` (drop the `/Cargo.lock` gitignore + rationale), regenerated with the local `.cargo` `[patch]` override inactive so it records the pinned git source and is valid on a fresh clone / in CI. The RTSP native-coverage harness (`simulators/run-rtsp-native-coverage.ps1`), which mounts the workspace read-only, keeps its **writable-overlay** lockfile — a single-file bind mount from outside the read-only source tree that masks the committed lock and is (re)generated in-container by a prep run — so cargo never has to rewrite the committed git-sourced lock on the read-only mount. The adapter's signal `data-types.md` page is **N/A**: this component publishes capture announcements (`app/image/*`), not `SouthboundSignalUpdate` envelopes, so the signal value-mapping page does not apply; the published shapes are documented in `messaging-interface.md`. | The three-way constraint (a committed lock must record the git source ⊕ a `[patch]`-ed local build rewrites it to a path source ⊕ the read-only validation mount cannot rewrite it) is resolved deliberately rather than by suppressing the `edgecommons component validate` warning: the committed lock is git-sourced and reproducible on a fresh clone/CI, and the two local frictions (a `git status` that shows lock churn while the patch is active, and the read-only harness) are handled by simply not committing that churn and by running the harness with the patch inactive. The manifest `license` field is reconciled to `BUSL-1.1` to match the `LICENSE` file (`Fixes #7`). |
| D-CAM-28 | `config.schema.json` | Author a Draft 2020-12 `config.schema.json` whose **root models `component.global`** (with `$defs/camera` for one `component.instances[]` entry and `$defs` for every backend/profile/schedule/enum), derived field-for-field from `src/config.rs`. Strict (`additionalProperties:false`) everywhere the parser is (`deny_unknown_fields`); permissive only where the parser is (`featureOverrides`, `resourceGroups`). | `edgecommons component validate` could not check camera configs and warned on the missing schema. The CLI validates `component.global` against the schema root (`ec-validate/schema.rs` checks `/component/global` only), so the root is `component.global`; `$defs/camera` documents and lets external Draft 2020-12 validators check `instances[]`, matching the scaffold's `$defs/device`. Validated: the shipped `deploy/docker/simulator-config.json` and the §10.1 full example ACCEPT; a malformed global key, an ONVIF backend missing `mediaProfile`, a bad enum, an unknown backend field, and a camera missing `backend` all REJECT. `edgecommons component validate` reports **no findings** (schema valid, config clean, lockfile warning resolved). |
| D-CAM-22 | Bare-RTSP backend | A distinct `rtsp` backend addresses a camera by a raw `rtsp://`/`rtsps://` URL, with no ONVIF. It is still-image only, reuses the shared RTSP engine (`RtspCaptureController`) and the network/credential/TLS primitives, and advertises `capture_modes=[rtsp-frame]` with all PTZ/snapshot/discovery capabilities off. To make it buildable without ONVIF, the protocol-neutral net/auth primitives and the credential-resolution seam are lifted from the `onvif` module into a shared `backend::net` module, and the `rtsp` cargo feature is decoupled from `onvif`. | ONVIF gives identity, capability discovery, media profiles, snapshot, PTZ, and the governed stream URI; a raw RTSP URL gives none of these, so it is a genuinely different camera kind rather than a mode of `onvif-rtsp` — a separate backend keeps the ONVIF backend's required-field invariants (`deviceServiceUrl`/`mediaProfile`) intact. `connect()` performs the RTSP `DESCRIBE`/`SETUP` + auth + SDP/codec validation so a dead URL, bad auth, or unsupported codec fails at connect (the supervisor keeps such a camera OFFLINE rather than falsely ONLINE, since reachability is inferred from a successful connect). The URL carries no credentials (userinfo is rejected); credentials are `$secret` references resolved through the same bounded EdgeCommons path as ONVIF, and the same host-allowlist / DNS-pin / RTSPS-SNI / forbidden-address policy applies to the user-supplied URL. Decoupling the feature lets an operator ship an RTSP-only binary without the ONVIF surface. |
Expand Down Expand Up @@ -1503,7 +1503,7 @@ must round-trip unchanged.
| `NO_SUCH_INSTANCE` | Target camera ID is not configured. |
| `CAMERA_DISABLED` | The configured camera is disabled. |
| `DEVICE_UNAVAILABLE` | Camera is offline (session down or unreachable) and the command policy does not wait. |
| `INSTANCE_PAUSED` | The camera is paused (`sb/pause`) and refuses new capture work until resumed. |
| `PAUSED` | The camera is paused (`sb/pause`) and refuses new capture work until resumed. |
| `CAMERA_MOVING` | Capture/PTZ interlock rejected capture while the camera was moving. |
| `UNSUPPORTED_CAPABILITY` | Camera/backend cannot perform the requested operation. |
| `UNKNOWN_CAPTURE_PROFILE` | Named profile is not configured for the camera. |
Expand Down Expand Up @@ -2000,21 +2000,28 @@ Greengrass recipes and MQTT ACLs grant only:

### 19.1 Standard southbound health

The adapter emits the standard `southbound_health` metric dimensioned by `instance` with the current
contract measures:
The adapter emits the standard `southbound_health` metric dimensioned by `instance` with exactly the
eight SOUTHBOUND §5 contract measures:

- `connectionState`: 1 when the camera session is online, otherwise 0;
- `signalsSubscribed`: the signal inventory the session serves — a camera serves no
`SouthboundSignalUpdate` signals (its data points are images announced on `app/image/*`), so the
gauge reads 0 (the contract's "0 while disconnected" holds trivially);
- `publishLatencyMs`: terminal application-message publication latency;
- `pollLatencyMs`: the most recent capture/status acquisition round-trip where available;
- `readErrors`: acquisition/status read errors in the interval;
- `writeErrors`: device-path failures of allow-listed southbound signal writes — the adapter exposes
no signal-write surface (`sb/write` is not served; PTZ failures ride `CameraPtz`), so the interval
counter reads 0;
- `staleSignals`: 1 when the camera has no successful health/capture observation within the configured
stale threshold, otherwise 0.
stale threshold, otherwise 0;
- `reconnects`: sessions re-established in the interval.

`publishLatencyMs` is observed on the terminal announcement that reached the transport; an announcement
that could not be published contributes no latency and is counted as `announcementFailed` instead.

The optional standard `reconnects` measure is also emitted. Additional capture, queue, storage, and PTZ
measures below are camera-specific metrics; they do not redefine `southbound_health`.
Additional capture, queue, storage, and PTZ measures below are camera-specific metrics; they do not
redefine `southbound_health`.

### 19.2 Camera metrics

Expand Down
6 changes: 3 additions & 3 deletions docs/reference/messaging-interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,7 @@ session, not the camera); ledgered and settled immediately.

**What it does.** Suspends a camera's new capture work: while paused, scheduled captures are held and new
commanded captures (`sb/capture`, `sb/capture-submit`, `sb/capture-group`, `sb/capture-group-submit`) are
refused with `INSTANCE_PAUSED`. Captures already in flight run to completion. The verb is idempotent, and
refused with `PAUSED`. Captures already in flight run to completion. The verb is idempotent, and
`changed` reports whether the state moved. Pause is in-memory only — a restart begins unpaused. It applies
only to capture workload: `sb/reconnect`, `sb/ptz*`, and the read-only verbs are unaffected, and the paused
state is surfaced by `sb/status`.
Expand Down Expand Up @@ -599,7 +599,7 @@ lifecycle event.
## Stable errors

The public error `code` is one of `BAD_ARGS`, `NO_SUCH_INSTANCE`, `CAMERA_DISABLED`,
`DEVICE_UNAVAILABLE`, `INSTANCE_PAUSED`, `CAMERA_MOVING`, `UNSUPPORTED_CAPABILITY`,
`DEVICE_UNAVAILABLE`, `PAUSED`, `CAMERA_MOVING`, `UNSUPPORTED_CAPABILITY`,
`UNKNOWN_CAPTURE_PROFILE`, `QUEUE_FULL`, `GROUP_TOO_LARGE`, `RESOURCE_LIMIT`, `CAPTURE_TIMEOUT`,
`CAPTURE_CANCELLED`, `PROCESS_INTERRUPTED`, `CAPTURE_NOT_FOUND`, `IDEMPOTENCY_CONFLICT`,
`PREVIOUS_OUTCOME_UNKNOWN`, `REPLY_REQUIRED`, `UNSUPPORTED_PIXEL_FORMAT`, `STORAGE_PRESSURE`,
Expand All @@ -608,7 +608,7 @@ The public error `code` is one of `BAD_ARGS`, `NO_SUCH_INSTANCE`, `CAMERA_DISABL

`BAD_ARGS` covers a malformed request body, an out-of-range field, and instance routing: a missing
`instance` when more than one camera is configured is `BAD_ARGS`, and an unknown `instance` is
`NO_SUCH_INSTANCE`. `INSTANCE_PAUSED` is returned when a camera has been paused with `sb/pause` and is
`NO_SUCH_INSTANCE`. `PAUSED` is returned when a camera has been paused with `sb/pause` and is
asked to accept new capture work. The routing and availability codes (`BAD_ARGS`, `NO_SUCH_INSTANCE`,
`DEVICE_UNAVAILABLE`) are the standardized southbound names shared across EdgeCommons adapters
(`core/docs/SOUTHBOUND.md` §2.2); the domain codes (`CAPTURE_*`, `PTZ_*`, `STORAGE_PRESSURE`, …) are
Expand Down
2 changes: 2 additions & 0 deletions docs/reference/metrics.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ a camera connects or disconnects.
| Measure | Unit | Meaning |
|---|---|---|
| `connectionState` | Count | 1 while the camera's session is live, 0 otherwise. |
| `signalsSubscribed` | Count | The signal inventory the session serves. A camera serves no `SouthboundSignalUpdate` signals — its data points are images announced on `app/image/*` — so the gauge reads 0. |
| `publishLatencyMs` | Milliseconds | How long the camera's last terminal message took to reach the transport. Absent until one has. |
| `pollLatencyMs` | Milliseconds | The last acquisition round-trip. Absent until the camera has produced a frame. |
| `readErrors` | Count | Acquisition failures in the interval. A failure to encode or to write to disk is not counted: it is not the camera's fault. |
| `writeErrors` | Count | Device-path failures of southbound signal writes. The adapter serves no signal-write surface (PTZ failures are counted in `CameraPtz`), so the counter reads 0. |
| `staleSignals` | Count | 1 when the camera has produced nothing within `healthThresholds.staleSignalSecs`, 0 otherwise. A camera can be connected and stale. |
| `reconnects` | Count | Sessions re-established in the interval. A camera's first connection is not a reconnect. |

Expand Down
Loading
Loading