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
25 changes: 24 additions & 1 deletion DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ The words **MUST**, **MUST NOT**, **SHOULD**, **SHOULD NOT**, and **MAY** are no
| D-CAM-29 | Core 0.4.0 adoption: scoped instance routing + conditional availability | Adopt core 0.4.0's immediate-reply scoped registration: the delivery topic's `{instance}` token authoritative for every immediate-reply verb, adapter-side conflict refusal, and `sb/discover`'s configuration-conditional availability 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 — this part of the decision stands unchanged). `receivedTs` is **N/A**: the adapter is a direct camera client with no upstream broker hop to stamp a receive time. PTZ capability is per-camera and runtime-discovered, so it is deliberately NOT mirrored into component-scope availability. | **The recorded gap is CLOSED by D-CAM-30 (core 0.5.0).** Core 0.4.0 exposed the addressed-instance token only to the immediate-reply registration, so `sb/capture`/`sb/capture-group` routed by the body and the topic token did not route them — surfaced up front here as a gap needing a core scoped-outcome registration. Core 0.5.0's breaking two-form surface delivers exactly that: the deferred verbs now receive the addressed instance like every other verb, with deferred settlement unchanged. The adapter-side routing/conflict layer this entry introduced (`register_scoped` + `scoped_request`) is deleted — addressing enforcement is library-owned. |
| D-CAM-30 | Core 0.5.0 adoption: declared verb scope + scoped deferred captures + keepalive instance state | Pin `edgecommons` at rust-lib/v0.5.0 (a14a3285). Every verb registers through the two-form surface `register(verb, scope, handler)` / `register_outcome(verb, scope, handler)` with a declared `CommandScope` (SOUTHBOUND §2.2 / D-SC-2), derived from its closed request schema: **`Component`** for the selector-less verbs (`sb/list`, `sb/discover`, `sb/capture-group`, `sb/capture-group-submit`, `sb/capture-cancel` — fleet answers, `instances[]` targets, durable capture/group ids), **`Instance`** for per-camera actuation (`sb/capture`, `sb/capture-submit`, `sb/reconnect`, `sb/ptz`, `sb/ptz-presets`, `sb/pause`, `sb/resume`), **`Both`** for the dual-semantics verbs where no addressing means the whole component (`sb/status` every camera, `sb/queue-status` the fleet, `sb/capture-status` component-wide lookups, `sb/queue-clear` the `allCameras` drain). The library enforces addressing ahead of dispatch (conflict-first `BAD_ARGS`, `Component`-scope rejection, D-SC-4); the adapter's hand-rolled `scoped_request` layer is deleted, keeping only the D-SC-4 component-side policies: the optional-iff-one configured-camera default and `NO_SUCH_INSTANCE` for an unknown name (`addressed_request` seeds the library-resolved token into the body selector those policies read). **The deferred verbs are scoped too — closing the D-CAM-29 gap:** `sb/capture` (`Instance`) and `sb/capture-group` (`Component`) register through the scoped outcome form, so the topic token routes a deferred capture while deferred settlement, permit release, and `sb/capture-cancel` settling the held reply are unchanged. Companion (D-SC-7): the state keepalive's `instances[]` `state` comes from the single instance state model that answers `sb/status` — a paused camera reports `PAUSED` (shared `CONNECTING`/`ONLINE`/`BACKOFF`/`PAUSED` vocabulary) while `connected` keeps reporting reachability; the exact wire element is pinned via the now-public `InstanceConnectivity::to_json`. | The 0.4.0 model needed an adapter-side enforcement layer and still left the two most consequential verbs blind to the envelope. With the declared scope the library owns addressing for every registration form, the camera class of gap is structurally impossible (D-SC-1), and `describe` advertises each verb's scope for the console. Dual-meaning verbs gain first-class component-wide semantics instead of overloading "no instance named". `PAUSED` in the keepalive lets a console distinguish expected-quiet from silently-stale (D-SC-8) without a second bookkeeping path. |
| 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. |
| D-CAM-31 | Simulator `playlist` pattern | `frame.pattern` accepts `{ "playlist": { "directory", "include", "order", "loop" } }` beside the four synthetic tokens. The directory is walked once at connect — deterministic, symbolic links refused, bounded at 10,000 files and 32 levels, and empty is a connect failure — ordered `sorted` (by relative path) or `seeded` (SplitMix64 Fisher-Yates over the sorted list, keyed by the simulator seed), and replayed one file per capture through the **unchanged** finalize path: temp write, fsync, sidecar first, atomic visibility, catalog row, `ImageCaptured`, thumbnail where configured. A JPEG member captured under `passthrough`/`raw` is delivered as the bytes on disk, so `image.sha256` is the source file's digest; every other combination decodes to `RGB8`/`Mono8` and re-encodes through the ordinary encoding stage. `backendMetadata.playlist` carries `sourcePath` and `index`; the session-status surface carries `playlist{count,index,directory}`. | The synthetic patterns prove plumbing, not vision: a downstream anomaly or classification model needs real imagery, and `image-processor`'s tier-4 end-to-end rehearsal (D-IP-18) needs it arriving through the real camera path — genuine sidecars, digests, and announcements — rather than from a fixture that writes files behind the adapter's back. Reading the directory at connect rather than per capture keeps an unbounded filesystem walk off every frame's acquisition deadline and gives the replay a fixed list to be deterministic about; `sb/reconnect` is the re-read. **Byte-preserving replay is JPEG-only**, because `OutputEncoding::Passthrough` already requires a declared complete JPEG source: extending it to PNG would mean a new `PixelFormat` variant and a changed encoding contract for every backend, so a PNG member is decoded and re-encoded instead and only a JPEG member yields a sidecar digest equal to the file's. The cursor advances once per capture and nothing else: a per-trigger hold was considered and dropped, because it would have to carry a trigger identity across the backend seam -- which knows protocols and not EdgeCommons message shapes -- to serve a case no configuration needs. |

