This executable recipe connects a ROS sensor_msgs/Image depth topic to a non-actuating obstacle observation. It provides a maintained launch/configuration example, a synthetic input fixture and a runnable pipeline check. It does not configure a physical camera, navigation controller, costmap or velocity output.
This page needs a source checkout: the recipe is in demo/depth_camera_recipe/, which is not in the install. Run the commands below from the repository root.
The input contract is 16UC1 in millimetres or 32FC1 in metres, a nonzero source header stamp in the node's ROS clock domain, and a nonempty frame ID. The observer publishes std_msgs/String containing flightops.depth-observation/0 JSON:
near: every depth pixel is finite/positive, evidence is fresh, and the minimum image depth is at or below the threshold (default 0.75 m).clear: every pixel is finite/positive and fresh, and the minimum is above that threshold. This means only that this image meets the threshold; it does not mean traversable space, safe motion or an absence of obstacles outside the image.unavailable: missing, stale, future, malformed, unsupported or invalid data.
Outputs include source stamp/frame, source age, receive age, minimum depth and valid/invalid pixel counts. Zero, negative, NaN and infinite values are missing measurements. Even one invalid pixel conservatively makes the observation unavailable. This strict initial policy is deliberately visible; typical camera images containing invalid regions may need a separately reviewed ROI/coverage policy. Do not silently change invalid values to “far.”
Build and run
From the checkout in a sourced ROS 2 environment with rclpy, sensor_msgs, std_msgs, launch and colcon installed:
colcon build --base-paths demo/depth_camera_recipe \
--build-base /tmp/depth-recipe-build --install-base /tmp/depth-recipe-install
source /tmp/depth-recipe-install/setup.bash
ros2 launch depth_camera_recipe depth_observer.launch.py \
depth_topic:=/camera/depth/image_raw observation_topic:=/depth_observationThe launch file loads config/depth_observer.yaml. Edit its startup threshold and age policy, rebuild, then rerun the same checks; parameters are read-only at runtime. The subscriber uses sensor-data QoS (best effort, volatile). A live camera must satisfy the encoding, units, clock and QoS contracts; device model support and calibration are not inferred from its topic name. The conventional package is intended for ROS 2 Humble/Jazzy; only executed acceptance establishes support on a particular runtime.
Synthetic demonstration, in another sourced terminal with the same ROS domain:
PYTHONPATH=demo/depth_camera_recipe python3 -m depth_camera_recipe.fixture \
--case near --encoding 16UC1 --seconds 5
ros2 topic echo /depth_observationRun echo in parallel to see near; after the fixture stops, the observer emits unavailable with stale_image. Try --case clear --encoding 32FC1, invalid, malformed and stale. These fabricated pixels demonstrate the pipeline, not physical obstacle perception.
For an existing recording containing a supported depth topic, start the observer with use_sim_time:=true, then play the bag with ROS clock publication:
ros2 launch depth_camera_recipe depth_observer.launch.py \
depth_topic:=/recorded/depth/image use_sim_time:=true
# Separate terminal, same sourced environment and ROS domain:
ros2 bag play /path/to/depth-bag --clockInspect the bag's actual topic/type/encoding and header clock first. Playback time and header time must agree; an old wall-clock stamp with a wall-clock observer is stale evidence. Playing a bag does not verify a device driver or a simulated camera. No recorded camera dataset or Gazebo camera compatibility is claimed by the synthetic fixture. The observer never synthesizes camera calibration or TF.
Checks and development task
Pure decoder tests (no ROS required):
python3 -m pytest integrations/tests/test_depth_camera_recipe.py -qThe live check starts the actual observer process, publishes uniquely named synthetic inputs, verifies each result and stops its owned process. Use an isolated ROS domain with no hardware:
python3 demo/depth_camera_recipe/live_check.py --ros-domain-id 174 \
> /tmp/depth-recipe-live.jsonIf using a virtual environment, retain the system ROS Python dependencies. For example, on the tested Ubuntu/ROS container setup prepend /usr/lib/python3/dist-packages to PYTHONPATH after sourcing ROS. A missing rclpy/numpy dependency is a runtime setup failure, not a passing recipe check.
To test the installed package and documented launch file instead of invoking the source Python module, build it as above, source its installation, then run the same acceptance cases with --installed-launch:
source /tmp/depth-recipe-install/setup.bash
python3 demo/depth_camera_recipe/live_check.py --ros-domain-id 175 --installed-launch \
> /tmp/depth-recipe-installed-live.jsonThe helper passes unique input/output topics to ros2 launch, checks the same near/clear/unavailable transitions, and shuts down its owned launch process group. Its JSON retains the execution mode and exact command. The installed code must come from the intended build; do not prepend the source recipe directory to PYTHONPATH for this mode. A successful source-module trial alone does not verify installation or launch-file wiring.
This tests missing input, near 16-bit input, clear float input, invalid values, malformed row sizes, old stamps and transition to unavailable after publishing stops. The JSON is measured pipeline evidence; a nonzero exit means the check did not establish its acceptance criteria. Offline tests separately cover big/little endianness, row padding, dimension/byte bounds and non-finite values.
Attach the check to a workspace task, replacing /absolute/flightops with this checkout. Paths must identify the same project when creating and opening the task.
flightops-tui tasks create /tmp/depth-task.json --project /absolute/flightops \
--title 'Integrate supported depth obstacle input' \
--next-step 'Prove synthetic near, clear, invalid and stale behavior'Plan the experiment before running the live check. Create a variables file:
{"mode":"synthetic_ros_images","threshold_m":0.75,"max_age_s":0.5,"ros_domain_id":174,"reset":"new observer process and unique topics"}Save it as /tmp/depth-variables.json, then:
flightops-tui experiments plan /tmp/depth-task.json \
--hypothesis 'The depth consumer preserves near/clear and refuses invalid or stale input' \
--prediction 'Fresh 0.25 m is near, fresh 2 m is clear; missing, invalid and stale are unavailable' \
--test 'python3 demo/depth_camera_recipe/live_check.py --ros-domain-id 174' \
--criteria-identity depth-recipe-synthetic-v1 --variables /tmp/depth-variables.jsonRetain the returned experiment ID. Run the check directly, saving stdout to /tmp/depth-recipe-live.json, or configure the workspace runner:
flightops-tui workspace --project /absolute/flightops --task /tmp/depth-task.json \
--session-dir /absolute/flightops/.launchcommit/depth-session \
--check-timeout 40 --check-command python3 demo/depth_camera_recipe/live_check.py \
--ros-domain-id 174In Results press g to execute the actual configured check. Its logs, command, source identity and outcome are retained and linked to the task. For an existing ROS container, supply --check-container NAME --check-workspace /work before --check-command, using the workspace's verified bind mount. Source ROS in the check environment; the runner does not install dependencies or launch a device.
After reviewing a successful check, finish the previously planned experiment:
flightops-tui experiments finish /tmp/depth-task.json EXPERIMENT_ID \
--outcome supported --artifact /tmp/depth-recipe-live.json \
--conclusion 'The retained synthetic trial satisfied near, clear, invalid, stale and cleanup criteria'
flightops-tui experiments show /tmp/depth-task.jsonUse the actual saved configured-check JSON artifact instead if the workspace ran the check. Record failed, interrupted, inconclusive or refuted when that is what occurred; do not use supported simply because the command was started.
Task notes and successful process exits do not upgrade synthetic evidence to physical-device acceptance. For a new driver, first record its actual encoding, units, frame, timestamp convention, QoS and invalid-pixel behavior; then preserve near/far and dropout examples before connecting any downstream application.
Bounds and limitations
Each image is limited to dimensions 1–4096, at most 1,048,576 pixels and 16 MiB of row data. Byte count must exactly match height * step; padding is permitted and ignored, declared endian order is honored. Decoding reads buffer views rather than copying padded image data. DDS receives/deserializes the message before these application checks; these are decoder bounds, not a network memory firewall.
A steady-clock timer checks both source ROS-stamp age and monotonic time since the last image. A paused /clock therefore cannot leave an old clear observation indefinitely fresh. More than 50 ms in the future is unavailable. Calibration, camera range/accuracy, occlusion and physical geometry remain unmeasured. There is no depth projection, TF lookup, obstacle localization or actuation in this recipe.