From db465947c72bf4d70dc2c7e46ce940a2f74e66d5 Mon Sep 17 00:00:00 2001 From: Isaac Yang Date: Fri, 17 Jul 2026 13:53:05 +0800 Subject: [PATCH 01/10] Add Jetson platform to CE OEM camera tests Add jetson as a fourth platform to the CE OEM camera framework, alongside genio, imx and rz, following the "Contributing: Adding New Platforms" process in units/camera/README.md. Captures use the sensor's native format with no encoder, mirroring camera_genio.py: nvarguscamerasrc -> nvvidconv -> filesink for raw NV12, and nvargus_nvraw --format nvraw for native Bayer raw. Dropping the H265/JPEG encoders removes the NVENC dependency, so the Orin Nano, which has no NVENC, can record video like any other board, and one imx219 scenario file serves both the Orin NX and the Orin Nano. The Argus mode index is declared per resolution in the scenario JSON rather than discovered at runtime. It is load-bearing: the IMX274 exposes four Argus modes across only two resolutions, so modes 1 and 3 (both 1920x1080@60, differing only in gain/exposure range) are indistinguishable on every field the framework keys on. Without an explicit mode, mode 3 is unreachable and the two render the same job id. The mode therefore also enters both job-id templates and the artifact pattern, behind a jinja2 conditional so platforms that declare no mode render exactly as before. v4l2_device_name carries the Argus source_index ("0"/"1"), the identifier that nvargus_nvraw --c and nvarguscamerasrc sensor-id= actually consume. The Tegra v4l2 names embed an i2c bus number and a device-tree VI channel, neither of which tracks the sensor index: the AGX Orin's two sensors sit on VI channels 0 and 2, and the Orin NX's only sensor sits on VI channel 1. Sensor modes are taken from the DUTs' own GST_ARGUS mode tables and cross-checked against vendor documentation. Coverage per device: AGX Orin 6 to 24 jobs, Orin NX 3 to 15, Orin Nano 2 to 15. Purely additive: units/Jetson/camera_*.pxu and bin/jetson_camera_sensors.py are untouched, and no test plan is modified. The new jobs run only on a DUT whose manifest sets has_vendor_specific_mipi_camera. Verified: all 11 Genio/NXP/RZ scenario files generate byte-identical resources with no mode key; Jetson generates 24/15 records each carrying its mode; the four IMX274 records render 4 distinct job ids (2 without the mode suffix); Genio job ids and command lines are byte-identical. --- .../bin/camera_jetson.py | 413 ++++++++++++++++++ .../bin/camera_test.py | 30 +- .../bin/camera_utils.py | 11 + .../Test_Scenario_and_Test_Setup.md | 98 +++++ ...tson_mipi_camera_test_scenario_imx219.json | 48 ++ ...mipi_camera_test_scenario_imx274_dual.json | 84 ++++ .../units/camera/README.md | 12 + .../units/camera/jobs.pxu | 8 +- 8 files changed, 698 insertions(+), 6 deletions(-) create mode 100755 contrib/checkbox-ce-oem/checkbox-provider-ce-oem/bin/camera_jetson.py create mode 100644 contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/Test_Scenario_and_Test_Setup.md create mode 100644 contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/jetson_mipi_camera_test_scenario_imx219.json create mode 100644 contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/jetson_mipi_camera_test_scenario_imx274_dual.json diff --git a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/bin/camera_jetson.py b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/bin/camera_jetson.py new file mode 100755 index 0000000000..20e7cb6337 --- /dev/null +++ b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/bin/camera_jetson.py @@ -0,0 +1,413 @@ +#!/usr/bin/env python3 +# This file is part of Checkbox. +# +# Copyright 2025 Canonical Ltd. +# Written by: +# Isaac Yang +# +# Checkbox is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3, +# as published by the Free Software Foundation. +# +# Checkbox is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Checkbox. If not, see . +import logging +import os +import shutil + +from enum import Enum +from typing import Optional, Type +from camera_utils import ( + CameraInterface, + execute_command, + SupportedMethods, + GST_LAUNCH_BIN, + CameraError, + CameraConfigurationError, + log_and_raise_error, +) + +logger = logging.getLogger(__name__) + +NVARGUS_NVRAW_BIN = shutil.which( + os.getenv("NVARGUS_NVRAW_BIN", "nvargus_nvraw") +) + +# GStreamer plugin search paths. The NVIDIA plugins (nvarguscamerasrc, +# nvvidconv) are not on the default search path, so every Jetson gstreamer job +# has to point GStreamer at them - the deb ones included. Values copied +# verbatim from units/Jetson/camera_job.pxu:74 (deb) and :152-153 (the snap +# video job, which is the working snap pair: it covers both the checkbox +# runtime's core elements and the NVIDIA dir). +DEB_GST_PLUGIN_PATH = "/usr/lib/aarch64-linux-gnu/gstreamer-1.0/" +SNAP_GST_PLUGIN_SYSTEM_PATHS = ( + "checkbox-runtime/usr/lib/aarch64-linux-gnu/gstreamer-1.0", + "usr/lib/aarch64-linux-gnu/gstreamer-1.0", +) +SNAP_GST_PLUGIN_SCANNER = ( + "checkbox-runtime/usr/lib/aarch64-linux-gnu/gstreamer1.0/" + "gstreamer-1.0/gst-plugin-scanner" +) + +# Timeouts in seconds for execute_command(). Mandatory, not a nice-to-have: +# wedged Argus captures have happened, which is why every current Jetson job is +# wrapped in jetson_timeout_wrapper.sh. This module does not use that wrapper, +# so the bound has to come from here. +# +# The still-image values are today's, unchanged: +# units/Jetson/camera_job.pxu:30 and :52 use 10s for nvargus_nvraw, :76 and :99 +# use 30s for a gstreamer still. +NVARGUS_TIMEOUT = 10 +GST_IMAGE_TIMEOUT = 30 +# Video needs more than today's 30s (camera_job.pxu:127, :155): that 30s only +# had to cover an H265-encoded 1080p mp4 (~30 MB). Dropping the encoder makes +# this path write-bound instead. Worst case here is 300 frames of 3280x2464 +# NV12 (IMX219 mode 0 @21fps) = ~3.5 GiB, which is ~14s of capture but several +# times that to land on disk. 180s tolerates a ~20 MB/s sustained write floor +# while still bounding a wedge, and stays under execute_command()'s 300s +# default so the bound is strictly tighter than doing nothing. +GST_VIDEO_TIMEOUT = 180 + + +class SupportedCamera(Enum): + """ + Supported camera modules on Jetson platforms. + + Each enum value corresponds to a concrete camera implementation class. + The string value matches the camera module identifier used in the system. + """ + + IMX274 = "imx274" # Sony IMX274 sensor (AGX Orin Developer Kit, x2) + IMX219 = "imx219" # Sony IMX219 sensor (Orin NX and Orin Nano, x1) + + def __str__(self): + return self.value + + +def jetson_camera_factory(camera_module: str) -> Type[CameraInterface]: + """ + Factory function to create camera handler instances. + + Args: + camera_module: String identifier of the camera module + + Returns: + Camera handler class that implements CameraInterface + + Raises: + CameraError: If camera_module is not supported + """ + # Map camera module strings to their handler classes + camera_handlers = { + str(cam): handler + for cam, handler in { + SupportedCamera.IMX274: Imx274Handler, + SupportedCamera.IMX219: Imx219Handler, + }.items() + } + + handler_class = camera_handlers.get(camera_module) + if not handler_class: + raise CameraError( + "Unsupported camera module: {}. " + "Supported modules are: {}".format( + camera_module, list(camera_handlers.keys()) + ) + ) + return handler_class + + +def gst_env_prefix() -> str: + """ + Build an 'env' prefix for the gst-launch command. + + execute_command() runs subprocess.run(shlex.split(cmd)) - there is no shell + and no env= parameter - so an 'export X=...; gst-launch ...' command string + is unrunnable and a literal "$SNAP" would never expand. /usr/bin/env + carries both the DISPLAY unset and the plugin variables with no framework + change, so the snap paths are built here in Python instead. + + Deb vs snap is decided by where GST_LAUNCH_BIN (already shutil.which() + resolved in camera_utils) lives. + + Returns: + The env prefix, without a trailing space. + + Raises: + CameraConfigurationError: If running from a snap with SNAP unset + """ + if not GST_LAUNCH_BIN or not GST_LAUNCH_BIN.startswith("/snap/"): + return "/usr/bin/env -u DISPLAY GST_PLUGIN_PATH={}".format( + DEB_GST_PLUGIN_PATH + ) + + snap = os.getenv("SNAP") + if not snap: + log_and_raise_error( + "'{}' is a snap binary but the SNAP environment variable is " + "unset, so the GStreamer plugin paths cannot be built.".format( + GST_LAUNCH_BIN + ), + CameraConfigurationError, + ) + + plugin_system_path = ":".join( + os.path.join(snap, path) for path in SNAP_GST_PLUGIN_SYSTEM_PATHS + ) + return ( + "/usr/bin/env -u DISPLAY GST_PLUGIN_SYSTEM_PATH={} " + "GST_PLUGIN_SCANNER={}".format( + plugin_system_path, + os.path.join(snap, SNAP_GST_PLUGIN_SCANNER), + ) + ) + + +class JetsonBaseCamera(CameraInterface): + """ + Base class for Jetson camera implementations. + + Every capture goes through NVIDIA's Argus stack, which owns the media graph + and addresses cameras by its own source index. There is therefore no + media-ctl topology to configure and no /dev/videoN to resolve, so this + class needs no __init__ of its own - CameraInterface's is enough. + """ + + def _get_artifact_path( + self, store_path: str, artifact_name: str, format: str + ) -> str: + """Get the appropriate file extension based on format.""" + suffix = ".nvraw" if format == "NVRAW" else ".yuv" + return os.path.join(store_path, artifact_name + suffix) + + def _get_sensor_id(self, v4l2_device_name: str) -> int: + """ + Get the Argus source index for the given camera. + + On Jetson, v4l2_device_name carries the Argus source_index as a string + ("0" / "1") rather than a v4l2 name - it is what 'nvargus_nvraw --c N' + and 'nvarguscamerasrc sensor-id=N' actually consume. + + Do not derive this from physical_interface, the VI channel or the i2c + bus number: the AGX Orin's two sensors sit on VI channels 0 and 2, and + the Orin NX's only sensor sits on VI channel 1, so neither tracks the + Argus source index. physical_interface (cam0 / cam1) is a silkscreen + label for the job id and carries no addressing meaning. + """ + try: + return int(v4l2_device_name) + except (TypeError, ValueError): + log_and_raise_error( + "Invalid v4l2_device_name '{}': on Jetson this field carries " + "the Argus source index (e.g. '0'), not a device name.".format( + v4l2_device_name + ), + CameraConfigurationError, + ) + + def _build_gstreamer_cmd( + self, + sensor_id: int, + width: int, + height: int, + format: str, + full_artifact_path: str, + count: Optional[int] = None, + framerate: Optional[int] = None, + mode: Optional[int] = None, + ) -> str: + """ + Build the GStreamer command. + + No encoder anywhere: the Argus ISP emits NV12 into NVMM (GPU) memory + and a file sink cannot consume NVMM buffers, so nvvidconv - the VIC + hardware block, not an encoder - is mandatory to write the raw frames + to disk. count/framerate decide filesink vs multifilesink. + """ + src_words = [ + "nvarguscamerasrc", + "num-buffers={}".format(count or 30), + "sensor-id={}".format(sensor_id), + ] + # Omitted when unset, which leaves Argus on its sensor-mode=-1 default + # and lets it auto-select. That is today's implicit behaviour. + if mode is not None: + src_words.append("sensor-mode={}".format(mode)) + + caps_words = [ + "video/x-raw(memory:NVMM)", + "width={}".format(width), + "height={}".format(height), + "format={}".format(format), + ] + if framerate is not None: + caps_words.append("framerate={}/1".format(framerate)) + + if count is not None: + sink = "filesink location={}".format(full_artifact_path) + else: + sink = "multifilesink location={} max-files=1".format( + full_artifact_path + ) + + # The caps are single-quoted so that the logged command can be pasted + # straight into a shell, where the '(memory:NVMM)' parens would + # otherwise be a syntax error. shlex.split() strips the quotes back off + # before execution, so argv is unaffected either way. + return "{} {} {} ! '{}' ! nvvidconv ! '{}' ! {}".format( + gst_env_prefix(), + GST_LAUNCH_BIN, + " ".join(src_words), + ",".join(caps_words), + "video/x-raw,format={}".format(format), + sink, + ) + + def _build_nvargus_cmd( + self, + sensor_id: int, + full_artifact_path: str, + mode: Optional[int] = None, + ) -> str: + """ + Build the nvargus_nvraw command. + + '--format nvraw' is the tool's own default and the sensor's native + pre-ISP Bayer raw; today's jobs pass '--format jpg', which is the + deviation being corrected. Resolution is not an argument here - the + Argus mode selects it. + """ + if not NVARGUS_NVRAW_BIN: + log_and_raise_error( + "Could not find the 'nvargus_nvraw' executable", + CameraConfigurationError, + ) + + words = [NVARGUS_NVRAW_BIN, "--c {}".format(sensor_id)] + if mode is not None: + words.append("--mode {}".format(mode)) + words.append("--format nvraw") + words.append("--file {}".format(full_artifact_path)) + + return " ".join(words) + + def capture_image( + self, + width: int, + height: int, + format: str, + store_path: str, + artifact_name: str, + method: str, + v4l2_device_name: str, + mode: Optional[int] = None, + ) -> None: + """Capture an image using the specified method.""" + full_artifact_path = self._get_artifact_path( + store_path, artifact_name, format + ) + logging.info("Capture image as {}".format(full_artifact_path)) + + sensor_id = self._get_sensor_id(v4l2_device_name) + + logger.info( + "Capture image from {} sensor-id {} with {}".format( + self._camera, sensor_id, method + ) + ) + + if method == SupportedMethods.GSTREAMER: + cmd = self._build_gstreamer_cmd( + sensor_id, + width, + height, + format, + full_artifact_path, + mode=mode, + ) + timeout = GST_IMAGE_TIMEOUT + elif method == SupportedMethods.NVARGUS_NVRAW: + cmd = self._build_nvargus_cmd( + sensor_id, full_artifact_path, mode=mode + ) + timeout = NVARGUS_TIMEOUT + else: + msg = "No suitable method such as '{}' or '{}' be provided".format( + SupportedMethods.GSTREAMER, SupportedMethods.NVARGUS_NVRAW + ) + log_and_raise_error(msg, CameraConfigurationError) + + logger.info("Executing command:\n{}".format(cmd)) + output = execute_command(cmd=cmd, timeout=timeout) + logger.info("Output:\n{}".format(output)) + + def record_video( + self, + width: int, + height: int, + framerate: int, + format: str, + count: int, + store_path: str, + artifact_name: str, + method: str, + v4l2_device_name: str, + mode: Optional[int] = None, + ) -> None: + """Record a video using the specified method.""" + full_artifact_path = self._get_artifact_path( + store_path, artifact_name, "YUV" + ) + logging.info("Record a video as {}".format(full_artifact_path)) + + sensor_id = self._get_sensor_id(v4l2_device_name) + + logger.info( + "Record video from {} sensor-id {} with {}".format( + self._camera, sensor_id, method + ) + ) + + # nvargus_nvraw is a still-image tool, so gstreamer is the only method + # able to record. + if method == SupportedMethods.GSTREAMER: + cmd = self._build_gstreamer_cmd( + sensor_id, + width, + height, + format, + full_artifact_path, + count=count, + framerate=framerate, + mode=mode, + ) + else: + msg = "No suitable method such as '{}' be provided".format( + SupportedMethods.GSTREAMER + ) + log_and_raise_error(msg, CameraConfigurationError) + + logger.info("Executing command:\n {}".format(cmd)) + output = execute_command(cmd=cmd, timeout=GST_VIDEO_TIMEOUT) + logger.info("Output:\n{}".format(output)) + + +class Imx274Handler(JetsonBaseCamera): + """Handler for the Sony IMX274 camera.""" + + def __init__(self, v4l2_devices: str): + super().__init__(v4l2_devices) + self._camera = SupportedCamera.IMX274 + + +class Imx219Handler(JetsonBaseCamera): + """Handler for the Sony IMX219 camera.""" + + def __init__(self, v4l2_devices: str): + super().__init__(v4l2_devices) + self._camera = SupportedCamera.IMX219 diff --git a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/bin/camera_test.py b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/bin/camera_test.py index 50d365e20c..6beae43669 100755 --- a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/bin/camera_test.py +++ b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/bin/camera_test.py @@ -70,10 +70,18 @@ def _generate_artifact_pattern( args.height, ) + # Keep sensor modes sharing a resolution in their own artifact folder, + # mirroring the mode suffix of the job id. + mode_pattern = "" + if args.mode is not None: + mode_pattern = "_mode{}".format(args.mode) + if scenario_name == CameraScenarios.CAPTURE_IMAGE: - return base_pattern + "_{}".format(args.format) + return base_pattern + "{}_{}".format(mode_pattern, args.format) elif scenario_name == CameraScenarios.RECORD_VIDEO: - return base_pattern + "@{}fps_{}".format(args.framerate, args.format) + return base_pattern + "@{}fps{}_{}".format( + args.framerate, mode_pattern, args.format + ) else: raise ValueError("Unsupported scenario: {}".format(scenario_name)) @@ -116,6 +124,10 @@ def _execute_capture_image_scenario( artifact_store_path: Path to store artifacts artifact_name: Base name for artifacts """ + # Only pass the sensor mode when the scenario declares one: the platforms + # that don't use it don't accept it either. + extra = {"mode": args.mode} if args.mode is not None else {} + iteration = 5 # Capture multiple images for i in range(1, iteration + 1): logger.info("\n\n===== Iteration {} =====\n".format(i)) @@ -127,6 +139,7 @@ def _execute_capture_image_scenario( store_path=artifact_store_path, artifact_name=artifact_name + "_{}".format(i), v4l2_device_name=args.v4l2_device_name, + **extra ) @@ -145,6 +158,10 @@ def _execute_record_video_scenario( artifact_store_path: Path to store artifacts artifact_name: Base name for artifacts """ + # Only pass the sensor mode when the scenario declares one: the platforms + # that don't use it don't accept it either. + extra = {"mode": args.mode} if args.mode is not None else {} + handler_instance.record_video( width=args.width, height=args.height, @@ -155,6 +172,7 @@ def _execute_record_video_scenario( artifact_name=artifact_name, method=args.method, v4l2_device_name=args.v4l2_device_name, + **extra ) @@ -326,6 +344,14 @@ def register_arguments() -> argparse.Namespace: help=("fps of camera"), ) + parser_testing.add_argument( + "-md", + "--mode", + type=int, + default=None, + help=("Optional: sensor mode of camera"), + ) + args = parser.parse_args() return args diff --git a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/bin/camera_utils.py b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/bin/camera_utils.py index fa0a6dd109..33aad835c6 100755 --- a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/bin/camera_utils.py +++ b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/bin/camera_utils.py @@ -85,6 +85,9 @@ class SupportedMethods(Enum): # gstreamer can be used to generate the mp4, jpeg and other common files. # It's more like the real user scenario. GSTREAMER = "gstreamer" + # nvargus_nvraw is the Argus CLI tool on NVIDIA Jetson platforms. + # It captures the native Bayer raw data before the ISP processes it. + NVARGUS_NVRAW = "nvargus_nvraw" def __str__(self): return self.value @@ -188,6 +191,10 @@ def camera_factory(platform: str, camera_module: str) -> Type[CameraInterface]: from camera_rz import rz_camera_factory return rz_camera_factory(platform, camera_module) + elif "jetson" in platform: + from camera_jetson import jetson_camera_factory + + return jetson_camera_factory(camera_module=camera_module) else: log_and_raise_error( "Cannot find the '{}' platform".format(platform), @@ -1100,6 +1107,10 @@ def _process_scenario_items( if scenario_type == "record_video": resource_item["fps"] = resolution["fps"] + # Add the sensor mode only when the resolution declares it + if "mode" in resolution: + resource_item["mode"] = resolution["mode"] + self._resource_items.append(resource_item) except Exception as e: diff --git a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/Test_Scenario_and_Test_Setup.md b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/Test_Scenario_and_Test_Setup.md new file mode 100644 index 0000000000..b0ecab2b93 --- /dev/null +++ b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/Test_Scenario_and_Test_Setup.md @@ -0,0 +1,98 @@ +# Test Scenario and Test Setup + +This document provides test scenarios for the NVIDIA Jetson MIPI camera +configurations. + +> **Note:** Jetson cameras are driven through the Argus stack. Argus owns the +> media graph, so no test setup (media-ctl) files are required — only test +> scenario files. + +> **Note:** All captures use the camera's native format — NV12 for the Argus +> gstreamer path, Bayer raw for nvargus_nvraw. No encoder is used, so no test +> depends on NVENC. + +> **Note:** every item declares an explicit Argus `mode` index per resolution. +> nvargus_nvraw passes it as `--mode`; gstreamer passes it as +> `nvarguscamerasrc sensor-mode=`. This is what makes sensor modes that share a +> resolution and frame rate (IMX274 modes 1 and 3) separately testable. + +> **Note:** `v4l2_device_name` holds the Argus `source_index` (0 / 1), not a +> `/dev/video` name. Argus addresses sensors by index; the Tegra v4l2 names embed +> an i2c bus number and a device-tree VI channel, neither of which tracks the +> sensor index (the AGX Orin's two sensors are on VI channels 0 and 2). + +## Overview + +The test scenario files are located in the +`contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup` +directory. + +## Argus Sensor Configurations + +Jetson configurations only require test scenario files (no test setup needed). + +### IMX274 Dual + +**Test Scenario:** + +- Hardware: Leopard Imaging LI-JETSON-IMX274-DUAL-090H, 2x Sony IMX274 +- Board: Jetson AGX Orin Developer Kit +- Documentation: [LI-JETSON-IMX274-DUAL-090H product page](https://leopardimaging.com/product/robotics-cameras/cis-2-mipi-modules/li-jetson-imx274-dual/) +- Documentation: [LI-JETSON-IMX274-DUAL-090H datasheet](https://leopardimaging.com/wp-content/uploads/2026/01/LI-JETSON-IMX274-DUAL-090H_Datasheet.pdf) +- Configuration: [`jetson_mipi_camera_test_scenario_imx274_dual.json`](jetson_mipi_camera_test_scenario_imx274_dual.json) + +| Argus mode | Resolution | FPS | +| --- | --- | --- | +| 0 | 3840x2160 | 60 | +| 1 | 1920x1080 | 60 | +| 2 | 3840x2160 | 30 | +| 3 | 1920x1080 | 60 | + +> **Note:** there is no 720p mode on this sensor. Modes 1 and 3 share +> resolution+fps (1920x1080@60) and differ only in gain/exposure range, so every +> job pins its mode explicitly (`--mode` / `sensor-mode=`) and the job id carries +> a `_modeN` suffix. All four modes are tested on all three capture paths (both +> sensors), for 24 jobs total. + +> **Note:** the Leopard Imaging datasheet's "Supported Platform" line lists only +> the Nvidia Holoscan Platform and omits Jetson AGX Orin. The vendor product +> page's compatibility table does confirm AGX Orin support (Orin NX / Orin Nano / +> Nano are marked N/A there, consistent with those boards carrying the IMX219 +> instead). + +### IMX219 + +**Test Scenario:** + +- Hardware: Arducam Nvidia Jetson native camera IMX219 +- Boards: Jetson Orin NX, Jetson Orin Nano (same sensor, same mode set) +- Documentation: [Arducam Nvidia Jetson native camera IMX219](https://docs.arducam.com/Nvidia-Jetson-Camera/Native-Camera/imx219/) (L4T 35.x table) +- Configuration: [`jetson_mipi_camera_test_scenario_imx219.json`](jetson_mipi_camera_test_scenario_imx219.json) + +| Argus mode | Resolution | FPS | +| --- | --- | --- | +| 0 | 3280x2464 | 21 | +| 1 | 3280x1848 | 28 | +| 2 | 1920x1080 | 30 | +| 3 | 1640x1232 | 30 | +| 4 | 1280x720 | 60 | + +> **Note:** this is the Arducam L4T 35.x mode table, byte-identical on both +> boards. The Jetson Orin Nano has no NVENC hardware encoder, but because every +> capture here uses the sensor's native format with no encoder, NVENC is never +> needed — the Orin Nano supports video recording on all five modes like any +> other board. + +## Capture Methods + +- `gstreamer` — `nvarguscamerasrc sensor-mode=M` → NVMM NV12 → `nvvidconv` → + `filesink` (raw, native NV12, no encoder) +- `nvargus_nvraw` — Argus CLI tool, native Bayer raw (`.nvraw`), explicit + `--mode M` + +## Quick Reference + +| Configuration | Board(s) | Test Scenario | Test Setup | Cameras | +| --- | --- | --- | --- | --- | +| IMX274 Dual | Jetson AGX Orin Developer Kit | ✅ Required | ❌ Not needed | 2 | +| IMX219 | Jetson Orin NX, Jetson Orin Nano | ✅ Required | ❌ Not needed | 1 | diff --git a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/jetson_mipi_camera_test_scenario_imx219.json b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/jetson_mipi_camera_test_scenario_imx219.json new file mode 100644 index 0000000000..bde5f5d8ca --- /dev/null +++ b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/jetson_mipi_camera_test_scenario_imx219.json @@ -0,0 +1,48 @@ +{ + "capture_image": [ + { + "camera": "imx219", + "method": "nvargus_nvraw", + "physical_interface": "cam0", + "v4l2_device_name": "0", + "formats": ["NVRAW"], + "resolutions": [ + {"width": 3280, "height": 2464, "mode": 0}, + {"width": 3280, "height": 1848, "mode": 1}, + {"width": 1920, "height": 1080, "mode": 2}, + {"width": 1640, "height": 1232, "mode": 3}, + {"width": 1280, "height": 720, "mode": 4} + ] + }, + { + "camera": "imx219", + "method": "gstreamer", + "physical_interface": "cam0", + "v4l2_device_name": "0", + "formats": ["NV12"], + "resolutions": [ + {"width": 3280, "height": 2464, "mode": 0}, + {"width": 3280, "height": 1848, "mode": 1}, + {"width": 1920, "height": 1080, "mode": 2}, + {"width": 1640, "height": 1232, "mode": 3}, + {"width": 1280, "height": 720, "mode": 4} + ] + } + ], + "record_video": [ + { + "camera": "imx219", + "method": "gstreamer", + "physical_interface": "cam0", + "v4l2_device_name": "0", + "formats": ["NV12"], + "resolutions": [ + {"width": 3280, "height": 2464, "fps": 21, "mode": 0}, + {"width": 3280, "height": 1848, "fps": 28, "mode": 1}, + {"width": 1920, "height": 1080, "fps": 30, "mode": 2}, + {"width": 1640, "height": 1232, "fps": 30, "mode": 3}, + {"width": 1280, "height": 720, "fps": 60, "mode": 4} + ] + } + ] +} diff --git a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/jetson_mipi_camera_test_scenario_imx274_dual.json b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/jetson_mipi_camera_test_scenario_imx274_dual.json new file mode 100644 index 0000000000..1352b0a10d --- /dev/null +++ b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/jetson_mipi_camera_test_scenario_imx274_dual.json @@ -0,0 +1,84 @@ +{ + "capture_image": [ + { + "camera": "imx274", + "method": "nvargus_nvraw", + "physical_interface": "cam0", + "v4l2_device_name": "0", + "formats": ["NVRAW"], + "resolutions": [ + {"width": 3840, "height": 2160, "mode": 0}, + {"width": 1920, "height": 1080, "mode": 1}, + {"width": 3840, "height": 2160, "mode": 2}, + {"width": 1920, "height": 1080, "mode": 3} + ] + }, + { + "camera": "imx274", + "method": "nvargus_nvraw", + "physical_interface": "cam1", + "v4l2_device_name": "1", + "formats": ["NVRAW"], + "resolutions": [ + {"width": 3840, "height": 2160, "mode": 0}, + {"width": 1920, "height": 1080, "mode": 1}, + {"width": 3840, "height": 2160, "mode": 2}, + {"width": 1920, "height": 1080, "mode": 3} + ] + }, + { + "camera": "imx274", + "method": "gstreamer", + "physical_interface": "cam0", + "v4l2_device_name": "0", + "formats": ["NV12"], + "resolutions": [ + {"width": 3840, "height": 2160, "mode": 0}, + {"width": 1920, "height": 1080, "mode": 1}, + {"width": 3840, "height": 2160, "mode": 2}, + {"width": 1920, "height": 1080, "mode": 3} + ] + }, + { + "camera": "imx274", + "method": "gstreamer", + "physical_interface": "cam1", + "v4l2_device_name": "1", + "formats": ["NV12"], + "resolutions": [ + {"width": 3840, "height": 2160, "mode": 0}, + {"width": 1920, "height": 1080, "mode": 1}, + {"width": 3840, "height": 2160, "mode": 2}, + {"width": 1920, "height": 1080, "mode": 3} + ] + } + ], + "record_video": [ + { + "camera": "imx274", + "method": "gstreamer", + "physical_interface": "cam0", + "v4l2_device_name": "0", + "formats": ["NV12"], + "resolutions": [ + {"width": 3840, "height": 2160, "fps": 60, "mode": 0}, + {"width": 1920, "height": 1080, "fps": 60, "mode": 1}, + {"width": 3840, "height": 2160, "fps": 30, "mode": 2}, + {"width": 1920, "height": 1080, "fps": 60, "mode": 3} + ] + }, + { + "camera": "imx274", + "method": "gstreamer", + "physical_interface": "cam1", + "v4l2_device_name": "1", + "formats": ["NV12"], + "resolutions": [ + {"width": 3840, "height": 2160, "fps": 60, "mode": 0}, + {"width": 1920, "height": 1080, "fps": 60, "mode": 1}, + {"width": 3840, "height": 2160, "fps": 30, "mode": 2}, + {"width": 1920, "height": 1080, "fps": 60, "mode": 3} + ] + } + ] +} diff --git a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/camera/README.md b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/camera/README.md index d400f7467d..a65026ce4b 100644 --- a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/camera/README.md +++ b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/camera/README.md @@ -407,6 +407,18 @@ For comprehensive details about RZ platform configurations, test scenarios, and **[RZ Test Scenario and Test Setup Documentation](../../data/RZ-MIPI-Camera-TestScenario-TestSetup/README.md)** +### Jetson Test Scenario and Test Setup Documentation + +For comprehensive details about Jetson platform configurations, test scenarios, and test setups, refer to: + +**[Jetson Test Scenario and Test Setup Documentation](../../data/Jetson-MIPI-Camera-TestScenario-TestSetup/Test_Scenario_and_Test_Setup.md)** + +This documentation includes: + +- Argus-based sensor configurations (no test setup / media-ctl files needed — scenario files only) +- IMX274 Dual (AGX Orin) and IMX219 (Orin NX + Orin Nano) configurations +- Quick reference table for all supported configurations + ## Troubleshooting diff --git a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/camera/jobs.pxu b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/camera/jobs.pxu index 63a858720f..e3414426f5 100644 --- a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/camera/jobs.pxu +++ b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/camera/jobs.pxu @@ -25,7 +25,7 @@ template-unit: job template-id: ce-oem-mipi-camera/capture-image-scenario template-filter: mipi_camera_resource.scenario == "capture_image" -id: ce-oem-mipi-camera/capture-image_{{ camera }}_{{ physical_interface }}_{{ method }}_{{ width }}x{{ height }}_{{ format }} +id: ce-oem-mipi-camera/capture-image_{{ camera }}_{{ physical_interface }}_{{ method }}_{{ width }}x{{ height }}{% if mode is defined %}_mode{{ mode }}{% endif %}_{{ format }} _template-summary: MIPI Camera Captures Image _summary: {{ camera }} captures {{ width }}x{{ height }} {{ format }} images via {{ method }} _description: Test {{ camera }} sensor on the {{ physical_interface }} slot can capture {{ width }}x{{ height }} {{ format }} images via {{ method }} method @@ -41,7 +41,7 @@ requires: manifest.has_vendor_specific_mipi_camera == "True" environ: PLATFORM_NAME MIPI_CAMERA_SETUP_CONF_FILE_PATH command: - camera_test.py testing -sn {{ scenario }} -p "$PLATFORM_NAME" -c {{ camera }} -pi {{ physical_interface }} -m {{ method }} -wi {{ width }} -hi {{ height }} -f {{ format }} -vdn "{{ v4l2_device_name }}" -cscf "$MIPI_CAMERA_SETUP_CONF_FILE_PATH" + camera_test.py testing -sn {{ scenario }} -p "$PLATFORM_NAME" -c {{ camera }} -pi {{ physical_interface }} -m {{ method }} -wi {{ width }} -hi {{ height }} -f {{ format }} -vdn "{{ v4l2_device_name }}" -cscf "$MIPI_CAMERA_SETUP_CONF_FILE_PATH"{% if mode is defined %} -md {{ mode }}{% endif %} unit: template template-engine: jinja2 @@ -50,7 +50,7 @@ template-unit: job template-id: ce-oem-mipi-camera/record-video-scenario template-filter: mipi_camera_resource.scenario == "record_video" -id: ce-oem-mipi-camera/record-video_{{ camera }}_{{ physical_interface }}_{{ method }}_{{ width }}x{{ height }}@{{ fps }}fps_{{ format }} +id: ce-oem-mipi-camera/record-video_{{ camera }}_{{ physical_interface }}_{{ method }}_{{ width }}x{{ height }}@{{ fps }}fps{% if mode is defined %}_mode{{ mode }}{% endif %}_{{ format }} _template-summary: MIPI Camera Records Video _summary: {{ camera }} records a {{ width }}x{{ height }}@{{ fps }}fps {{ format }} video via {{ method }} _description: Test {{ camera }} sensor on the {{ physical_interface }} slot can record {{ width }}x{{ height }}@{{ fps }}fps {{ format }} video via {{ method }} method @@ -66,4 +66,4 @@ requires: manifest.has_vendor_specific_mipi_camera == "True" environ: PLATFORM_NAME MIPI_CAMERA_SETUP_CONF_FILE_PATH command: - camera_test.py testing -sn {{ scenario }} -p "$PLATFORM_NAME" -c {{ camera }} -pi {{ physical_interface }} -m {{ method }} -wi {{ width }} -hi {{ height }} -f {{ format }} -vdn "{{ v4l2_device_name }}" -cscf "$MIPI_CAMERA_SETUP_CONF_FILE_PATH" -fps {{ fps }} + camera_test.py testing -sn {{ scenario }} -p "$PLATFORM_NAME" -c {{ camera }} -pi {{ physical_interface }} -m {{ method }} -wi {{ width }} -hi {{ height }} -f {{ format }} -vdn "{{ v4l2_device_name }}" -cscf "$MIPI_CAMERA_SETUP_CONF_FILE_PATH" -fps {{ fps }}{% if mode is defined %} -md {{ mode }}{% endif %} From b35dbff484fc9fa68067172a602ae658c07d6dca Mon Sep 17 00:00:00 2001 From: Isaac Yang Date: Fri, 17 Jul 2026 14:28:30 +0800 Subject: [PATCH 02/10] Rework camera identifiers, job ids and GST paths Address review feedback on the Jetson camera addition: - GStreamer plugin paths are no longer hardcoded in camera_jetson.py. GST_PLUGIN_PATH / GST_PLUGIN_SYSTEM_PATH / GST_PLUGIN_SCANNER now flow from the checkbox configuration through the jobs' environ list (the capture subprocess inherits them), so any project can supply its own values, the same way GST_LAUNCH_BIN works. Only the DISPLAY unset stays in code. GST_LAUNCH_BIN / MEDIA_CTL_CMD / V4L2_CTL_CMD are also added to the template jobs' environ so their config overrides reach the testing jobs, not just the readiness job. - The scenario JSON gains an optional camera_id key that overrides v4l2_device_name, which remains the default camera identifier, for capture methods that do not address the camera by its v4l2 name. The Jetson files declare camera_id (the Argus source index) and drop the overloaded v4l2_device_name. - The resource job now composes a per-record name field and the job-id templates collapse to capture-image_{{ name }} / record-video_{{ name }}, removing the hard-to-read jinja2 mode conditionals from the ids. Rendered ids are byte-identical for all 12 existing scenario files (verified old-vs-new across Genio, NXP, RZ and Jetson IMX274). - The IMX219 scenario is split into cam0 and cam1 files: the Orin NX and Orin Nano fit the same module on different carrier connectors, and the connector label is part of every job id. camera_id stays 0 in both files since Argus indexes detected sensors, not connectors. - Add unit tests for CameraResources name composition and the camera_id override. --- .../bin/camera_jetson.py | 92 +++++-------------- .../bin/camera_utils.py | 46 +++++++++- .../Test_Scenario_and_Test_Setup.md | 40 +++++++- ...ipi_camera_test_scenario_imx219_cam0.json} | 6 +- ...mipi_camera_test_scenario_imx219_cam1.json | 48 ++++++++++ ...mipi_camera_test_scenario_imx274_dual.json | 12 +-- .../tests/test_camera_utils.py | 69 ++++++++++++++ .../units/camera/README.md | 27 +++++- .../units/camera/jobs.pxu | 8 +- 9 files changed, 256 insertions(+), 92 deletions(-) rename contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/{jetson_mipi_camera_test_scenario_imx219.json => jetson_mipi_camera_test_scenario_imx219_cam0.json} (93%) create mode 100644 contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/jetson_mipi_camera_test_scenario_imx219_cam1.json create mode 100644 contrib/checkbox-ce-oem/checkbox-provider-ce-oem/tests/test_camera_utils.py diff --git a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/bin/camera_jetson.py b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/bin/camera_jetson.py index 20e7cb6337..07603a9382 100755 --- a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/bin/camera_jetson.py +++ b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/bin/camera_jetson.py @@ -38,21 +38,18 @@ os.getenv("NVARGUS_NVRAW_BIN", "nvargus_nvraw") ) -# GStreamer plugin search paths. The NVIDIA plugins (nvarguscamerasrc, -# nvvidconv) are not on the default search path, so every Jetson gstreamer job -# has to point GStreamer at them - the deb ones included. Values copied -# verbatim from units/Jetson/camera_job.pxu:74 (deb) and :152-153 (the snap -# video job, which is the working snap pair: it covers both the checkbox -# runtime's core elements and the NVIDIA dir). -DEB_GST_PLUGIN_PATH = "/usr/lib/aarch64-linux-gnu/gstreamer-1.0/" -SNAP_GST_PLUGIN_SYSTEM_PATHS = ( - "checkbox-runtime/usr/lib/aarch64-linux-gnu/gstreamer-1.0", - "usr/lib/aarch64-linux-gnu/gstreamer-1.0", -) -SNAP_GST_PLUGIN_SCANNER = ( - "checkbox-runtime/usr/lib/aarch64-linux-gnu/gstreamer1.0/" - "gstreamer-1.0/gst-plugin-scanner" -) +# DISPLAY must be unset for every Argus capture (both nvargus_nvraw and +# nvarguscamerasrc try to bring up an EGL preview when it is set, which +# wedges headless/ssh runs) - the legacy units/Jetson jobs all start with +# 'unset DISPLAY'. execute_command() runs without a shell, so /usr/bin/env +# carries the unset instead. +# +# The GStreamer plugin search paths (the NVIDIA plugins are not on the +# default search path) are deliberately NOT set here: GST_PLUGIN_PATH, +# GST_PLUGIN_SYSTEM_PATH and GST_PLUGIN_SCANNER are passed through the +# checkbox environment via the jobs' environ list, so each project supplies +# its own values - see units/camera/README.md. +ENV_PREFIX = "/usr/bin/env -u DISPLAY" # Timeouts in seconds for execute_command(). Mandatory, not a nice-to-have: # wedged Argus captures have happened, which is why every current Jetson job is @@ -122,52 +119,6 @@ def jetson_camera_factory(camera_module: str) -> Type[CameraInterface]: return handler_class -def gst_env_prefix() -> str: - """ - Build an 'env' prefix for the gst-launch command. - - execute_command() runs subprocess.run(shlex.split(cmd)) - there is no shell - and no env= parameter - so an 'export X=...; gst-launch ...' command string - is unrunnable and a literal "$SNAP" would never expand. /usr/bin/env - carries both the DISPLAY unset and the plugin variables with no framework - change, so the snap paths are built here in Python instead. - - Deb vs snap is decided by where GST_LAUNCH_BIN (already shutil.which() - resolved in camera_utils) lives. - - Returns: - The env prefix, without a trailing space. - - Raises: - CameraConfigurationError: If running from a snap with SNAP unset - """ - if not GST_LAUNCH_BIN or not GST_LAUNCH_BIN.startswith("/snap/"): - return "/usr/bin/env -u DISPLAY GST_PLUGIN_PATH={}".format( - DEB_GST_PLUGIN_PATH - ) - - snap = os.getenv("SNAP") - if not snap: - log_and_raise_error( - "'{}' is a snap binary but the SNAP environment variable is " - "unset, so the GStreamer plugin paths cannot be built.".format( - GST_LAUNCH_BIN - ), - CameraConfigurationError, - ) - - plugin_system_path = ":".join( - os.path.join(snap, path) for path in SNAP_GST_PLUGIN_SYSTEM_PATHS - ) - return ( - "/usr/bin/env -u DISPLAY GST_PLUGIN_SYSTEM_PATH={} " - "GST_PLUGIN_SCANNER={}".format( - plugin_system_path, - os.path.join(snap, SNAP_GST_PLUGIN_SCANNER), - ) - ) - - class JetsonBaseCamera(CameraInterface): """ Base class for Jetson camera implementations. @@ -189,9 +140,11 @@ def _get_sensor_id(self, v4l2_device_name: str) -> int: """ Get the Argus source index for the given camera. - On Jetson, v4l2_device_name carries the Argus source_index as a string - ("0" / "1") rather than a v4l2 name - it is what 'nvargus_nvraw --c N' - and 'nvarguscamerasrc sensor-id=N' actually consume. + On Jetson the scenario JSON declares camera_id - the Argus + source_index as a string ("0" / "1"), which is what + 'nvargus_nvraw --c N' and 'nvarguscamerasrc sensor-id=N' actually + consume - and the resource generator carries it in the framework's + default identifier field, v4l2_device_name. Do not derive this from physical_interface, the VI channel or the i2c bus number: the AGX Orin's two sensors sit on VI channels 0 and 2, and @@ -203,10 +156,9 @@ def _get_sensor_id(self, v4l2_device_name: str) -> int: return int(v4l2_device_name) except (TypeError, ValueError): log_and_raise_error( - "Invalid v4l2_device_name '{}': on Jetson this field carries " - "the Argus source index (e.g. '0'), not a device name.".format( - v4l2_device_name - ), + "Invalid camera identifier '{}': on Jetson the scenario's " + "camera_id carries the Argus source index (e.g. '0'), not " + "a device name.".format(v4l2_device_name), CameraConfigurationError, ) @@ -260,7 +212,7 @@ def _build_gstreamer_cmd( # otherwise be a syntax error. shlex.split() strips the quotes back off # before execution, so argv is unaffected either way. return "{} {} {} ! '{}' ! nvvidconv ! '{}' ! {}".format( - gst_env_prefix(), + ENV_PREFIX, GST_LAUNCH_BIN, " ".join(src_words), ",".join(caps_words), @@ -288,7 +240,7 @@ def _build_nvargus_cmd( CameraConfigurationError, ) - words = [NVARGUS_NVRAW_BIN, "--c {}".format(sensor_id)] + words = [ENV_PREFIX, NVARGUS_NVRAW_BIN, "--c {}".format(sensor_id)] if mode is not None: words.append("--mode {}".format(mode)) words.append("--format nvraw") diff --git a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/bin/camera_utils.py b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/bin/camera_utils.py index 33aad835c6..a982f803b6 100755 --- a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/bin/camera_utils.py +++ b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/bin/camera_utils.py @@ -1063,6 +1063,31 @@ def _validate_dimension( CameraConfigurationError, ) + @staticmethod + def _compose_name( + item: dict, resolution: dict, format_str: str, scenario_type: str + ) -> str: + """ + Compose the human-readable name a job id is built from, so the + job templates stay free of jinja2 conditionals. + + e.g. imx219_cam0_gstreamer_1920x1080@30fps_mode2_NV12 + """ + name = "{}_{}_{}_{}x{}".format( + item["camera"], + item["physical_interface"], + item["method"], + resolution["width"], + resolution["height"], + ) + if scenario_type == CameraScenarios.RECORD_VIDEO.value: + name += "@{}fps".format(resolution["fps"]) + # The mode suffix keeps sensor modes sharing a resolution and frame + # rate distinguishable (e.g. IMX274 Argus modes 1 and 3) + if "mode" in resolution: + name += "_mode{}".format(resolution["mode"]) + return name + "_{}".format(format_str) + def _process_scenario_items( self, scenarios: list, scenario_type: str ) -> None: @@ -1078,7 +1103,6 @@ def _process_scenario_items( "camera", "method", "physical_interface", - "v4l2_device_name", "resolutions", "formats", ] @@ -1088,16 +1112,34 @@ def _process_scenario_items( self._validate_scenario_item(item, required_fields) self._validate_resolution_formats(item, scenario_type) + # v4l2_device_name is the default camera identifier; the + # optional camera_id key overrides it for capture methods + # that do not address the camera by its v4l2 name (e.g. the + # Argus source index on Jetson). + device_name = item.get("camera_id") or item.get( + "v4l2_device_name" + ) + if device_name is None: + log_and_raise_error( + "Scenario item for camera '{}' needs " + "'v4l2_device_name' or its 'camera_id' " + "override".format(item["camera"]), + CameraConfigurationError, + ) + # Generate resources for each resolution/format combination for resolution, format_str in product( item["resolutions"], item["formats"] ): resource_item = { "scenario": self._current_scenario_name, + "name": self._compose_name( + item, resolution, format_str, scenario_type + ), "camera": item["camera"], "method": item["method"], "physical_interface": item["physical_interface"], - "v4l2_device_name": item["v4l2_device_name"], + "v4l2_device_name": device_name, "format": format_str, "width": resolution["width"], "height": resolution["height"], diff --git a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/Test_Scenario_and_Test_Setup.md b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/Test_Scenario_and_Test_Setup.md index b0ecab2b93..f39098aed2 100644 --- a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/Test_Scenario_and_Test_Setup.md +++ b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/Test_Scenario_and_Test_Setup.md @@ -16,10 +16,31 @@ configurations. > `nvarguscamerasrc sensor-mode=`. This is what makes sensor modes that share a > resolution and frame rate (IMX274 modes 1 and 3) separately testable. -> **Note:** `v4l2_device_name` holds the Argus `source_index` (0 / 1), not a -> `/dev/video` name. Argus addresses sensors by index; the Tegra v4l2 names embed -> an i2c bus number and a device-tree VI channel, neither of which tracks the -> sensor index (the AGX Orin's two sensors are on VI channels 0 and 2). +> **Note:** every item declares `camera_id` — the Argus `source_index` +> (0 / 1) — instead of the framework's default `v4l2_device_name` identifier. +> Argus addresses sensors by index; the Tegra v4l2 names embed an i2c bus +> number and a device-tree VI channel, neither of which tracks the sensor +> index (the AGX Orin's two sensors are on VI channels 0 and 2). + +## Required Checkbox Environment + +The NVIDIA GStreamer plugins (`nvarguscamerasrc`, `nvvidconv`) are not on +GStreamer's default search path, so the checkbox configuration must point +GStreamer at them (the jobs pass these variables through to the test): + +```ini +# Deb / classic image +GST_PLUGIN_PATH=/usr/lib/aarch64-linux-gnu/gstreamer-1.0/ + +# Snap (checkbox-ce-oem): the working pair covers both the checkbox +# runtime's core elements and the NVIDIA plugin dir +GST_PLUGIN_SYSTEM_PATH=/snap/checkbox-ce-oem/current/checkbox-runtime/usr/lib/aarch64-linux-gnu/gstreamer-1.0:/snap/checkbox-ce-oem/current/usr/lib/aarch64-linux-gnu/gstreamer-1.0 +GST_PLUGIN_SCANNER=/snap/checkbox-ce-oem/current/checkbox-runtime/usr/lib/aarch64-linux-gnu/gstreamer1.0/gstreamer-1.0/gst-plugin-scanner +``` + +`DISPLAY` is unset by the test itself (Argus tries to bring up an EGL +preview when it is set, which wedges headless runs), so nothing is needed +in the configuration for it. ## Overview @@ -67,7 +88,16 @@ Jetson configurations only require test scenario files (no test setup needed). - Hardware: Arducam Nvidia Jetson native camera IMX219 - Boards: Jetson Orin NX, Jetson Orin Nano (same sensor, same mode set) - Documentation: [Arducam Nvidia Jetson native camera IMX219](https://docs.arducam.com/Nvidia-Jetson-Camera/Native-Camera/imx219/) (L4T 35.x table) -- Configuration: [`jetson_mipi_camera_test_scenario_imx219.json`](jetson_mipi_camera_test_scenario_imx219.json) +- Configuration (one file per carrier-board connector): + - [`jetson_mipi_camera_test_scenario_imx219_cam0.json`](jetson_mipi_camera_test_scenario_imx219_cam0.json) + - [`jetson_mipi_camera_test_scenario_imx219_cam1.json`](jetson_mipi_camera_test_scenario_imx219_cam1.json) + +> **Note:** the Orin NX and Orin Nano DUTs carry the same single module but +> fitted on different carrier-board connectors — one on `cam0`, the other on +> `cam1` — and the connector label is part of every job id. Point +> `MIPI_SCENARIO_DEFINITION_FILE_PATH` at the file matching where the module +> is fitted. `camera_id` stays `0` in both files: Argus indexes the sensors +> it detects, not the connectors. | Argus mode | Resolution | FPS | | --- | --- | --- | diff --git a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/jetson_mipi_camera_test_scenario_imx219.json b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/jetson_mipi_camera_test_scenario_imx219_cam0.json similarity index 93% rename from contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/jetson_mipi_camera_test_scenario_imx219.json rename to contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/jetson_mipi_camera_test_scenario_imx219_cam0.json index bde5f5d8ca..c1467fd7dc 100644 --- a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/jetson_mipi_camera_test_scenario_imx219.json +++ b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/jetson_mipi_camera_test_scenario_imx219_cam0.json @@ -4,7 +4,7 @@ "camera": "imx219", "method": "nvargus_nvraw", "physical_interface": "cam0", - "v4l2_device_name": "0", + "camera_id": "0", "formats": ["NVRAW"], "resolutions": [ {"width": 3280, "height": 2464, "mode": 0}, @@ -18,7 +18,7 @@ "camera": "imx219", "method": "gstreamer", "physical_interface": "cam0", - "v4l2_device_name": "0", + "camera_id": "0", "formats": ["NV12"], "resolutions": [ {"width": 3280, "height": 2464, "mode": 0}, @@ -34,7 +34,7 @@ "camera": "imx219", "method": "gstreamer", "physical_interface": "cam0", - "v4l2_device_name": "0", + "camera_id": "0", "formats": ["NV12"], "resolutions": [ {"width": 3280, "height": 2464, "fps": 21, "mode": 0}, diff --git a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/jetson_mipi_camera_test_scenario_imx219_cam1.json b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/jetson_mipi_camera_test_scenario_imx219_cam1.json new file mode 100644 index 0000000000..a66f647dca --- /dev/null +++ b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/jetson_mipi_camera_test_scenario_imx219_cam1.json @@ -0,0 +1,48 @@ +{ + "capture_image": [ + { + "camera": "imx219", + "method": "nvargus_nvraw", + "physical_interface": "cam1", + "camera_id": "0", + "formats": ["NVRAW"], + "resolutions": [ + {"width": 3280, "height": 2464, "mode": 0}, + {"width": 3280, "height": 1848, "mode": 1}, + {"width": 1920, "height": 1080, "mode": 2}, + {"width": 1640, "height": 1232, "mode": 3}, + {"width": 1280, "height": 720, "mode": 4} + ] + }, + { + "camera": "imx219", + "method": "gstreamer", + "physical_interface": "cam1", + "camera_id": "0", + "formats": ["NV12"], + "resolutions": [ + {"width": 3280, "height": 2464, "mode": 0}, + {"width": 3280, "height": 1848, "mode": 1}, + {"width": 1920, "height": 1080, "mode": 2}, + {"width": 1640, "height": 1232, "mode": 3}, + {"width": 1280, "height": 720, "mode": 4} + ] + } + ], + "record_video": [ + { + "camera": "imx219", + "method": "gstreamer", + "physical_interface": "cam1", + "camera_id": "0", + "formats": ["NV12"], + "resolutions": [ + {"width": 3280, "height": 2464, "fps": 21, "mode": 0}, + {"width": 3280, "height": 1848, "fps": 28, "mode": 1}, + {"width": 1920, "height": 1080, "fps": 30, "mode": 2}, + {"width": 1640, "height": 1232, "fps": 30, "mode": 3}, + {"width": 1280, "height": 720, "fps": 60, "mode": 4} + ] + } + ] +} diff --git a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/jetson_mipi_camera_test_scenario_imx274_dual.json b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/jetson_mipi_camera_test_scenario_imx274_dual.json index 1352b0a10d..1d5231e5bb 100644 --- a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/jetson_mipi_camera_test_scenario_imx274_dual.json +++ b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/jetson_mipi_camera_test_scenario_imx274_dual.json @@ -4,7 +4,7 @@ "camera": "imx274", "method": "nvargus_nvraw", "physical_interface": "cam0", - "v4l2_device_name": "0", + "camera_id": "0", "formats": ["NVRAW"], "resolutions": [ {"width": 3840, "height": 2160, "mode": 0}, @@ -17,7 +17,7 @@ "camera": "imx274", "method": "nvargus_nvraw", "physical_interface": "cam1", - "v4l2_device_name": "1", + "camera_id": "1", "formats": ["NVRAW"], "resolutions": [ {"width": 3840, "height": 2160, "mode": 0}, @@ -30,7 +30,7 @@ "camera": "imx274", "method": "gstreamer", "physical_interface": "cam0", - "v4l2_device_name": "0", + "camera_id": "0", "formats": ["NV12"], "resolutions": [ {"width": 3840, "height": 2160, "mode": 0}, @@ -43,7 +43,7 @@ "camera": "imx274", "method": "gstreamer", "physical_interface": "cam1", - "v4l2_device_name": "1", + "camera_id": "1", "formats": ["NV12"], "resolutions": [ {"width": 3840, "height": 2160, "mode": 0}, @@ -58,7 +58,7 @@ "camera": "imx274", "method": "gstreamer", "physical_interface": "cam0", - "v4l2_device_name": "0", + "camera_id": "0", "formats": ["NV12"], "resolutions": [ {"width": 3840, "height": 2160, "fps": 60, "mode": 0}, @@ -71,7 +71,7 @@ "camera": "imx274", "method": "gstreamer", "physical_interface": "cam1", - "v4l2_device_name": "1", + "camera_id": "1", "formats": ["NV12"], "resolutions": [ {"width": 3840, "height": 2160, "fps": 60, "mode": 0}, diff --git a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/tests/test_camera_utils.py b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/tests/test_camera_utils.py new file mode 100644 index 0000000000..547ecf2921 --- /dev/null +++ b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/tests/test_camera_utils.py @@ -0,0 +1,69 @@ +import unittest + +from camera_utils import CameraResources + + +class TestCameraResourcesItems(unittest.TestCase): + """Resource generation: composed name and camera_id override.""" + + def _item(self, **overrides): + item = { + "camera": "imx219", + "method": "gstreamer", + "physical_interface": "cam0", + "v4l2_device_name": "vi-output, imx219 9-0010", + "formats": ["NV12"], + "resolutions": [{"width": 1920, "height": 1080}], + } + item.update(overrides) + return item + + def test_capture_image_default_identifier(self): + resources = CameraResources() + resources._current_scenario_name = "capture_image" + resources.capture_image([self._item()]) + + self.assertEqual(len(resources._resource_items), 1) + record = resources._resource_items[0] + self.assertEqual( + record["name"], "imx219_cam0_gstreamer_1920x1080_NV12" + ) + self.assertEqual( + record["v4l2_device_name"], "vi-output, imx219 9-0010" + ) + self.assertNotIn("mode", record) + + def test_record_video_camera_id_override_and_mode(self): + resources = CameraResources() + resources._current_scenario_name = "record_video" + item = self._item( + camera_id="0", + resolutions=[ + {"width": 1920, "height": 1080, "fps": 30, "mode": 2} + ], + ) + del item["v4l2_device_name"] + resources.record_video([item]) + + self.assertEqual(len(resources._resource_items), 1) + record = resources._resource_items[0] + self.assertEqual( + record["name"], "imx219_cam0_gstreamer_1920x1080@30fps_mode2_NV12" + ) + # camera_id overrides the default identifier + self.assertEqual(record["v4l2_device_name"], "0") + self.assertEqual(record["mode"], 2) + self.assertEqual(record["fps"], 30) + + def test_missing_identifier_skips_item(self): + resources = CameraResources() + resources._current_scenario_name = "capture_image" + item = self._item() + del item["v4l2_device_name"] + resources.capture_image([item]) + + self.assertEqual(resources._resource_items, []) + + +if __name__ == "__main__": + unittest.main() diff --git a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/camera/README.md b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/camera/README.md index a65026ce4b..d7d53c8f8a 100644 --- a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/camera/README.md +++ b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/camera/README.md @@ -49,6 +49,16 @@ export MIPI_SCENARIO_DEFINITION_FILE_PATH=data/Genio-MIPI-Camera-TestScenario-Te export MIPI_SCENARIO_DEFINITION_FILE_PATH=/usr/share/checkbox/data/Genio-MIPI-Camera-TestScenario-TestSetup/scenario.json ``` +### Camera Identifier + +Each scenario item identifies its camera with `v4l2_device_name` (the name +shown by `v4l2-ctl --list-devices`) — this is the default. Platforms whose +capture method does not address the camera by its v4l2 name can declare +`camera_id` instead, which overrides `v4l2_device_name` as the identifier +passed to the test (e.g. on Jetson, `camera_id` holds the Argus source +index consumed by `nvarguscamerasrc sensor-id=` / `nvargus_nvraw --c`). +Every item must declare at least one of the two. + ## Template Jobs Two template jobs are generated based on the output of `mipi_camera_resource`: @@ -56,15 +66,27 @@ Two template jobs are generated based on the output of `mipi_camera_resource`: ### Image Capture Job ```text -id: mipi-camera/capture-image_{{ camera }}_{{ physical_interface }}_{{ method }}_{{ width }}x{{ height }}_{{ format }} +id: ce-oem-mipi-camera/capture-image_{{ name }} ``` ### Video Recording Job ```text -id: mipi-camera/record-video_{{ camera }}_{{ physical_interface }}_{{ method }}_{{ width }}x{{ height }}@{{ fps }}fps_{{ format }} +id: ce-oem-mipi-camera/record-video_{{ name }} ``` +The `name` field is composed by the resource job so the job templates stay +free of conditionals: + +```text +___x[@fps][_mode]_ +``` + +`@fps` appears only for `record_video` items, and `_mode` only +when the scenario declares a sensor `mode` for the resolution (e.g. the +Argus mode index on Jetson). Example: +`imx219_cam0_gstreamer_1920x1080@30fps_mode2_NV12`. + **Required Environment Variables:** - `PLATFORM_NAME`: **Required** - The platform name of the Device Under Test (DUT). This variable is used to identify which project's code will be used for camera operations. @@ -83,6 +105,7 @@ id: mipi-camera/record-video_{{ camera }}_{{ physical_interface }}_{{ method }}_ ### Optional - `MIPI_CAMERA_SETUP_CONF_FILE_PATH`: Path to the setup configuration file in JSON format (required if your camera needs to configure format/resolution of pads or set pad links) +- `GST_PLUGIN_PATH`, `GST_PLUGIN_SYSTEM_PATH`, `GST_PLUGIN_SCANNER`: GStreamer plugin search paths, passed through to the test environment. Required on platforms whose GStreamer plugins live off the default search path (e.g. the NVIDIA plugins on Jetson — see the [Jetson documentation](../../data/Jetson-MIPI-Camera-TestScenario-TestSetup/Test_Scenario_and_Test_Setup.md) for the deb and snap values) ### Environment Variable Examples diff --git a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/camera/jobs.pxu b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/camera/jobs.pxu index e3414426f5..0faf5cc8a6 100644 --- a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/camera/jobs.pxu +++ b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/camera/jobs.pxu @@ -25,7 +25,7 @@ template-unit: job template-id: ce-oem-mipi-camera/capture-image-scenario template-filter: mipi_camera_resource.scenario == "capture_image" -id: ce-oem-mipi-camera/capture-image_{{ camera }}_{{ physical_interface }}_{{ method }}_{{ width }}x{{ height }}{% if mode is defined %}_mode{{ mode }}{% endif %}_{{ format }} +id: ce-oem-mipi-camera/capture-image_{{ name }} _template-summary: MIPI Camera Captures Image _summary: {{ camera }} captures {{ width }}x{{ height }} {{ format }} images via {{ method }} _description: Test {{ camera }} sensor on the {{ physical_interface }} slot can capture {{ width }}x{{ height }} {{ format }} images via {{ method }} method @@ -39,7 +39,7 @@ imports: from com.canonical.plainbox import manifest requires: manifest.has_vendor_specific_mipi_camera == "True" -environ: PLATFORM_NAME MIPI_CAMERA_SETUP_CONF_FILE_PATH +environ: PLATFORM_NAME MIPI_CAMERA_SETUP_CONF_FILE_PATH GST_LAUNCH_BIN MEDIA_CTL_CMD V4L2_CTL_CMD GST_PLUGIN_PATH GST_PLUGIN_SYSTEM_PATH GST_PLUGIN_SCANNER command: camera_test.py testing -sn {{ scenario }} -p "$PLATFORM_NAME" -c {{ camera }} -pi {{ physical_interface }} -m {{ method }} -wi {{ width }} -hi {{ height }} -f {{ format }} -vdn "{{ v4l2_device_name }}" -cscf "$MIPI_CAMERA_SETUP_CONF_FILE_PATH"{% if mode is defined %} -md {{ mode }}{% endif %} @@ -50,7 +50,7 @@ template-unit: job template-id: ce-oem-mipi-camera/record-video-scenario template-filter: mipi_camera_resource.scenario == "record_video" -id: ce-oem-mipi-camera/record-video_{{ camera }}_{{ physical_interface }}_{{ method }}_{{ width }}x{{ height }}@{{ fps }}fps{% if mode is defined %}_mode{{ mode }}{% endif %}_{{ format }} +id: ce-oem-mipi-camera/record-video_{{ name }} _template-summary: MIPI Camera Records Video _summary: {{ camera }} records a {{ width }}x{{ height }}@{{ fps }}fps {{ format }} video via {{ method }} _description: Test {{ camera }} sensor on the {{ physical_interface }} slot can record {{ width }}x{{ height }}@{{ fps }}fps {{ format }} video via {{ method }} method @@ -64,6 +64,6 @@ imports: from com.canonical.plainbox import manifest requires: manifest.has_vendor_specific_mipi_camera == "True" -environ: PLATFORM_NAME MIPI_CAMERA_SETUP_CONF_FILE_PATH +environ: PLATFORM_NAME MIPI_CAMERA_SETUP_CONF_FILE_PATH GST_LAUNCH_BIN MEDIA_CTL_CMD V4L2_CTL_CMD GST_PLUGIN_PATH GST_PLUGIN_SYSTEM_PATH GST_PLUGIN_SCANNER command: camera_test.py testing -sn {{ scenario }} -p "$PLATFORM_NAME" -c {{ camera }} -pi {{ physical_interface }} -m {{ method }} -wi {{ width }} -hi {{ height }} -f {{ format }} -vdn "{{ v4l2_device_name }}" -cscf "$MIPI_CAMERA_SETUP_CONF_FILE_PATH" -fps {{ fps }}{% if mode is defined %} -md {{ mode }}{% endif %} From 91eb27fad8a56af95e5c8b8588bc155daad74cf5 Mon Sep 17 00:00:00 2001 From: Isaac Yang Date: Fri, 17 Jul 2026 15:38:46 +0800 Subject: [PATCH 03/10] Fix camera-full nested parts and cam0 wiring note The ce-oem-mipi-camera-full plan nested mipi-camera-manual and mipi-camera-automated, plan ids that do not exist in any provider; the actual plans are ce-oem-mipi-camera-manual/-automated (pre-existing, the top-level ce-oem plan already nests the correct ids). The IMX219 doc note assumed the Orin NX and Orin Nano DUTs sit on different carrier connectors; the certification DUTs both carry the module on cam0. Keep one scenario file per connector and describe the selection by actual wiring. --- .../Test_Scenario_and_Test_Setup.md | 13 +++++++------ .../units/camera/test-plan.pxu | 4 ++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/Test_Scenario_and_Test_Setup.md b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/Test_Scenario_and_Test_Setup.md index f39098aed2..21f0793dd5 100644 --- a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/Test_Scenario_and_Test_Setup.md +++ b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/Test_Scenario_and_Test_Setup.md @@ -92,12 +92,13 @@ Jetson configurations only require test scenario files (no test setup needed). - [`jetson_mipi_camera_test_scenario_imx219_cam0.json`](jetson_mipi_camera_test_scenario_imx219_cam0.json) - [`jetson_mipi_camera_test_scenario_imx219_cam1.json`](jetson_mipi_camera_test_scenario_imx219_cam1.json) -> **Note:** the Orin NX and Orin Nano DUTs carry the same single module but -> fitted on different carrier-board connectors — one on `cam0`, the other on -> `cam1` — and the connector label is part of every job id. Point -> `MIPI_SCENARIO_DEFINITION_FILE_PATH` at the file matching where the module -> is fitted. `camera_id` stays `0` in both files: Argus indexes the sensors -> it detects, not the connectors. +> **Note:** the single module can be fitted on either carrier-board +> connector (`cam0` / `cam1`), and the connector label is part of every job +> id, so there is one scenario file per connector. Point +> `MIPI_SCENARIO_DEFINITION_FILE_PATH` at the file matching the DUT's +> wiring — the current certification Orin NX and Orin Nano DUTs both carry +> the module on `cam0`. `camera_id` stays `0` in both files: Argus indexes +> the sensors it detects, not the connectors. | Argus mode | Resolution | FPS | | --- | --- | --- | diff --git a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/camera/test-plan.pxu b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/camera/test-plan.pxu index 0d3cb33121..35a5c32b09 100644 --- a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/camera/test-plan.pxu +++ b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/camera/test-plan.pxu @@ -4,8 +4,8 @@ _name: MIPI Camera tests _description: Full tests for MIPI Camera include: nested_part: - mipi-camera-manual - mipi-camera-automated + ce-oem-mipi-camera-manual + ce-oem-mipi-camera-automated id: ce-oem-mipi-camera-manual unit: test plan From 7f605e7ff3437885c5acaf470e1f355a2fd59839 Mon Sep 17 00:00:00 2001 From: Isaac Yang Date: Fri, 17 Jul 2026 16:28:18 +0800 Subject: [PATCH 04/10] Fix Jetson camera jobs per Orin NX target run First run on the Orin NX (202304-31523, Ubuntu Core 22) generated all 15 expected cam0 jobs with correct ids, but every capture failed. Four test-definition fixes, all reproduced and root-caused on the DUT: - Add user: root to both camera templates. checkbox normalizes job execution to the unprivileged session user, which has no EGL/Argus device access on Jetson (Argus 'No EGL device available', nvraw exit 4). The legacy Jetson jobs all carry user: root. This also switches existing platforms' generated jobs to root, a superset of their current privileges. - Pin the capture framerate: gstreamer captures negotiated the Argus 30 fps default, which exceeds IMX219 mode 0 (21) and mode 1 (28) maxima, so those jobs could never pass. The scenario JSON now declares fps on gstreamer capture resolutions, the resource emits fps whenever a resolution declares it, and the capture command and Jetson handler pass it through to the caps. Capture job names and ids are unchanged. - Tolerate post-EOS teardown errors: some CSI modules (Arducam IMX219 clones) stochastically post 'Argus Correctable Error' after EOS, failing gst-launch although every frame was captured. If the artifact is non-empty the error is logged and tolerated; check_nonzero_files() remains the final pass criterion. - Log the child's stdout/stderr when execute_command fails, so the Argus diagnostics reach the job io-log. Also add NVARGUS_NVRAW_BIN to the template jobs' environ (the code honors it but the config override never reached the jobs) and document the Ubuntu Core setup: capture tools come from the NVIDIA multimedia snap via snap aliases (gst-launch-1.0, nvargus_nvraw), which hosts the Argus daemon and resolves its own GStreamer plugin paths, so no GST_PLUGIN_* variables are needed there. --- .../__pycache__/camera_jetson.cpython-312.pyc | Bin 0 -> 13839 bytes .../__pycache__/camera_utils.cpython-312.pyc | Bin 0 -> 44984 bytes .../bin/camera_jetson.py | 49 ++++- .../bin/camera_test.py | 7 +- .../bin/camera_utils.py | 16 +- .../Test_Scenario_and_Test_Setup.md | 34 +++- ...mipi_camera_test_scenario_imx219_cam0.json | 10 +- ...mipi_camera_test_scenario_imx219_cam1.json | 10 +- ...mipi_camera_test_scenario_imx274_dual.json | 184 +++++++++++++++--- .../test_camera_utils.cpython-312.pyc | Bin 0 -> 4174 bytes .../tests/test_camera_utils.py | 18 ++ .../units/camera/jobs.pxu | 8 +- 12 files changed, 272 insertions(+), 64 deletions(-) create mode 100644 contrib/checkbox-ce-oem/checkbox-provider-ce-oem/bin/__pycache__/camera_jetson.cpython-312.pyc create mode 100644 contrib/checkbox-ce-oem/checkbox-provider-ce-oem/bin/__pycache__/camera_utils.cpython-312.pyc create mode 100644 contrib/checkbox-ce-oem/checkbox-provider-ce-oem/tests/__pycache__/test_camera_utils.cpython-312.pyc diff --git a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/bin/__pycache__/camera_jetson.cpython-312.pyc b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/bin/__pycache__/camera_jetson.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..49a1c7ccb8114c1e9c9c1b6fe4f22a20fd6de209 GIT binary patch literal 13839 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

