diff --git a/.gitea/workflows/validate.yml b/.gitea/workflows/validate.yml new file mode 100644 index 0000000..9fc8ce3 --- /dev/null +++ b/.gitea/workflows/validate.yml @@ -0,0 +1,32 @@ +# Checks the dataflow against the typed contract in dora-node.yml. This runs no +# node, so it needs neither a camera nor the ZED SDK. +name: validate + +on: [push, pull_request] + +env: + DORA_VERSION: v1.0.1 + DORA_TARGET: x86_64-unknown-linux-gnu + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install the dora CLI + run: | + curl -fsSL -o dora-cli.tar.gz \ + "https://github.com/dora-rs/dora/releases/download/${DORA_VERSION}/dora-cli-${DORA_TARGET}.tar.gz" + tar xzf dora-cli.tar.gz + mkdir -p "$HOME/.local/bin" + install -m 755 "dora-cli-${DORA_TARGET}/dora" "$HOME/.local/bin/dora" + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + rm -rf dora-cli.tar.gz "dora-cli-${DORA_TARGET}" + + - run: dora --version + + # strict_types: true in dataflow.yml turns a type warning into a non-zero + # exit, so a mismatched annotation fails here rather than at runtime. + - name: Validate the dataflow + run: dora validate dataflow.yml diff --git a/.gitignore b/.gitignore index d3aa2de..94824ed 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ build/ bin/ tmp/ -test/assets/*.png -test/assets/*.jpg -test/assets/*_result.* + +# dora build working directories +git/ +out/ diff --git a/AGENTS.md b/AGENTS.md index c4f26b4..1c14d96 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -41,7 +41,9 @@ Camera behavior is configured entirely through environment variables (see README - **`src/main.cc`** — Dora event loop. Parses `ZED_PC_MODE` (`FULL`/`HALF`) once before the loop. On any input event (intended to be driven by a tick timer; the id is not inspected): calls `camera.grab()`, then sends `image_bgr` and `depth`, plus `point_cloud` when `ZED_POINT_CLOUD` is on and `camera_info` on the first frame of a camera session (`camera_info_pending`, re-armed on reconnection). `pack_depth` copies the depth map into a tightly packed `[H, W]` float buffer (sl::Mat rows can be padded) and clears `inf`/`nan` to `0`. `set_intrinsics` and `set_frame_stamps` stamp every output of a frame with the same intrinsics, `frame_id` and the two timestamps (`device_timestamp_ns`, `host_timestamp_ns`; both Unix-epoch nanoseconds). Point cloud output varies by mode: `FULL` sends the raw `sl::Mat` pointer (no copy); `HALF` downsamples by 2 in each dimension. Recovery is a state machine over ticks rather than a blocking retry: the camera is opened from inside the loop, `begin_recovery` closes (first attempt) or resets (later ones) and sets a backoff deadline, and `camera_state` is published on each transition. Nothing sleeps, so `Stop` is honoured while the camera is down. - **`DoraTargets.cmake`** — Handles building or fetching dora-rs and generating the CXX bridge files. Two paths: local (`DORA_ROOT_DIR`) or git clone. Produces `dora_node_api_cxx` library and the `node_bridge.cc` glue. - **`scripts/build.sh`** — Configure/compile/install in one shell-free-callable step. Referenced by `dataflow.yml`, the `Dockerfile` and the README. -- **`dataflow.yml`** — Example dora dataflow definition using the `git:` source format, pinned with `tag:` — dora 1.0 pins a source with the mutually exclusive `tag:`, `rev:` or `branch:`. +- **`dora-node.yml`** — Node manifest (`apiVersion: 1`): typed input/output contract, entrypoint, build command. `dora validate` injects these types into any dataflow that reaches the node by `path:`; a `git:`-sourced node does **not** pick it up, which is why `dataflow.yml` builds from the checkout. Type URNs name the element type (`std/core/v1/UInt8`, `.../Float32`, `.../String`) because every payload is a raw buffer described by its metadata, not an Arrow struct. +- **`dataflow.yml`** — Example dora dataflow, built from this checkout with `path:` + `build:` and `strict_types: true`. A consumer wires the node from a tag instead (see README) — dora 1.0 pins a source with the mutually exclusive `tag:`, `rev:` or `branch:`. +- **`.gitea/workflows/validate.yml`** — CI gate running `dora validate dataflow.yml` on push and pull request. Needs no camera and no ZED SDK. - **`Dockerfile`** — Ubuntu 24.04 x86-64 build environment (CUDA 12.8, ZED SDK 5.2, Rust). No ARM64/Jetson image; build natively there. No camera needed; run the compile with `--gpus all`. Sets `NVIDIA_DRIVER_CAPABILITIES=compute,utility,video` — the `video` capability is what makes the container runtime mount NVDEC/NVENC, which `libsl_zed.so` links against. Rust lives at `/usr/local/{rustup,cargo}` rather than `/root` so the build can run as the invoking user. `docker build -t dora-zed-build . && docker run --rm --gpus all --user "$(id -u):$(id -g)" -v "$PWD:/src" -v dora-zed-cargo:/usr/local/cargo/registry dora-zed-build`. ## Key APIs diff --git a/README.md b/README.md index b634abe..62facbe 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,33 @@ nodes: - camera_state ``` -See [`dataflow.yml`](dataflow.yml) for a complete runnable example with a camera display node. +See [`dataflow.yml`](dataflow.yml) for a complete runnable example with a camera display +node. That file builds the node from the checkout (`path:` + `build:`) rather than from a +tag, which is what lets `dora validate` pick up the manifest described below. + +### Node manifest + +[`dora-node.yml`](dora-node.yml) declares the node's inputs and outputs with type URNs. +No source package is shared between node repositories, so it is the only contract a +consumer has, and it is what makes a wiring mistake fail at build time instead of at +runtime: + +``` +$ dora validate dataflow.yml + node "zed-camera": injected 6 contract type(s) from .../dora-node.yml +All type annotations OK. +``` + +Types are `std/core/v1/UInt8` for `image_bgr`, `std/core/v1/Float32` for `depth`, +`camera_info` and `point_cloud`, and `std/core/v1/String` for `camera_state`; every +payload is a raw buffer whose shape and dtype are in the metadata, so the URN names the +element type rather than a struct. The `tick` input is `std/core/v1/UInt64`, matching how +dora types timer inputs. + +`strict_types: true` in `dataflow.yml` makes a type warning a non-zero exit. +[`.gitea/workflows/validate.yml`](.gitea/workflows/validate.yml) runs +`dora validate dataflow.yml` on every push and pull request; it needs no camera and no +ZED SDK, since validation never runs the node. ## Dependencies diff --git a/dataflow.yml b/dataflow.yml index f03c9db..28b6a9f 100644 --- a/dataflow.yml +++ b/dataflow.yml @@ -1,9 +1,12 @@ +# The repository's own example: builds and runs the node from this checkout, so +# `dora validate` picks up the typed contract in dora-node.yml next to it. +# A consumer wires the node from a tag instead — see README.md. +strict_types: true + nodes: - id: zed-camera - git: ssh://git@gitea.liria.cl:5001/liria/dora-zed.git - tag: v0.2.0 - build: scripts/build.sh path: bin/dora_zed + build: scripts/build.sh inputs: tick: dora/timer/millis/100 outputs: diff --git a/dora-node.yml b/dora-node.yml new file mode 100644 index 0000000..336e30a --- /dev/null +++ b/dora-node.yml @@ -0,0 +1,39 @@ +# Node manifest: the typed contract a consumer wires against. +# +# No source package is shared between node repositories, so this file and its +# type URNs are the only contract a consumer has. `dora validate` checks a +# dataflow against it without running the node, so CI needs no camera and no +# ZED SDK. +apiVersion: 1 +name: dora-zed +namespace: liria +description: ZED stereo camera source publishing a BGR image, a depth map, intrinsics and an optional point cloud +categories: [sensor] +keywords: [zed, stereolabs, camera, depth, stereo, jetson] + +runtime: cpp +entrypoint: bin/dora_zed +build: scripts/build.sh + +inputs: + tick: + type: std/core/v1/UInt64 + required: true + description: Each tick triggers one camera grab. Connect to dora/timer/millis/N. + +outputs: + image_bgr: + type: std/core/v1/UInt8 + description: BGR image bytes, [H, W, 3]. Shape and encoding are in the metadata. + depth: + type: std/core/v1/Float32 + description: Per-pixel depth in millimetres, [H, W]. Missing depth is 0, never inf or nan. + camera_info: + type: std/core/v1/Float32 + description: Intrinsics [fx, fy, cx, cy], distortion in the metadata. Published once per camera session. + point_cloud: + type: std/core/v1/Float32 + description: XYZRGBA point cloud. Published only when ZED_POINT_CLOUD is on. + camera_state: + type: std/core/v1/String + description: One of opening, streaming, recovering. Published on every change.