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/camera_jetson.py b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/bin/camera_jetson.py new file mode 100755 index 0000000000..0d09da7140 --- /dev/null +++ b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/bin/camera_jetson.py @@ -0,0 +1,396 @@ +#!/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, + CameraOperationError, + log_and_raise_error, +) + +logger = logging.getLogger(__name__) + +# 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 +# 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 +# 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 + + +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 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 + 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 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, + ) + + 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, + 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( + 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 = [ENV_PREFIX, 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, + framerate: 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: + # 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, + ) + 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 + ) + 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) + + 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) + + self._execute_gst_cmd(cmd, GST_VIDEO_TIMEOUT, full_artifact_path) + + +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..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 @@ -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,13 @@ def _execute_capture_image_scenario( artifact_store_path: Path to store artifacts artifact_name: Base name for artifacts """ + # 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): logger.info("\n\n===== Iteration {} =====\n".format(i)) @@ -127,6 +142,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 +161,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 +175,7 @@ def _execute_record_video_scenario( artifact_name=artifact_name, method=args.method, v4l2_device_name=args.v4l2_device_name, + **extra ) @@ -326,6 +347,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..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 @@ -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 @@ -163,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), @@ -188,6 +200,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), @@ -1056,6 +1072,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: @@ -1071,7 +1112,6 @@ def _process_scenario_items( "camera", "method", "physical_interface", - "v4l2_device_name", "resolutions", "formats", ] @@ -1081,25 +1121,50 @@ 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"], } - # 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 + 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..09e0aa783a --- /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,158 @@ +# 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:** 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 capture tools (`gst-launch-1.0` with the NVIDIA plugins, and +`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: + +**Classic / deb images** — the NVIDIA GStreamer plugins are off the default +search path: + +```ini +GST_LAUNCH_BIN=/usr/bin/gst-launch-1.0 +GST_PLUGIN_PATH=/usr/lib/aarch64-linux-gnu/gstreamer-1.0/ +``` + +**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. 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 +``` + +> **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: 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 +in the configuration for it. + +## 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 (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 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 | +| --- | --- | --- | +| 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_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 new file mode 100644 index 0000000000..93a4b26e7c --- /dev/null +++ b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/data/Jetson-MIPI-Camera-TestScenario-TestSetup/jetson_mipi_camera_test_scenario_imx219_cam0.json @@ -0,0 +1,48 @@ +{ + "capture_image": [ + { + "camera": "imx219", + "method": "nvargus_nvraw", + "physical_interface": "cam0", + "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": "cam0", + "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} + ] + } + ], + "record_video": [ + { + "camera": "imx219", + "method": "gstreamer", + "physical_interface": "cam0", + "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_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..8bed548d02 --- /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, "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} + ] + } + ], + "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 new file mode 100644 index 0000000000..50265fa9cf --- /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,208 @@ +{ + "capture_image": [ + { + "camera": "imx274", + "method": "nvargus_nvraw", + "physical_interface": "cam0", + "camera_id": "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", + "camera_id": "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", + "camera_id": "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", + "camera_id": "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 + } + ] + } + ], + "record_video": [ + { + "camera": "imx274", + "method": "gstreamer", + "physical_interface": "cam0", + "camera_id": "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", + "camera_id": "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/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..467a094641 --- /dev/null +++ b/contrib/checkbox-ce-oem/checkbox-provider-ce-oem/tests/test_camera_utils.py @@ -0,0 +1,87 @@ +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_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" + 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 d400f7467d..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 @@ -407,6 +430,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..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 @@ -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_{{ 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,9 +39,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 +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" + 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 @@ -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_{{ 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 }} + 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 %} 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