## 5. System context

Expand Down Expand Up @@ -281,6 +282,28 @@ classDiagram
The production interface MUST be mockable without a native camera library. The in-process `sim` backend
is a required implementation, not only a test fixture hidden behind conditional compilation.

#### 6.2.1 Simulator frame sources

`frame.pattern` selects what a simulated capture acquires. The four synthetic generators (`color-bars`,
`gradient`, `checkerboard`, `solid`) draw pixels from the simulator seed and the capture ordinal. The
`playlist` pattern replays a directory of real image files: the directory is walked once at connect,
ordered `sorted` (ascending by relative path) or `seeded` (a SplitMix64 Fisher-Yates shuffle of that sorted
list, keyed by the simulator seed), and each capture yields the next file's bytes with that file's own
dimensions and format. Every capture takes the next file. `loop` restarts replay after the last file; with
`loop: false` a capture past the end fails `DEVICE_UNAVAILABLE`.

A replayed frame takes the same path as any other frame. A JPEG member captured under a byte-preserving
output (`passthrough` or `raw`) is handed on as the bytes on disk, so the announced `image.sha256` is the
source file's digest; every other combination is decoded to `RGB8`/`Mono8` pixels and re-encoded by the
ordinary encoding stage. The originating file travels with the frame as `backendMetadata.playlist`
(`sourcePath`, `index`) and so reaches the terminal announcement, the catalog's terminal result, and the
metadata sidecar; the session-status surface reports `playlist` (`count`, `index`, `directory`).

Containment is structural: the walk refuses every symbolic link and descends only real directories under the
canonicalized root, and the same check runs again at capture time because the directory is a live
filesystem. The walk is bounded at 10,000 matching files and 32 levels, and a directory that matches no file
fails the connect rather than accepting captures it would refuse one at a time.

### 6.3 Threading and blocking I/O

