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
34 changes: 13 additions & 21 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,18 +105,14 @@ jobs:
run: rustup toolchain install 1.85 --profile minimal


# NOTE: no `--locked`, and `Cargo.lock` is not tracked. Three things cannot all be true at once:
# 1. a committed lockfile must record the pinned GIT source, or it lies to a fresh clone;
# 2. local dev builds through the gitignored `.cargo/config.toml` `[patch]`, and a patched
# build REWRITES the lockfile to a PATH source;
# 3. simulators/run-rtsp-native-coverage.ps1 mounts the workspace READ-ONLY, so a git-sourced
# lock plus the patch means cargo must rewrite it and cannot -- the build dies outright.
# Tracking the lockfile satisfies (1) and breaks (2) and (3); a patched lockfile satisfies (2)
# and (3) and lies about (1). `file-replicator` resolves this by not tracking it. So do we.
# `--locked` against the committed `Cargo.lock`, which records the pinned GIT source (it is
# regenerated on a fresh clone with the local `.cargo` `[patch]` inactive, so it never carries a
# path override, and the MSRV-aware resolver held every dependency to a 1.85-compatible version).
# CI has no `.cargo` override, so the lock resolves the pinned rev reproducibly.
- name: Check on the declared MSRV
env:
CARGO_NET_GIT_FETCH_WITH_CLI: "true"
run: cargo +1.85 check --all-targets
run: cargo +1.85 check --locked --all-targets

rtsp-backend:
# The `rtsp` (GStreamer) backend was compiled by NOTHING. The jobs above build default features
Expand Down Expand Up @@ -178,25 +174,21 @@ jobs:

image:
# The Dockerfile was built by NOTHING. Not by CI, not by any script -- the only automated docker
# builds in this repo are the simulator validation images, which are different files. So when
# 3c0d83d stopped tracking `Cargo.lock`, the `COPY camera-adapter/Cargo.lock` on line 35 and the
# two `cargo build --locked` invocations became a build that CANNOT SUCCEED in a clean clone --
# `"/camera-adapter/Cargo.lock": not found` -- and it stayed that way, because a developer with a
# stale lock still lying around in their working tree never saw it. The shipped container image
# was unbuildable by anyone who cloned the repo, and nothing said a word.
# builds in this repo are the simulator validation images, which are different files. This job
# builds the shipped image so it cannot silently rot.
#
# This job builds it. Both targets, from a clean checkout, and it runs the binary it produced --
# compiling is not the same as producing a working image, and the RTSP target exists precisely to
# carry a GStreamer runtime the ONVIF one does not.
# It builds both targets from a clean checkout `--locked` against the committed `Cargo.lock` (which
# records the pinned git source; the image context carries no `.cargo` override) and runs the
# binary it produced -- compiling is not the same as producing a working image, and the RTSP target
# exists precisely to carry a GStreamer runtime the ONVIF one does not.
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4

# A clean checkout has no `Cargo.lock` (it is untracked -- see the note in the `msrv` job). If a
# future change reintroduces `--locked` or copies the lock, the build dies here, which is the
# entire point of this job.
# A clean checkout carries the committed `Cargo.lock`; the Dockerfile copies it and builds
# `--locked` for a reproducible image. This job proves the image builds from a clean clone.
- name: Build the default image (onvif)
run: docker build --target onvif -t camera-adapter:onvif .

Expand Down
25 changes: 25 additions & 0 deletions .github/workflows/deploy-docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: deploy-docs

# Doc-only pushes skip the build CI (see paths-ignore in ci.yml) but should still refresh the
# EdgeCommons docs site, which aggregates this repo's docs/ at build time. This hits the Cloudflare
# Workers Builds deploy hook for the edgecommons-docs project to trigger a rebuild (a few seconds of
# Actions, not the full build). Enable by setting the repo Actions secret CLOUDFLARE_DEPLOY_HOOK to
# the deploy-hook URL; without it this is a no-op (docs rebuild on the next edgecommons main push).
on:
push:
branches: [main]
paths: ['docs/**', '**.md']

jobs:
trigger-docs-rebuild:
runs-on: ubuntu-latest
steps:
- name: Trigger Cloudflare docs rebuild
env:
HOOK: ${{ secrets.CLOUDFLARE_DEPLOY_HOOK }}
run: |
if [ -n "$HOOK" ]; then
curl -fsS -X POST "$HOOK" && echo "triggered edgecommons-docs rebuild"
else
echo "CLOUDFLARE_DEPLOY_HOOK not set; skipping (docs rebuild on the next edgecommons main push)"
fi
8 changes: 3 additions & 5 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,9 @@ simulators/generated/
__pycache__/
*.py[cod]

# Cargo.lock is NOT tracked. See .github/workflows/ci.yml — a local build through the gitignored
# .cargo/config.toml [patch] rewrites it to a path source, and the RTSP validation container mounts
# the workspace read-only, so a git-sourced lock cannot be rewritten there either. The three cannot
# all be true. file-replicator does the same.
/Cargo.lock
# Cargo.lock IS committed (git-sourced; regenerate with the .cargo [patch] inactive so it records the
# pinned git rev, not a path source). A local build through the gitignored .cargo/config.toml [patch]
# rewrites it to a path source — do not commit that churn.