+zUEPw_6@9w_>vF}GZYJo?c0 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 int: CameraConfigurationError, ) + def _execute_gst_cmd( + self, cmd: str, timeout: int, artifact_path: str + ) -> None: + """ + Run a gstreamer capture command, tolerating teardown-only failures. + + Some CSI modules (e.g. Arducam IMX219 clones) stochastically post + 'Argus Correctable Error' events after EOS, making gst-launch exit + non-zero although every frame was captured. If the artifact landed + with a non-zero size, log and continue - the framework's + check_nonzero_files() remains the final pass criterion. + """ + logger.info("Executing command:\n{}".format(cmd)) + try: + output = execute_command(cmd=cmd, timeout=timeout) + logger.info("Output:\n{}".format(output)) + except CameraOperationError: + try: + artifact_ok = os.path.getsize(artifact_path) > 0 + except OSError: + artifact_ok = False + if not artifact_ok: + raise + logger.warning( + "gst-launch exited non-zero but the artifact '{}' is " + "non-empty; tolerating as a post-EOS teardown error.".format( + artifact_path + ) + ) + def _build_gstreamer_cmd( self, sensor_id: int, @@ -258,6 +289,7 @@ def capture_image( method: str, v4l2_device_name: str, mode: Optional[int] = None, + framerate: Optional[int] = None, ) -> None: """Capture an image using the specified method.""" full_artifact_path = self._get_artifact_path( @@ -274,30 +306,31 @@ def capture_image( ) if method == SupportedMethods.GSTREAMER: + # framerate pins modes whose maximum rate is below the Argus + # 30 fps negotiation default (e.g. IMX219 modes 0 and 1) cmd = self._build_gstreamer_cmd( sensor_id, width, height, format, full_artifact_path, + framerate=framerate, mode=mode, ) - timeout = GST_IMAGE_TIMEOUT + self._execute_gst_cmd(cmd, GST_IMAGE_TIMEOUT, full_artifact_path) elif method == SupportedMethods.NVARGUS_NVRAW: cmd = self._build_nvargus_cmd( sensor_id, full_artifact_path, mode=mode ) - timeout = NVARGUS_TIMEOUT + logger.info("Executing command:\n{}".format(cmd)) + output = execute_command(cmd=cmd, timeout=NVARGUS_TIMEOUT) + logger.info("Output:\n{}".format(output)) else: msg = "No suitable method such as '{}' or '{}' be provided".format( SupportedMethods.GSTREAMER, SupportedMethods.NVARGUS_NVRAW ) log_and_raise_error(msg, CameraConfigurationError) - logger.info("Executing command:\n{}".format(cmd)) - output = execute_command(cmd=cmd, timeout=timeout) - logger.info("Output:\n{}".format(output)) - def record_video( self, width: int, @@ -344,9 +377,7 @@ def record_video( ) log_and_raise_error(msg, CameraConfigurationError) - logger.info("Executing command:\n {}".format(cmd)) - output = execute_command(cmd=cmd, timeout=GST_VIDEO_TIMEOUT) - logger.info("Output:\n{}".format(output)) + self._execute_gst_cmd(cmd, GST_VIDEO_TIMEOUT, full_artifact_path) class Imx274Handler(JetsonBaseCamera): diff --git a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/bin/camera_test.py b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/bin/camera_test.py index 6beae43669..ec21badb5e 100755 --- a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/bin/camera_test.py +++ b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/bin/camera_test.py @@ -124,9 +124,12 @@ def _execute_capture_image_scenario( artifact_store_path: Path to store artifacts artifact_name: Base name for artifacts """ - # Only pass the sensor mode when the scenario declares one: the platforms - # that don't use it don't accept it either. + # Only pass the sensor mode / capture framerate when the scenario + # declares them: the platforms that don't use them don't accept them + # either. extra = {"mode": args.mode} if args.mode is not None else {} + if args.framerate is not None: + extra["framerate"] = args.framerate iteration = 5 # Capture multiple images for i in range(1, iteration + 1): diff --git a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/bin/camera_utils.py b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/bin/camera_utils.py index a982f803b6..3e17afc67d 100755 --- a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/bin/camera_utils.py +++ b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/bin/camera_utils.py @@ -166,6 +166,15 @@ def execute_command(cmd: str = "", timeout: int = 300) -> str: "Command timed out: {}".format(e), CameraTimeoutError, ) + except subprocess.CalledProcessError as e: + # Surface the child's own diagnostics (e.g. the Argus error text), + # which otherwise never reach the job's io-log + logger.error("stdout:\n{}".format(e.stdout)) + logger.error("stderr:\n{}".format(e.stderr)) + log_and_raise_error( + "Failed to execute command: {}".format(e), + CameraOperationError, + ) except Exception as e: log_and_raise_error( "Failed to execute command: {}".format(e), @@ -1145,8 +1154,11 @@ def _process_scenario_items( "height": resolution["height"], } - # Add fps for video scenarios - if scenario_type == "record_video": + # fps is mandatory for video scenarios and optional for + # capture ones, where it pins sensor modes whose maximum + # rate sits below the default negotiation rate (Argus + # defaults to 30 fps, above e.g. IMX219 mode 0's 21 fps) + if scenario_type == "record_video" or "fps" in resolution: resource_item["fps"] = resolution["fps"] # Add the sensor mode only when the resolution declares it diff --git a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/Test_Scenario_and_Test_Setup.md b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/Test_Scenario_and_Test_Setup.md index 21f0793dd5..f725d807b3 100644 --- a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/Test_Scenario_and_Test_Setup.md +++ b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/Test_Scenario_and_Test_Setup.md @@ -24,18 +24,36 @@ configurations. ## Required Checkbox Environment -The NVIDIA GStreamer plugins (`nvarguscamerasrc`, `nvvidconv`) are not on -GStreamer's default search path, so the checkbox configuration must point -GStreamer at them (the jobs pass these variables through to the test): +The capture tools (`gst-launch-1.0` with the NVIDIA plugins, and +`nvargus_nvraw`) must be reachable from the checkbox jobs. The jobs pass +`GST_LAUNCH_BIN`, `NVARGUS_NVRAW_BIN`, `GST_PLUGIN_PATH`, +`GST_PLUGIN_SYSTEM_PATH` and `GST_PLUGIN_SCANNER` through from the checkbox +configuration, so each image type supplies what it needs: + +**Classic / deb images** — the NVIDIA GStreamer plugins are off the default +search path: ```ini -# Deb / classic image GST_PLUGIN_PATH=/usr/lib/aarch64-linux-gnu/gstreamer-1.0/ +``` -# Snap (checkbox-ce-oem): the working pair covers both the checkbox -# runtime's core elements and the NVIDIA plugin dir -GST_PLUGIN_SYSTEM_PATH=/snap/checkbox-ce-oem/current/checkbox-runtime/usr/lib/aarch64-linux-gnu/gstreamer-1.0:/snap/checkbox-ce-oem/current/usr/lib/aarch64-linux-gnu/gstreamer-1.0 -GST_PLUGIN_SCANNER=/snap/checkbox-ce-oem/current/checkbox-runtime/usr/lib/aarch64-linux-gnu/gstreamer1.0/gstreamer-1.0/gst-plugin-scanner +**Ubuntu Core images** — checkbox-ce-oem carries no NVIDIA stack. Install +the NVIDIA multimedia snap (which hosts the Argus daemon and its own +GStreamer with the NVIDIA plugins) and alias its tools to the classic +command names before testing: + +```bash +sudo snap alias .gst-launch gst-launch-1.0 +sudo snap alias .nvargus-nvraw nvargus_nvraw +``` + +The multimedia snap resolves its own plugin paths, so no `GST_PLUGIN_*` +variables are needed. If the aliases are not on the job `PATH`, point the +tool overrides at them instead: + +```ini +GST_LAUNCH_BIN=/snap/bin/gst-launch-1.0 +NVARGUS_NVRAW_BIN=/snap/bin/nvargus_nvraw ``` `DISPLAY` is unset by the test itself (Argus tries to bring up an EGL diff --git a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/jetson_mipi_camera_test_scenario_imx219_cam0.json b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/jetson_mipi_camera_test_scenario_imx219_cam0.json index c1467fd7dc..93a4b26e7c 100644 --- a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/jetson_mipi_camera_test_scenario_imx219_cam0.json +++ b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/jetson_mipi_camera_test_scenario_imx219_cam0.json @@ -21,11 +21,11 @@ "camera_id": "0", "formats": ["NV12"], "resolutions": [ - {"width": 3280, "height": 2464, "mode": 0}, - {"width": 3280, "height": 1848, "mode": 1}, - {"width": 1920, "height": 1080, "mode": 2}, - {"width": 1640, "height": 1232, "mode": 3}, - {"width": 1280, "height": 720, "mode": 4} + {"width": 3280, "height": 2464, "fps": 21, "mode": 0}, + {"width": 3280, "height": 1848, "fps": 28, "mode": 1}, + {"width": 1920, "height": 1080, "fps": 30, "mode": 2}, + {"width": 1640, "height": 1232, "fps": 30, "mode": 3}, + {"width": 1280, "height": 720, "fps": 60, "mode": 4} ] } ], diff --git a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/jetson_mipi_camera_test_scenario_imx219_cam1.json b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/jetson_mipi_camera_test_scenario_imx219_cam1.json index a66f647dca..8bed548d02 100644 --- a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/jetson_mipi_camera_test_scenario_imx219_cam1.json +++ b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/jetson_mipi_camera_test_scenario_imx219_cam1.json @@ -21,11 +21,11 @@ "camera_id": "0", "formats": ["NV12"], "resolutions": [ - {"width": 3280, "height": 2464, "mode": 0}, - {"width": 3280, "height": 1848, "mode": 1}, - {"width": 1920, "height": 1080, "mode": 2}, - {"width": 1640, "height": 1232, "mode": 3}, - {"width": 1280, "height": 720, "mode": 4} + {"width": 3280, "height": 2464, "fps": 21, "mode": 0}, + {"width": 3280, "height": 1848, "fps": 28, "mode": 1}, + {"width": 1920, "height": 1080, "fps": 30, "mode": 2}, + {"width": 1640, "height": 1232, "fps": 30, "mode": 3}, + {"width": 1280, "height": 720, "fps": 60, "mode": 4} ] } ], diff --git a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/jetson_mipi_camera_test_scenario_imx274_dual.json b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/jetson_mipi_camera_test_scenario_imx274_dual.json index 1d5231e5bb..50265fa9cf 100644 --- a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/jetson_mipi_camera_test_scenario_imx274_dual.json +++ b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/jetson_mipi_camera_test_scenario_imx274_dual.json @@ -5,12 +5,30 @@ "method": "nvargus_nvraw", "physical_interface": "cam0", "camera_id": "0", - "formats": ["NVRAW"], + "formats": [ + "NVRAW" + ], "resolutions": [ - {"width": 3840, "height": 2160, "mode": 0}, - {"width": 1920, "height": 1080, "mode": 1}, - {"width": 3840, "height": 2160, "mode": 2}, - {"width": 1920, "height": 1080, "mode": 3} + { + "width": 3840, + "height": 2160, + "mode": 0 + }, + { + "width": 1920, + "height": 1080, + "mode": 1 + }, + { + "width": 3840, + "height": 2160, + "mode": 2 + }, + { + "width": 1920, + "height": 1080, + "mode": 3 + } ] }, { @@ -18,12 +36,30 @@ "method": "nvargus_nvraw", "physical_interface": "cam1", "camera_id": "1", - "formats": ["NVRAW"], + "formats": [ + "NVRAW" + ], "resolutions": [ - {"width": 3840, "height": 2160, "mode": 0}, - {"width": 1920, "height": 1080, "mode": 1}, - {"width": 3840, "height": 2160, "mode": 2}, - {"width": 1920, "height": 1080, "mode": 3} + { + "width": 3840, + "height": 2160, + "mode": 0 + }, + { + "width": 1920, + "height": 1080, + "mode": 1 + }, + { + "width": 3840, + "height": 2160, + "mode": 2 + }, + { + "width": 1920, + "height": 1080, + "mode": 3 + } ] }, { @@ -31,12 +67,34 @@ "method": "gstreamer", "physical_interface": "cam0", "camera_id": "0", - "formats": ["NV12"], + "formats": [ + "NV12" + ], "resolutions": [ - {"width": 3840, "height": 2160, "mode": 0}, - {"width": 1920, "height": 1080, "mode": 1}, - {"width": 3840, "height": 2160, "mode": 2}, - {"width": 1920, "height": 1080, "mode": 3} + { + "width": 3840, + "height": 2160, + "fps": 60, + "mode": 0 + }, + { + "width": 1920, + "height": 1080, + "fps": 60, + "mode": 1 + }, + { + "width": 3840, + "height": 2160, + "fps": 30, + "mode": 2 + }, + { + "width": 1920, + "height": 1080, + "fps": 60, + "mode": 3 + } ] }, { @@ -44,12 +102,34 @@ "method": "gstreamer", "physical_interface": "cam1", "camera_id": "1", - "formats": ["NV12"], + "formats": [ + "NV12" + ], "resolutions": [ - {"width": 3840, "height": 2160, "mode": 0}, - {"width": 1920, "height": 1080, "mode": 1}, - {"width": 3840, "height": 2160, "mode": 2}, - {"width": 1920, "height": 1080, "mode": 3} + { + "width": 3840, + "height": 2160, + "fps": 60, + "mode": 0 + }, + { + "width": 1920, + "height": 1080, + "fps": 60, + "mode": 1 + }, + { + "width": 3840, + "height": 2160, + "fps": 30, + "mode": 2 + }, + { + "width": 1920, + "height": 1080, + "fps": 60, + "mode": 3 + } ] } ], @@ -59,12 +139,34 @@ "method": "gstreamer", "physical_interface": "cam0", "camera_id": "0", - "formats": ["NV12"], + "formats": [ + "NV12" + ], "resolutions": [ - {"width": 3840, "height": 2160, "fps": 60, "mode": 0}, - {"width": 1920, "height": 1080, "fps": 60, "mode": 1}, - {"width": 3840, "height": 2160, "fps": 30, "mode": 2}, - {"width": 1920, "height": 1080, "fps": 60, "mode": 3} + { + "width": 3840, + "height": 2160, + "fps": 60, + "mode": 0 + }, + { + "width": 1920, + "height": 1080, + "fps": 60, + "mode": 1 + }, + { + "width": 3840, + "height": 2160, + "fps": 30, + "mode": 2 + }, + { + "width": 1920, + "height": 1080, + "fps": 60, + "mode": 3 + } ] }, { @@ -72,12 +174,34 @@ "method": "gstreamer", "physical_interface": "cam1", "camera_id": "1", - "formats": ["NV12"], + "formats": [ + "NV12" + ], "resolutions": [ - {"width": 3840, "height": 2160, "fps": 60, "mode": 0}, - {"width": 1920, "height": 1080, "fps": 60, "mode": 1}, - {"width": 3840, "height": 2160, "fps": 30, "mode": 2}, - {"width": 1920, "height": 1080, "fps": 60, "mode": 3} + { + "width": 3840, + "height": 2160, + "fps": 60, + "mode": 0 + }, + { + "width": 1920, + "height": 1080, + "fps": 60, + "mode": 1 + }, + { + "width": 3840, + "height": 2160, + "fps": 30, + "mode": 2 + }, + { + "width": 1920, + "height": 1080, + "fps": 60, + "mode": 3 + } ] } ] diff --git a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/tests/__pycache__/test_camera_utils.cpython-312.pyc b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/tests/__pycache__/test_camera_utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1452b41dd14a1330627d020efc0e5b23dfdada1a GIT binary patch literal 4174 zcmcInK};Lj8GbV!kH^LY2$TYw7&jp)CIO6-W`U%uXuF$k)W(&%Ri))Jot^hELu`-x zX3PR|q*O|^L_KWOONf*Ms~*ZKJ>=M9@4etI5$?2yNL5vFsc1+qIrabFc+9{8MP2nt z{^!lS|NZZO?|t9@|NdipdxAjw!9PBlUuYxbZ}<|lXtT3hfzD$>36+Xu2A^_KD#cMLj@!Sswo$&5^;nFy61kQoKtn~72dq)MY8V^jsH zEyXgL*Y^|VxChMNc3H_;o(`5v7g!lqcP+a-oi^=K#da7?m%*j9QKo5A_@-MlZ7(p+ zEy~8sz$bJ~gC86$*?kWL9HK;J8leh}79^_D7}cK2(2GA+@5wcJK0FijvDKt!&94%v z$sdwTBJZhIX)!l3<;4q*%Ncs$Dd0%B`)aW0^)#x5_T#vxdpM&V{SR zoKD$-WilNP!Hdn>ykxkJ*T&(3id8%)2X0NRR;b~!OvFYnfMBOXU_OM za~->^`_7M>cG>0D$E^vYCL6U`Y45$l?FG0c9t^mE0B0O!(}WINC^}=6B`>O@qsytI zKr$xAR!0i)Zk&Zd(^E<|Wf_g*O?kb#S>>FSUEMKR+2EEf=)qH{{uXMq8IHra`~J_X zM$wBDS=sB*kun(51dxD2zDnM9Z{!N?~4|E()<;=7>4Vc3RaQs3Ox}lu`KWB|< z(S@i44q3AnO+8mfX_%>RR8^?$wXB&R(mZz4m4Q+rRJH)UG$W2DZ9J>Rlu26ZNjq z6}1tlHj<|wU3-|{IP>o7h#XJss6Z9p2#joWU#KT9ZVZ2AY_)f-_k}jh&wzLPScIWh zu;|xt(R4GWm(*o>a$foh{BIXa_Sq%bvD0Cl+=Myt55v+M6!`cMH zB1bSRLX(0w?IAmddQ^+hxX=u-=SZ8Ugr<4|n;k+k#BX3^XWmQth}JP8+n$u=7-e`n zh;1a!HnQ^zg`dK&NOCVSTXFa~{3?5KKjZ_-Il|Lt<{J zsdYBQak9Vvjrkd*(sm09o?TTKU{9~_P-oRtq(otf0(FuzP}TZ z`%mpeN$0>WiHcm6B<)=rUFSEFLxSX~x|Ui~Ru?w4i(iZ0d7s`|i{7if8|hrV|H|_Z zH?^r&H~?a+Pg3g;GA@O`MW38qmtSa?jv@R%Vb}jB!k>id(maNq1C5Hrbq@*JN~?P+ zE!u@Ba-?QLkg08tc0i_N-cL27VAI+F8d+g`lAFvf z=B{45D)8v%y^MO04I;B42YgCgh28<&4<3?Q>?1jGlb;&t#`6bJ7ta2e#iRjK< z3nH$ltgjz`|Hn@LA^72-I1&QCs;wxEWM?RV`BjR4+d+P~Egu_yQZ-51*1;vVyv zxToo&eA9I=p(6{bMF#z(u4DCv)grvbbe-C!uKUSh1kdF=D5g+c2k`|F_CylkSf2}l zf0G>~&ja#j<-J!*BHHt+uRWT1mFkIJes#Gsn%EtRMJN5KI18OA8CF@>GcixAmMs^L z)=M_ipZL4t7onPeo=|3r)o$Qq1*h_x(A{(9jdsJ|(>weY3}7MbAfcYgV!od{6|gzf(TcT>6F literal 0 HcmV?d00001 diff --git a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/tests/test_camera_utils.py b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/tests/test_camera_utils.py index 547ecf2921..467a094641 100644 --- a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/tests/test_camera_utils.py +++ b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/tests/test_camera_utils.py @@ -55,6 +55,24 @@ def test_record_video_camera_id_override_and_mode(self): self.assertEqual(record["mode"], 2) self.assertEqual(record["fps"], 30) + def test_capture_image_optional_fps(self): + resources = CameraResources() + resources._current_scenario_name = "capture_image" + item = self._item( + camera_id="0", + resolutions=[ + {"width": 3280, "height": 2464, "fps": 21, "mode": 0} + ], + ) + resources.capture_image([item]) + + record = resources._resource_items[0] + # capture fps pins slow sensor modes but stays out of the job name + self.assertEqual(record["fps"], 21) + self.assertEqual( + record["name"], "imx219_cam0_gstreamer_3280x2464_mode0_NV12" + ) + def test_missing_identifier_skips_item(self): resources = CameraResources() resources._current_scenario_name = "capture_image" diff --git a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/camera/jobs.pxu b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/camera/jobs.pxu index 0faf5cc8a6..1dd4d449ae 100644 --- a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/camera/jobs.pxu +++ b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/camera/jobs.pxu @@ -30,6 +30,7 @@ _template-summary: MIPI Camera Captures Image _summary: {{ camera }} captures {{ width }}x{{ height }} {{ format }} images via {{ method }} _description: Test {{ camera }} sensor on the {{ physical_interface }} slot can capture {{ width }}x{{ height }} {{ format }} images via {{ method }} method plugin: shell +user: root category_id: com.canonical.plainbox::camera estimated_duration: 10s flags: also-after-suspend @@ -39,9 +40,9 @@ imports: from com.canonical.plainbox import manifest requires: manifest.has_vendor_specific_mipi_camera == "True" -environ: PLATFORM_NAME MIPI_CAMERA_SETUP_CONF_FILE_PATH GST_LAUNCH_BIN MEDIA_CTL_CMD V4L2_CTL_CMD GST_PLUGIN_PATH GST_PLUGIN_SYSTEM_PATH GST_PLUGIN_SCANNER +environ: PLATFORM_NAME MIPI_CAMERA_SETUP_CONF_FILE_PATH GST_LAUNCH_BIN NVARGUS_NVRAW_BIN MEDIA_CTL_CMD V4L2_CTL_CMD GST_PLUGIN_PATH GST_PLUGIN_SYSTEM_PATH GST_PLUGIN_SCANNER command: - camera_test.py testing -sn {{ scenario }} -p "$PLATFORM_NAME" -c {{ camera }} -pi {{ physical_interface }} -m {{ method }} -wi {{ width }} -hi {{ height }} -f {{ format }} -vdn "{{ v4l2_device_name }}" -cscf "$MIPI_CAMERA_SETUP_CONF_FILE_PATH"{% if mode is defined %} -md {{ mode }}{% endif %} + camera_test.py testing -sn {{ scenario }} -p "$PLATFORM_NAME" -c {{ camera }} -pi {{ physical_interface }} -m {{ method }} -wi {{ width }} -hi {{ height }} -f {{ format }} -vdn "{{ v4l2_device_name }}" -cscf "$MIPI_CAMERA_SETUP_CONF_FILE_PATH"{% if mode is defined %} -md {{ mode }}{% endif %}{% if fps is defined %} -fps {{ fps }}{% endif %} unit: template template-engine: jinja2 @@ -55,6 +56,7 @@ _template-summary: MIPI Camera Records Video _summary: {{ camera }} records a {{ width }}x{{ height }}@{{ fps }}fps {{ format }} video via {{ method }} _description: Test {{ camera }} sensor on the {{ physical_interface }} slot can record {{ width }}x{{ height }}@{{ fps }}fps {{ format }} video via {{ method }} method plugin: shell +user: root category_id: com.canonical.plainbox::camera estimated_duration: 10s flags: also-after-suspend @@ -64,6 +66,6 @@ imports: from com.canonical.plainbox import manifest requires: manifest.has_vendor_specific_mipi_camera == "True" -environ: PLATFORM_NAME MIPI_CAMERA_SETUP_CONF_FILE_PATH GST_LAUNCH_BIN MEDIA_CTL_CMD V4L2_CTL_CMD GST_PLUGIN_PATH GST_PLUGIN_SYSTEM_PATH GST_PLUGIN_SCANNER +environ: PLATFORM_NAME MIPI_CAMERA_SETUP_CONF_FILE_PATH GST_LAUNCH_BIN NVARGUS_NVRAW_BIN MEDIA_CTL_CMD V4L2_CTL_CMD GST_PLUGIN_PATH GST_PLUGIN_SYSTEM_PATH GST_PLUGIN_SCANNER command: camera_test.py testing -sn {{ scenario }} -p "$PLATFORM_NAME" -c {{ camera }} -pi {{ physical_interface }} -m {{ method }} -wi {{ width }} -hi {{ height }} -f {{ format }} -vdn "{{ v4l2_device_name }}" -cscf "$MIPI_CAMERA_SETUP_CONF_FILE_PATH" -fps {{ fps }}{% if mode is defined %} -md {{ mode }}{% endif %} From 86b8d120dcddd8ca3d47e37448b15e43b5438c31 Mon Sep 17 00:00:00 2001 From: Isaac Yang Date: Fri, 17 Jul 2026 16:32:22 +0800 Subject: [PATCH 05/10] Document snap wrapper GST env preset caveat Snap-packaged checkbox pre-exports GST_PLUGIN_SYSTEM_PATH and GST_PLUGIN_SCANNER in its wrapper, and checkbox only injects config environment values for variables not already set, so GST_PLUGIN_* overrides take no effect under a checkbox snap (verified on checkbox 7.3.0 on the Orin Nano DUT). GST_LAUNCH_BIN and NVARGUS_NVRAW_BIN are not preset and always inject, which makes the multimedia-snap aliases the supported route on Ubuntu Core. --- .../Test_Scenario_and_Test_Setup.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/Test_Scenario_and_Test_Setup.md b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/Test_Scenario_and_Test_Setup.md index f725d807b3..54e6c94218 100644 --- a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/Test_Scenario_and_Test_Setup.md +++ b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/Test_Scenario_and_Test_Setup.md @@ -56,6 +56,16 @@ GST_LAUNCH_BIN=/snap/bin/gst-launch-1.0 NVARGUS_NVRAW_BIN=/snap/bin/nvargus_nvraw ``` +> **Note:** snap-packaged checkbox pre-exports `GST_PLUGIN_SYSTEM_PATH` and +> `GST_PLUGIN_SCANNER` in its wrapper, and checkbox only injects config +> `[environment]` values for variables that are not already set — so +> `GST_PLUGIN_*` overrides take no effect under a checkbox snap (verified on +> checkbox 7.3.0). This is one more reason the multimedia-snap aliases are +> the supported route on Ubuntu Core: `GST_LAUNCH_BIN` and +> `NVARGUS_NVRAW_BIN` are not preset, always inject, and the aliased tools +> run inside the multimedia snap where its own plugin paths and Argus +> socket apply. + `DISPLAY` is unset by the test itself (Argus tries to bring up an EGL preview when it is set, which wedges headless runs), so nothing is needed in the configuration for it. From 6bce22add9f8a781708838a4f844d4d8d404fd90 Mon Sep 17 00:00:00 2001 From: Isaac Yang Date: Fri, 17 Jul 2026 16:37:36 +0800 Subject: [PATCH 06/10] Remove committed __pycache__ and gitignore it Three .pyc files slipped into the previous commit; byte-code caches do not belong in the repository. Ignore __pycache__ globally so it cannot recur. --- .gitignore | 2 +- .../__pycache__/camera_jetson.cpython-312.pyc | Bin 13839 -> 0 bytes .../__pycache__/camera_utils.cpython-312.pyc | Bin 44984 -> 0 bytes .../test_camera_utils.cpython-312.pyc | Bin 4174 -> 0 bytes 4 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 contrib/checkbox-ce-oem/checkbox-provider-ce-oem/bin/__pycache__/camera_jetson.cpython-312.pyc delete mode 100644 contrib/checkbox-ce-oem/checkbox-provider-ce-oem/bin/__pycache__/camera_utils.cpython-312.pyc delete mode 100644 contrib/checkbox-ce-oem/checkbox-provider-ce-oem/tests/__pycache__/test_camera_utils.cpython-312.pyc diff --git a/.gitignore b/.gitignore index c317f6dc81..8c3df1617b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ node_modules -venv \ No newline at end of file +venv__pycache__/ diff --git a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/bin/__pycache__/camera_jetson.cpython-312.pyc b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/bin/__pycache__/camera_jetson.cpython-312.pyc deleted file mode 100644 index 49a1c7ccb8114c1e9c9c1b6fe4f22a20fd6de209..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13839 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

+zUEPw_6@9w_>vF}GZYJo?c0 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 From 521cabd9c25b35f20bd242db9c0be569a02ada01 Mon Sep 17 00:00:00 2001 From: Isaac Yang Date: Fri, 17 Jul 2026 16:37:36 +0800 Subject: [PATCH 07/10] Resolve nvargus_nvraw from PATH without env override Isaac: the capture tools are guaranteed by the pre-test setup (the multimedia snap aliases on Ubuntu Core, native binaries on classic), so the NVARGUS_NVRAW_BIN override knob is unnecessary. Resolve nvargus_nvraw from PATH directly and drop the variable from the jobs' environ list and the docs. --- .../bin/camera_jetson.py | 6 +++--- .../Test_Scenario_and_Test_Setup.md | 18 +++++++++--------- .../units/camera/jobs.pxu | 4 ++-- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/bin/camera_jetson.py b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/bin/camera_jetson.py index 79f6173985..0d09da7140 100755 --- a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/bin/camera_jetson.py +++ b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/bin/camera_jetson.py @@ -35,9 +35,9 @@ logger = logging.getLogger(__name__) -NVARGUS_NVRAW_BIN = shutil.which( - os.getenv("NVARGUS_NVRAW_BIN", "nvargus_nvraw") -) +# Resolved from PATH: /snap/bin/nvargus_nvraw (the multimedia snap alias set +# up before testing on Ubuntu Core) or /usr/bin / /usr/sbin on classic images +NVARGUS_NVRAW_BIN = shutil.which("nvargus_nvraw") # DISPLAY must be unset for every Argus capture (both nvargus_nvraw and # nvarguscamerasrc try to bring up an EGL preview when it is set, which diff --git a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/Test_Scenario_and_Test_Setup.md b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/Test_Scenario_and_Test_Setup.md index 54e6c94218..d7356978d9 100644 --- a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/Test_Scenario_and_Test_Setup.md +++ b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/Test_Scenario_and_Test_Setup.md @@ -25,8 +25,9 @@ configurations. ## Required Checkbox Environment The capture tools (`gst-launch-1.0` with the NVIDIA plugins, and -`nvargus_nvraw`) must be reachable from the checkbox jobs. The jobs pass -`GST_LAUNCH_BIN`, `NVARGUS_NVRAW_BIN`, `GST_PLUGIN_PATH`, +`nvargus_nvraw`) must be reachable from the checkbox jobs — making them so +is a pre-test setup step, not the test's job. `nvargus_nvraw` is resolved +from `PATH`; the jobs pass `GST_LAUNCH_BIN`, `GST_PLUGIN_PATH`, `GST_PLUGIN_SYSTEM_PATH` and `GST_PLUGIN_SCANNER` through from the checkbox configuration, so each image type supplies what it needs: @@ -48,12 +49,12 @@ sudo snap alias .nvargus-nvraw nvargus_nvraw ``` The multimedia snap resolves its own plugin paths, so no `GST_PLUGIN_*` -variables are needed. If the aliases are not on the job `PATH`, point the -tool overrides at them instead: +variables are needed. The aliases land in `/snap/bin`, which is on `PATH`, +so `nvargus_nvraw` resolves without configuration; if the job environment +misses it for gst, point the override at the alias: ```ini GST_LAUNCH_BIN=/snap/bin/gst-launch-1.0 -NVARGUS_NVRAW_BIN=/snap/bin/nvargus_nvraw ``` > **Note:** snap-packaged checkbox pre-exports `GST_PLUGIN_SYSTEM_PATH` and @@ -61,10 +62,9 @@ NVARGUS_NVRAW_BIN=/snap/bin/nvargus_nvraw > `[environment]` values for variables that are not already set — so > `GST_PLUGIN_*` overrides take no effect under a checkbox snap (verified on > checkbox 7.3.0). This is one more reason the multimedia-snap aliases are -> the supported route on Ubuntu Core: `GST_LAUNCH_BIN` and -> `NVARGUS_NVRAW_BIN` are not preset, always inject, and the aliased tools -> run inside the multimedia snap where its own plugin paths and Argus -> socket apply. +> the supported route on Ubuntu Core: the aliased tools run inside the +> multimedia snap where its own plugin paths and Argus socket apply, and +> `GST_LAUNCH_BIN` (not preset, always injects) can point at the alias. `DISPLAY` is unset by the test itself (Argus tries to bring up an EGL preview when it is set, which wedges headless runs), so nothing is needed diff --git a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/camera/jobs.pxu b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/camera/jobs.pxu index 1dd4d449ae..acf621bae5 100644 --- a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/camera/jobs.pxu +++ b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/camera/jobs.pxu @@ -40,7 +40,7 @@ imports: from com.canonical.plainbox import manifest requires: manifest.has_vendor_specific_mipi_camera == "True" -environ: PLATFORM_NAME MIPI_CAMERA_SETUP_CONF_FILE_PATH GST_LAUNCH_BIN NVARGUS_NVRAW_BIN MEDIA_CTL_CMD V4L2_CTL_CMD GST_PLUGIN_PATH GST_PLUGIN_SYSTEM_PATH GST_PLUGIN_SCANNER +environ: PLATFORM_NAME MIPI_CAMERA_SETUP_CONF_FILE_PATH GST_LAUNCH_BIN MEDIA_CTL_CMD V4L2_CTL_CMD GST_PLUGIN_PATH GST_PLUGIN_SYSTEM_PATH GST_PLUGIN_SCANNER command: camera_test.py testing -sn {{ scenario }} -p "$PLATFORM_NAME" -c {{ camera }} -pi {{ physical_interface }} -m {{ method }} -wi {{ width }} -hi {{ height }} -f {{ format }} -vdn "{{ v4l2_device_name }}" -cscf "$MIPI_CAMERA_SETUP_CONF_FILE_PATH"{% if mode is defined %} -md {{ mode }}{% endif %}{% if fps is defined %} -fps {{ fps }}{% endif %} @@ -66,6 +66,6 @@ imports: from com.canonical.plainbox import manifest requires: manifest.has_vendor_specific_mipi_camera == "True" -environ: PLATFORM_NAME MIPI_CAMERA_SETUP_CONF_FILE_PATH GST_LAUNCH_BIN NVARGUS_NVRAW_BIN MEDIA_CTL_CMD V4L2_CTL_CMD GST_PLUGIN_PATH GST_PLUGIN_SYSTEM_PATH GST_PLUGIN_SCANNER +environ: PLATFORM_NAME MIPI_CAMERA_SETUP_CONF_FILE_PATH GST_LAUNCH_BIN MEDIA_CTL_CMD V4L2_CTL_CMD GST_PLUGIN_PATH GST_PLUGIN_SYSTEM_PATH GST_PLUGIN_SCANNER command: camera_test.py testing -sn {{ scenario }} -p "$PLATFORM_NAME" -c {{ camera }} -pi {{ physical_interface }} -m {{ method }} -wi {{ width }} -hi {{ height }} -f {{ format }} -vdn "{{ v4l2_device_name }}" -cscf "$MIPI_CAMERA_SETUP_CONF_FILE_PATH" -fps {{ fps }}{% if mode is defined %} -md {{ mode }}{% endif %} From 5a1bee7506990189e44cb88c2fe656b59076cba8 Mon Sep 17 00:00:00 2001 From: Isaac Yang Date: Fri, 17 Jul 2026 16:38:57 +0800 Subject: [PATCH 08/10] Add classic GST_LAUNCH_BIN example to Jetson doc --- .../Test_Scenario_and_Test_Setup.md | 1 + 1 file changed, 1 insertion(+) diff --git a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/Test_Scenario_and_Test_Setup.md b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/Test_Scenario_and_Test_Setup.md index d7356978d9..09e0aa783a 100644 --- a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/Test_Scenario_and_Test_Setup.md +++ b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/Test_Scenario_and_Test_Setup.md @@ -35,6 +35,7 @@ configuration, so each image type supplies what it needs: search path: ```ini +GST_LAUNCH_BIN=/usr/bin/gst-launch-1.0 GST_PLUGIN_PATH=/usr/lib/aarch64-linux-gnu/gstreamer-1.0/ ``` From 4fa847281ea20d59264f67125c9e8136af532385 Mon Sep 17 00:00:00 2001 From: Isaac Yang Date: Sat, 18 Jul 2026 12:05:54 +0800 Subject: [PATCH 09/10] Drop user:root from jetson-core camera templates The three jetson-core-camera/* (snap-path) templates carried user: root to reach the Tegra camera nodes (root:video 0660, runner not in the video group). Rootless capture is now proven on the IGX UC22 DUT, so the elevation is no longer needed and the jobs run as the invoking user. The deb-path jetson-camera/* templates never set user: root and are left untouched. DEPENDENCY: rootless operation is not self-contained in this provider. It requires both, shipped by the image/gadget, not by checkbox or the snap: (a) the runner user holding an active seat (autologin console session), so logind uaccess ACLs apply; and (b) a uaccess udev rule tagging the Tegra camera nodes (video4linux, nvmap, nvhost-*, capture-isp/vi-channel*, tegra_camera_ctrl, nvidia-gpu-v2*) with an explicit RUN{builtin}+="uaccess" fallback, because a 99- rule lands after the stock uaccess ACL builtin at 73-seat-late.rules. DUTs lacking either (e.g. AGX Orin / Orin Nano images without the rule) will fail these jobs rootless. Ref: OEMQA-6807, OEMQA-6806. --- .../checkbox-provider-ce-oem/units/Jetson/camera_job.pxu | 3 --- 1 file changed, 3 deletions(-) diff --git a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/Jetson/camera_job.pxu b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/Jetson/camera_job.pxu index 865386c393..a11420c762 100644 --- a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/Jetson/camera_job.pxu +++ b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/Jetson/camera_job.pxu @@ -39,7 +39,6 @@ _summary: Capture an image using nvargus_nvraw (sensor{source_index}) plugin: shell category_id: jetson-core-camera flags: also-after-suspend -user: root imports: from com.canonical.certification import executable from com.canonical.plainbox import manifest @@ -87,7 +86,6 @@ _summary: Capture a JPEG image via gstreamer (sensor{source_index}) plugin: shell category_id: jetson-core-camera flags: also-after-suspend -user: root imports: from com.canonical.certification import device from com.canonical.plainbox import manifest @@ -140,7 +138,6 @@ _summary: Record an H265 video via gstreamer (sensor{source_index}) plugin: shell category_id: jetson-core-camera flags: also-after-suspend -user: root imports: from com.canonical.certification import device from com.canonical.contrib import jetson_tegra_details From 255fddffe37fb7d98d5d10d132a985d7bf607a37 Mon Sep 17 00:00:00 2001 From: Isaac Yang Date: Sat, 18 Jul 2026 12:31:35 +0800 Subject: [PATCH 10/10] Scope rootless change to generic camera jobs Author decision: the jetson-core-camera templates keep user: root because rootless capture on Jetson depends on an image-level udev uaccess rule not yet shipped (see OEMQA-6807 follow-up; PR comment 2026-07-18 has the evidence). The generic ce-oem-mipi-camera jobs drop user: root instead. Restores the three user: root lines in units/Jetson/camera_job.pxu (reverting 4fa847281) and removes the two user: root lines from units/camera/jobs.pxu. Ref: OEMQA-6807. --- .../checkbox-provider-ce-oem/units/Jetson/camera_job.pxu | 3 +++ .../checkbox-provider-ce-oem/units/camera/jobs.pxu | 2 -- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/Jetson/camera_job.pxu b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/Jetson/camera_job.pxu index a11420c762..865386c393 100644 --- a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/Jetson/camera_job.pxu +++ b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/Jetson/camera_job.pxu @@ -39,6 +39,7 @@ _summary: Capture an image using nvargus_nvraw (sensor{source_index}) plugin: shell category_id: jetson-core-camera flags: also-after-suspend +user: root imports: from com.canonical.certification import executable from com.canonical.plainbox import manifest @@ -86,6 +87,7 @@ _summary: Capture a JPEG image via gstreamer (sensor{source_index}) plugin: shell category_id: jetson-core-camera flags: also-after-suspend +user: root imports: from com.canonical.certification import device from com.canonical.plainbox import manifest @@ -138,6 +140,7 @@ _summary: Record an H265 video via gstreamer (sensor{source_index}) plugin: shell category_id: jetson-core-camera flags: also-after-suspend +user: root imports: from com.canonical.certification import device from com.canonical.contrib import jetson_tegra_details diff --git a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/camera/jobs.pxu b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/camera/jobs.pxu index acf621bae5..3034d3b178 100644 --- a/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/camera/jobs.pxu +++ b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/units/camera/jobs.pxu @@ -30,7 +30,6 @@ _template-summary: MIPI Camera Captures Image _summary: {{ camera }} captures {{ width }}x{{ height }} {{ format }} images via {{ method }} _description: Test {{ camera }} sensor on the {{ physical_interface }} slot can capture {{ width }}x{{ height }} {{ format }} images via {{ method }} method plugin: shell -user: root category_id: com.canonical.plainbox::camera estimated_duration: 10s flags: also-after-suspend @@ -56,7 +55,6 @@ _template-summary: MIPI Camera Records Video _summary: {{ camera }} records a {{ width }}x{{ height }}@{{ fps }}fps {{ format }} video via {{ method }} _description: Test {{ camera }} sensor on the {{ physical_interface }} slot can record {{ width }}x{{ height }}@{{ fps }}fps {{ format }} video via {{ method }} method plugin: shell -user: root category_id: com.canonical.plainbox::camera estimated_duration: 10s flags: also-after-suspend