- Tokio tasks MAY manage camera state, timers, queues, messaging, HTTP, and durable catalog work.
Expand Down Expand Up @@ -2267,7 +2290,7 @@ build checks.

| Simulator | Purpose | Required scenarios | Limitation |
|---|---|---|---|
| In-process `SimBackend` | Fast deterministic camera fleet | 1–1,024 cameras, delays, disconnects, bad frames, PTZ ranges, cancellation, memory pressure | Does not validate a protocol stack. |
| In-process `SimBackend` | Fast deterministic camera fleet, and replay of real imagery | 1–1,024 cameras, delays, disconnects, bad frames, PTZ ranges, cancellation, memory pressure, `playlist` replay of a directory of JPEG/PNG files | Does not validate a protocol stack. |
| Aravis fake GigE Vision camera (`arv-fake-gv-camera`, sometimes version-suffixed by distribution) | Real Aravis discovery and acquisition path | Software trigger, payload size, incomplete/timeout injection where supported, reconnect | Primarily GigE Vision; not a USB3 Vision substitute. |
| GStreamer `videotestsrc` with `gst-rtsp-server`, or a pinned MediaMTX test service fed by generated video | RTSP negotiation and frame extraction | H.264/H.265 where licensed/available, reconnect, codec change, slow first frame, invalid stream | Does not provide ONVIF control. |
| In-repository ONVIF device simulator | Deterministic SOAP, auth, capability, snapshot, and PTZ behavior | GetCapabilities, media profiles, GetSnapshotUri, Digest auth, PTZ operations/presets, faults, hostile URI/redirect | Must be maintained with the component contract. |
Expand Down
42 changes: 40 additions & 2 deletions config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -527,9 +527,47 @@
"description": "Capture mechanism."
},
"simPattern": {
"enum": ["color-bars", "gradient", "checkerboard", "solid"],
"description": "Simulator frame pattern: one of the synthetic generators, or a playlist that replays a directory of real image files.",
"default": "color-bars",
"description": "Simulator frame pattern."
"oneOf": [
{ "enum": ["color-bars", "gradient", "checkerboard", "solid"] },
{
"type": "object",
"additionalProperties": false,
"required": ["playlist"],
"properties": { "playlist": { "$ref": "#/$defs/simPlaylist" } }
}
]
},
"simPlaylist": {
"type": "object",
"additionalProperties": false,
"required": ["directory"],
"description": "Replay of a directory of real image files, read once when the camera connects.",
"properties": {
"directory": {
"type": "string",
"description": "Absolute directory holding the image files."
},
"include": {
"type": "array",
"items": { "type": "string", "minLength": 1 },
"minItems": 1,
"default": ["**/*.jpg", "**/*.jpeg", "**/*.png"],
"description": "Case-sensitive globs matched against each file's `/`-separated path relative to `directory`. `**` spans path segments, `*` matches within one segment, `?` matches one character."
},
"order": { "$ref": "#/$defs/simPlaylistOrder" },
"loop": {
"type": "boolean",
"default": true,
"description": "Whether replay restarts at the first file after the last one."
}
}
},
"simPlaylistOrder": {
"enum": ["sorted", "seeded"],
"default": "sorted",
"description": "Playlist replay order: ascending by relative path, or a deterministic shuffle derived from the simulator seed."
},
"genicamTransport": {
"enum": ["auto", "gige-vision", "usb3-vision"],
Expand Down
44 changes: 44 additions & 0 deletions docs/how-to-guides.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,50 @@ Put no credentials in the `url` (`rtsp://user:pass@…` is rejected) — supply
or discovery. It is built with the `rtsp` feature (which no longer requires `onvif`) plus the GStreamer
runtime. See the [sample configuration](sample-configurations.md#5-bare-rtsp-camera-no-onvif).

## Replay real images with the simulator

Point the `sim` backend at a directory of images to feed a downstream vision component real pictures through
the real camera path. Each capture takes the next file and finalizes it the way a camera frame is finalized:
the metadata sidecar lands first, the image becomes visible atomically, the catalog records the job, and
`ImageCaptured` announces the result.

To replay a directory of images:

1. Put the images in a directory the adapter can read. Only regular files join the playlist, and the walk
rejects symbolic links, so a camera whose directory holds one refuses to connect.
2. Set the camera's `frame.pattern` to a `playlist` object that names that absolute `directory`.
3. Choose the capture profile's output encoding. `passthrough` installs a JPEG member byte for byte, so
`image.sha256` is the digest of the source file. `jpeg`, `png`, and `tiff` decode the member and re-encode
it.
4. Start the camera and capture. `sb/capture` and schedules both draw from the same playlist.

```json
"backend": {
"type": "sim",
"frame": {
"pattern": {
"playlist": {
"directory": "/srv/line-clearance/reference-images",
"include": ["**/*.jpg", "**/*.jpeg", "**/*.png"],
"order": "sorted",
"loop": true
}
}
}
}
```

The adapter reads the directory once, when the camera connects, so images added later take effect on
`sb/reconnect`. `order: "seeded"` shuffles the list deterministically from the camera's `seed`, which gives a
repeatable order that is not alphabetical. `loop: false` replays each file once and then fails further
captures with `DEVICE_UNAVAILABLE`, which is how you drive a fixed-length rehearsal.

Every replayed capture names its source. The terminal `ImageCaptured` body and the metadata sidecar beside
the image both carry `backendMetadata.playlist.sourcePath`, the file's path relative to the playlist
directory, and `backendMetadata.playlist.index`, its position in the replay order. With `passthrough` output
the installed file is the source file, so a consumer that verifies `image.sha256` is verifying the image an
operator put in the directory.

## Hand completed files to file-replicator

The adapter and [file-replicator](https://docs.edgecommons.mbreissi.com/components/file-replicator/) couple
Expand Down
16 changes: 16 additions & 0 deletions docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,22 @@ authority; they do not disable per-connection address validation.
`pattern`), `connectDelayMs`, `captureDelayMs` (default 10), PTZ capability switches, and deterministic
fault counters. It is intended for configured test and development cameras.

`frame.pattern` is either one of the four synthetic generators (`color-bars` by default, `gradient`,
`checkerboard`, `solid`), which draw pixels from the seed and the capture ordinal, or a `playlist` object
that replays a directory of real image files. A playlist requires an absolute `directory` and accepts
`include` (case-sensitive globs matched against each file's `/`-separated path relative to `directory`,
default `**/*.jpg`, `**/*.jpeg`, `**/*.png`; `**` spans path segments, `*` matches within one segment, `?`
matches one character), `order` (`sorted` by relative path, or `seeded` for a deterministic shuffle keyed by
the camera's `seed`), and `loop` (default true; with `false` a capture past the last file fails
`DEVICE_UNAVAILABLE`). Every capture replays the next file. The directory is read once when
the camera connects and holds at most 10,000 files nested at most 32 levels deep; symbolic links are
refused. `frame.width`, `frame.height`, and `frame.pixelFormat` configure the synthetic generators and are
not consulted for a playlist — each capture reports the replayed file's own dimensions and format. A JPEG
member captured under a `passthrough` or `raw` profile is installed byte for byte, so `image.sha256` is the
source file's digest; every other combination decodes the member to pixels and re-encodes it. The terminal
body and the metadata sidecar carry `backendMetadata.playlist.sourcePath` and
`backendMetadata.playlist.index`.

`rtsp` is a bare-RTSP backend for a camera addressed directly by an `rtsp://` or `rtsps://` `url`, with no
ONVIF device. It captures still frames only — `captureMode` is `rtsp-frame`, its single valid value — and
exposes no PTZ, snapshot, or discovery. The `url` must carry no embedded credentials (`rtsp://user:pass@…`
Expand Down
Loading
Loading