# The ONVIF simulator TLS material is MINTED, never committed: simulators/generate-tls-fixtures.{sh,ps1}.
# A private key in a public repository is a private key in a public repository, however worthless.
Expand Down
63 changes: 63 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# camera-adapter — component notes

EdgeCommons **southbound camera adapter** (Rust). Crate/binary `camera-adapter`, GG component
`com.mbreissi.edgecommons.CameraAdapter`. Depends on the `edgecommons` Rust library. If this repo
lives inside the EdgeCommons org umbrella workspace, read its root `AGENTS.md` first (org repo map,
design-fidelity contract, validation matrix, platform/transport model); everything below is this
component's own detail.

## What it is

Connects to cameras (ONVIF/RTSP, bare RTSP, GenICam/Aravis, plus an in-process simulator), captures
still images on demand and on schedules, and publishes capture announcements onto the Unified
Namespace. It is a **camera**, not a signal adapter: a data point is an image on disk, not a
`SouthboundSignalUpdate`. The image bytes are the data plane (files, delivered by `file-replicator`);
the bus carries control and terminal metadata (`app/image/*` announcements, `evt` operator events).

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
`GREENGRASS` / `HOST` / `KUBERNETES` via `edgecommons`, with no platform branching in this component.

## The seam

`src/backend/`'s `CameraBackendFactory` / `CameraSession` trait pair is the one place protocol
knowledge lives; the `SimBackend` implementation is compiled into every build and drives the
deterministic test suite. Everything above the seam — the durable job catalog (`src/catalog.rs`), the
fleet capture scheduler (`src/dispatch.rs`), the command plane (`src/runtime/command.rs`), the metric
families (`src/observability.rs`) — is written against the trait and does not change when a protocol
is added. The boundary rule: a backend knows protocols; it does not know EdgeCommons topics, the UNS,
envelopes, or metrics.

## Config location

This component's settings live under `component.global` / `component.instances[]` in the EdgeCommons
config document (`config.schema.json` is the contract; `src/config.rs` is the parser); the sibling
sections (`tags`, `hierarchy`, `identity`, `messaging`, `metricEmission`, `logging`, `heartbeat`) are
the standard `edgecommons` envelope, owned by the canonical schema and not redeclared here.
`test-configs/` and `docs/sample-configurations.md` carry runnable examples.

## Validation expectations

- `cargo test` covers every module against the simulator and a mocked device-control channel — no
network, no broker, no camera required.
- The coverage gate is **90% line + a 95% diff gate** (`.github/workflows/ci.yml`'s `coverage` job):
`cargo llvm-cov report --summary-only --fail-under-lines 90` plus a `diff-cover --fail-under=95` on
the changed lines. Do not lower a gate or exclude testable code to pass it — add tests.
- The `rtsp` (GStreamer) and `genicam` (Aravis) backends need system libraries; they are compiled and
exercised in the simulator-backed containers (`simulators/rtsp_validation.Dockerfile`,
`simulators/native_all_validation.Dockerfile`) and the `rtsp-backend` CI job, not the default build.
Their live-fixture suites are `#[ignore]`d and run against MediaMTX / real devices.
- `Cargo.lock` **is committed** (git-sourced), so a fresh clone and CI resolve reproducibly.
- `edgecommons component validate` checks this repo's config against `config.schema.json`.

## Org conventions this component inherits

- 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.
- 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
Git.
21 changes: 21 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# camera-adapter (Claude Code)

EdgeCommons southbound camera adapter (Rust), `com.mbreissi.edgecommons.CameraAdapter`. The full
picture — what this component is, the backend seam, config location, and the org conventions it
inherits — lives in `AGENTS.md` and is shared with every agent tool. It is imported here in full:

@AGENTS.md

## Local-dev notes

- **Sibling library override.** `Cargo.toml` pins the `edgecommons` dependency by git `rev`. For local
development a gitignored `.cargo/config.toml` `[patch]` block redirects that pin at your sibling
`core/libs/rust` checkout, so a plain `cargo build` tracks your working copy without touching the
committed pin CI uses. `Cargo.lock` is committed and records the git source; a `[patch]`-ed build
rewrites it to a path source locally — do not commit that churn.
- **Simulators.** `SimBackend` is compiled into every build and drives the deterministic suite with no
hardware. The ONVIF/RTSP/GenICam simulators under `simulators/` back the live validation containers;
`simulators/run-rtsp-native-coverage.ps1` runs the RTSP native-coverage harness (run it with the
`.cargo` `[patch]` inactive so cargo never needs to rewrite the read-only-mounted committed lock).
- **Feature builds.** Default features are `standalone,onvif` (pure Rust, Windows-buildable). `rtsp`
needs GStreamer and `genicam` needs Aravis ≥ 0.8.36 — build those in the simulator containers or WSL.
Loading
Loading