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
5 changes: 3 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ the bus carries control and terminal metadata (`app/image/*` announcements, `evt
It serves the canonical `southbound_health` metric plus operational families (`camera_captures`,
`camera_queue`, `CameraCommand`), the standardized lifecycle verbs `sb/pause` / `sb/resume` /
`sb/reconnect`, and 16 domain `sb/*` verbs (`sb/capture` and friends, `sb/ptz*`, queue verbs) on the
D-U28 component command inbox — SOUTHBOUND.md §2.2 sanctions `sb/capture`-style domain verbs. Runs on
D-U28 two-scope command inbox — SOUTHBOUND.md §2.2 sanctions `sb/capture`-style domain verbs. Runs on
`GREENGRASS` / `HOST` / `KUBERNETES` via `edgecommons`, with no platform branching in this component.

## The seam
Expand Down Expand Up @@ -56,7 +56,8 @@ the standard `edgecommons` envelope, owned by the canonical schema and not redec

- Southbound routing/availability error codes are the standardized `BAD_ARGS` / `NO_SUCH_INSTANCE` /
`DEVICE_UNAVAILABLE` (SOUTHBOUND.md §2.2); domain codes (`CAPTURE_*`, `PTZ_*`, …) are camera-specific.
- Instance routing is D-EIP-13/D-U28: body `instance`, optional iff exactly one camera is configured.
- Instance routing is D-EIP-13/D-U28: body `instance`, optional iff exactly one camera is configured;
an instance-addressed command topic routes by its token, which is authoritative (SOUTHBOUND.md §2.2).
- Builders/facades are the construction path (`app()`, `events()`, `commands()`, `MetricBuilder`) —
never hand-built topics or envelopes.
- Runtime artifacts (durable state DBs, captured images, TLS fixtures, logs, build output) stay out of
Expand Down
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,10 @@ native-all = ["genicam", "rtsp"]
capacity-harness = []

[dependencies]
# Pinned to a published rev so CI (a standalone repo) can resolve it. Local dev builds against
# Pinned to a published rev (rust-lib/v0.4.0) so CI (a standalone repo) can resolve it. Local dev builds against
# the sibling checkout via the gitignored `.cargo/config.toml` [patch] override, which replaces
# this source outright. Same pattern as file-replicator.
edgecommons = { git = "https://github.com/edgecommons/edgecommons.git", rev = "36a70c48b65b35f77bfab70d3a73869debdfc407", default-features = false, features = ["credentials"] }
edgecommons = { git = "https://github.com/edgecommons/edgecommons.git", rev = "ef4c6248eec9cabc0acfc88eb0698528aa83a3ab", default-features = false, features = ["credentials"] }
anyhow = "1"
async-trait = "0.1"
bytes = "1"
Expand Down Expand Up @@ -101,7 +101,7 @@ rustls-pemfile = { version = "2", optional = true }
tokio-rustls = { version = "0.26", optional = true }

[dev-dependencies]
edgecommons = { git = "https://github.com/edgecommons/edgecommons.git", rev = "36a70c48b65b35f77bfab70d3a73869debdfc407", default-features = false, features = ["credentials", "standalone"] }
edgecommons = { git = "https://github.com/edgecommons/edgecommons.git", rev = "ef4c6248eec9cabc0acfc88eb0698528aa83a3ab", default-features = false, features = ["credentials", "standalone"] }
proptest = "1"
tokio = { version = "1", features = ["io-util", "macros", "net", "rt-multi-thread", "test-util"] }

Expand Down
29 changes: 16 additions & 13 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ The words **MUST**, **MUST NOT**, **SHOULD**, **SHOULD NOT**, and **MAY** are no
| D-CAM-15 | PTZ | Common normalized command contract mapped through backend capabilities | ONVIF provides the initial PTZ implementation; unsupported backends return a capability error. |
| D-CAM-16 | Fleet safety | Layered bounded queues and byte-based admission | Camera count alone is not a safe memory or bandwidth bound. |
| D-CAM-17 | Delivery | Integrate with `file-replicator` through disk and metadata, not code coupling | Keeps acquisition and delivery independently deployable. |
| D-CAM-18 | Command addressing | Use the shipped component `main` inbox and select camera `instance` in the body | Matches the shipped CommandInbox contract and both shipped adapters. **Resolved by core decision D-U28** (optional-instance UNS addressing: instance present ⇒ instance-scoped, absent ⇒ component/global-scoped, retiring the `main` sentinel), which supersedes the Phase 5 per-instance `cmd/sb/*` addressing in `core/docs/SOUTHBOUND.md` §2.2. The adapter currently ships the `main`-inbox + body-`instance` behavior; migration to optional-instance topic addressing (instance-scope `.../{instance}/cmd/sb/{verb}`, component/fleet `.../cmd/sb/{verb}`) is tracked under the D-U28 rollout, which also updates `core/docs/SOUTHBOUND.md`. |
| D-CAM-18 | Command addressing | Use the shipped component `main` inbox and select camera `instance` in the body | Matches the shipped CommandInbox contract and both shipped adapters. **Resolved by core decision D-U28** (optional-instance UNS addressing: instance present ⇒ instance-scoped, absent ⇒ component/global-scoped, retiring the `main` sentinel), which supersedes the Phase 5 per-instance `cmd/sb/*` addressing in `core/docs/SOUTHBOUND.md` §2.2. **Adopted with core 0.4.0** (rust-lib/v0.4.0, D-CAM-29): the inbox serves both scopes and the topic instance token is authoritative for every immediate-reply verb; `sb/capture`/`sb/capture-group` keep body-only routing pending a core scoped-outcome registration (the recorded gap in D-CAM-29). |
| D-CAM-19 | Outbox acknowledgement | Withdrawn | There is no outbox and no acknowledgement to wait for. Terminal announcements publish once, best effort. Durable, acknowledged delivery is a generic messaging concern and belongs in the EdgeCommons messaging service as an opt-in augmentation across all four languages, available to any component, rather than being reimplemented inside one. |
| 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. |
Expand All @@ -145,6 +145,7 @@ The words **MUST**, **MUST NOT**, **SHOULD**, **SHOULD NOT**, and **MAY** are no
| 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-29 | Core 0.4.0 adoption: scoped instance routing + conditional availability | Pin `edgecommons` at rust-lib/v0.4.0 (ef4c6248). Every immediate-reply verb is registered via `register_scoped` (SOUTHBOUND §2.2 / D-U28): the delivery topic’s `{instance}` token is authoritative — `scoped_request` refuses a conflicting `body.instance` with `BAD_ARGS`, injects a topic-only token as the routing selector (so the registry resolves it, `NO_SUCH_INSTANCE` for an unknown token), and passes component-scoped deliveries through to the existing body routing; verbs whose closed schemas carry no camera selector (`sb/list`, `sb/discover`, `sb/capture-group-submit`, `sb/capture-cancel`) refuse an instance-addressed delivery with `BAD_ARGS` rather than silently ignoring the token. `sb/discover`’s configuration-conditional availability is published into `describe` via `set_command_availability` (`disabled` with a reason while `global.discovery.enabled` is false; reapplied on committed reloads through the configuration listener). `receivedTs` is **N/A**: the adapter is a direct camera client with no upstream broker hop to stamp a receive time. | **Recorded gap, surfaced up front:** core 0.4.0 exposes the addressed-instance token only to the immediate-reply scoped registration; the two deferred verbs (`sb/capture`, `sb/capture-group`) must stay on `register_outcome` — trading deferred settlement (the dispatch permit is released for the capture’s duration and `sb/capture-cancel` settles the held reply) for token visibility would regress the command plane — so they route by the body on either topic and the topic token does not route them. Closing it needs a core scoped-outcome registration (a core follow-up, not an adapter workaround). PTZ capability is per-camera and runtime-discovered, so it is deliberately NOT mirrored into component-scope availability. |
| 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. |

## 5. System context
Expand Down Expand Up @@ -978,12 +979,14 @@ ecv1/{device}/{component}/{instance}/{class}[/{channel...}]
Commands use at most two channel tokens (`sb/{verb}`), so they remain valid when `topic.includeRoot`
reduces the channel budget.

The component command inbox plus body `instance` rule is the camera adapter's messaging contract. Org-level
core decision **D-U28** defines an optional-instance UNS grammar: the instance token is present for
instance-scoped traffic and absent for component/global-scoped traffic, and there is no `main` sentinel. The
camera adapter is single-instance at the component level, so its commands are component-scope
`ecv1/{device}/camera-adapter/cmd/sb/{verb}`; a specific camera is selected by the body `instance` field,
not by a per-instance command topic.
The two-scope command inbox is the camera adapter's messaging contract. Org-level core decision
**D-U28** defines an optional-instance UNS grammar: the instance token is present for instance-scoped
traffic and absent for component/global-scoped traffic, and there is no `main` sentinel. The adapter
serves both command scopes (core 0.4.0, D-CAM-29): component-scope
`ecv1/{device}/camera-adapter/cmd/sb/{verb}` selects a camera by the body `instance` field, and
instance-addressed `ecv1/{device}/camera-adapter/{instance}/cmd/sb/{verb}` routes by the topic token,
which is authoritative (a conflicting body `instance` is `BAD_ARGS`). The deferred verbs `sb/capture` /
`sb/capture-group` route by the body only (the D-CAM-29 recorded gap).

### 12.2 Core prerequisites

Expand Down Expand Up @@ -2504,12 +2507,12 @@ Reviewers should explicitly decide:
6. Should PTZ preset mutation stay disabled by default as proposed?
7. Physical-camera validation is waived for this project because no hardware is available; a future
hardware-certified release must select models for the compatibility matrix.
8. Command addressing — **resolved.** Core decision D-U28 adopts an optional-instance UNS grammar
(instance present ⇒ instance-scoped, absent ⇒ component/global-scoped, retiring the `main` sentinel),
superseding both the Phase 5 per-instance `cmd/sb/*` target in `core/docs/SOUTHBOUND.md` §2.2 and this
adapter's body-`instance` selection. The adapter ships the `main`-inbox + body-`instance` behavior
today; it migrates to optional-instance topic addressing under the D-U28 rollout, which updates
`core/docs/SOUTHBOUND.md`.
8. Command addressing — **resolved and adopted.** Core decision D-U28 adopts an optional-instance UNS
grammar (instance present ⇒ instance-scoped, absent ⇒ component/global-scoped, retiring the `main`
sentinel), and `core/docs/SOUTHBOUND.md` §2.2 now specifies addressed-instance routing. The adapter
serves both scopes via the core 0.4.0 scoped registration (D-CAM-29): the topic instance token is
authoritative for every immediate-reply verb; the deferred capture verbs keep body-only routing
pending a core scoped-outcome registration (the D-CAM-29 recorded gap).
9. Should the core add a scatter-gather message exchange pattern — one request producing multiple
correlated replies or a streamed reply set? v1 group capture deliberately aggregates member results
into one reply within the existing single-reply command model. A core pattern would also serve other
Expand Down
Loading
Loading