From 6e2551e9170b57a33f9f92b6953e7ef9a9f01602 Mon Sep 17 00:00:00 2001 From: Jiwen Cai Date: Fri, 4 Sep 2026 04:04:20 +0000 Subject: [PATCH] sensing: add host and container setup for the SENSING SG8A SIPL rig Provisioning for the SENSING SG8A-AGON-G2Y-A1 carrier with two Astra SHW5G modules on JetPack 7.2.1 / L4T R39.2.1. setup.sh dispatches to a host half (fetch and install the vendor package, overlay, groups) and a container half (device access, build SDKs); verify.sh reports on either side. SENSING publishes driver packages as directories in a git repo with no releases, so the host half takes a blobless, shallow, sparse clone of just this board's tree -- 3.6 MB of a ~700 MB repo -- into a gitignored .cache/. That package is what puts nvsipl_camera on PATH. It defines no SHW5G_2 platform config, only mixed populations, so configs/shw5g.json vendors the one SENSING ships for a 2x SHW5G rig and setup drops it in. Two container facts the scripts exist to make discoverable, since SIPL reports both as a bare "SetPlatformConfig ... status: 10": - Access needs groups, not root: every node SIPL opens is 0660 root:, so --group-add AND the matching gids in the image are both required -- docker exec reads the container's own /etc/group and ignores HostConfig.GroupAdd. - The NVIDIA container runtime mounts a fixed CSV file list, so files the vendor installer adds stay invisible until their directories are bind-mounted. docker/ carries both halves, importable by another Dockerfile. Signed-off-by: Jiwen Cai --- docs/source/device/camera/sensing.rst | 424 +++++++++++++++ docs/source/device/cameras/index.rst | 1 + src/plugins/sensing/.gitignore | 4 + src/plugins/sensing/AGENTS.md | 13 + src/plugins/sensing/README.md | 40 ++ src/plugins/sensing/configs/shw5g.json | 259 ++++++++++ src/plugins/sensing/docker/Dockerfile.example | 35 ++ src/plugins/sensing/docker/image_setup.sh | 38 ++ src/plugins/sensing/docker/setup_container.sh | 255 +++++++++ src/plugins/sensing/scripts/common.sh | 489 ++++++++++++++++++ .../sensing/scripts/run_docker_example.sh | 136 +++++ src/plugins/sensing/scripts/setup.sh | 265 ++++++++++ src/plugins/sensing/scripts/validate.sh | 74 +++ src/plugins/sensing/scripts/verify.sh | 133 +++++ 14 files changed, 2166 insertions(+) create mode 100644 docs/source/device/camera/sensing.rst create mode 100644 src/plugins/sensing/.gitignore create mode 100644 src/plugins/sensing/AGENTS.md create mode 100644 src/plugins/sensing/README.md create mode 100644 src/plugins/sensing/configs/shw5g.json create mode 100644 src/plugins/sensing/docker/Dockerfile.example create mode 100755 src/plugins/sensing/docker/image_setup.sh create mode 100755 src/plugins/sensing/docker/setup_container.sh create mode 100644 src/plugins/sensing/scripts/common.sh create mode 100755 src/plugins/sensing/scripts/run_docker_example.sh create mode 100755 src/plugins/sensing/scripts/setup.sh create mode 100755 src/plugins/sensing/scripts/validate.sh create mode 100755 src/plugins/sensing/scripts/verify.sh diff --git a/docs/source/device/camera/sensing.rst b/docs/source/device/camera/sensing.rst new file mode 100644 index 0000000000..807389185a --- /dev/null +++ b/docs/source/device/camera/sensing.rst @@ -0,0 +1,424 @@ +.. SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +.. SPDX-License-Identifier: Apache-2.0 + +SENSING GMSL Camera +=================== + +Bring-up for SENSING GMSL2 cameras on a Jetson AGX Orin, where capture goes +through **SIPL** rather than Argus or V4L2. This page covers provisioning the +rig and confirming it streams with the vendor's own tool; scripts live in +:code-dir:`src/plugins/sensing`. + +.. contents:: On this page + :local: + :depth: 2 + +Supported hardware +------------------ + +This plugin is developed and verified against exactly one configuration: + +.. list-table:: + :widths: 30 70 + :header-rows: 0 + + * - Host + - NVIDIA Jetson AGX Orin (``p3737-0000 + p3701-0005``, 64 GB) + * - Software + - JetPack 7.2.1 / L4T R39.2.1 + * - Carrier + - SENSING **SG8A-AGON-G2Y-A1** (GMSL2) + * - Cameras + - Two Astra **SHW5G** (VB1940), 2560×1984 @ 60 fps, RAW10 → ISP + * - Platform config + - ``SHW5G_2`` on CN1 CAM2/CAM3, link masks ``0x0000 0x1100`` + * - Device tree + - ``Jetson Sensing SG8A-AGON-G2Y-A1 SIPL GMSL2x8`` + +The carrier also takes S56C and SHF3L modules, and the vendor package ships +``S56C_1_SHF3L_2`` and ``SHF3L_2_SHW5G_2`` configs for them. Those remain +*addressable* — the config name and JSON path are arguments — but nothing is +built or tested for them. They would also need an ICP passthrough path, since +the SHF3L modules are YUV422 sensors that bypass the ISP entirely. + +Drivers come from the vendor SIPL package, which supplies userspace device +drivers (``libnvuddf_*.so``), a device-tree overlay, NITO ISP tuning files and +the ``nvsipl_camera`` reference application. The scripts below wrap it; they do +not replace it. + +SENSING publishes it in `nvidia-jetson-camera-drivers +`_ as a +directory rather than a release, so ``setup.sh`` takes a blobless, shallow, +sparse clone of just this board's tree — 3.6 MB of a ~700 MB repo — into a +gitignored ``.cache/``. Nothing needs sourcing by hand. + +What SIPL changes +----------------- + +If you know the previous Argus-based rig, almost every operational assumption +moved: + +.. list-table:: + :widths: 22 39 39 + :header-rows: 1 + + * - + - Argus (SG10A) + - SIPL (SG8A) + * - Sensor drivers + - kernel ``.ko``, loaded every boot + - userspace ``.so`` in ``/usr/lib/nvsipl_drv`` + * - Device nodes + - ``/dev/video0..9`` + - none + * - Arbitration + - ``nvargus-daemon``, multi-client + - in-process, **exclusive** + * - Per-boot step + - ``load_modules.sh`` + systemd unit + - none + * - Frame sync + - PWM trigger + J19 strap + - deserializer ``fsyncMode: osc_manual`` + * - Geometry + - ``--width``/``--height``/``--sensor-mode`` + - read from the platform config + * - Privilege + - container user + ``/tmp/argus_socket`` + - container user + two groups + +.. warning:: + + SIPL takes **exclusive** ownership of the hardware. ``nvsipl_camera`` and + ``camera_plugin_sensing`` cannot run at the same time; stop one before + starting the other. There is no daemon to arbitrate between them. + +Setup +----- + +Drivers are installed on the **host**, always — a container cannot do it, and +does not need to. Whether you then run the camera on the host or in a container +only decides what comes after: + +.. code-block:: bash + + # 1. On the Jetson, once: install the drivers. + src/plugins/sensing/scripts/setup.sh + + # 2. Confirm the rig streams. Same script on the host and in a container. + src/plugins/sensing/scripts/validate.sh + + # 3. Only if you want a container: + src/plugins/sensing/scripts/run_docker_example.sh # ready-made, drops you at a shell + src/plugins/sensing/docker/setup_container.sh # or adapt your own + +:code-dir:`scripts/ ` holds what you run — +on the host, or on either side in the case of ``validate.sh`` and +``verify.sh``. :code-dir:`docker/ ` holds what +runs *inside* a container: the example image and the two scripts that only +make sense there. ``verify.sh`` is a read-only report and is what to +paste into a bug. Every script names the privileged actions it will take before +the first ``sudo`` prompt and asks before each optional one; ``--yes`` accepts +them all, and a non-interactive stdin declines them all. + +.. note:: + + First install only: after the drivers land you must select the device-tree + overlay and reboot before anything streams. ``setup.sh`` stops and says + so. That is the one step no script can do for you. + +Host +~~~~ + +.. code-block:: bash + + src/plugins/sensing/scripts/setup.sh + +It refreshes the vendor package, compares every file ``install.sh`` would copy +against what is installed, and offers to run it when anything is missing *or +stale*. ``--pkg DIR`` uses a package you already have, ``--no-fetch`` forbids +the download, and ``--install-drivers`` reinstalls unconditionally. + +Every file is classified by camera module, and only the modules this rig +populates — ``SHW5G`` — are compared or installed. A drop that touches only +S56C changes nothing here: + +.. code-block:: text + + ==> Installed vs package (modules: SHW5G) + · package SG8A-AGON-G2Y-A1-JetPack7.2.1 @ f4a59f7 (installed) + ✓ libnvuddf_max96712_library.so [shared] + - libnvuddf_s56c_cameramodule_library.so [S56C] not on this rig + ! libnvuddf_shw5g_cameramodule_library.so [SHW5G] stale + ! SHW5G.nito [SHW5G] stale + +.. note:: + + Staleness is checked by content, not by version. SENSING reuses the package + name across drops and does not bump ``Version.md``, so a release that only + rebuilds the drivers and retunes the ISP is invisible to any check that + looks at names or timestamps. What was installed is recorded in + ``/var/lib/sensing/installed.json`` — a per-module digest plus the upstream + commit, which is the version to quote when reporting a problem to SENSING. + + Such a drop needs no reboot either: only files are copied, and the overlay + is the sole artifact the kernel reads at boot. The script says so rather + than sending you through ``jetson-io`` again. + +.. warning:: + + Setup copies the files itself rather than running the vendor + ``install.sh``, which wipes ``/usr/lib/nvsipl_drv`` and installs every + module's driver. The trade is that a new vendor artifact could go + unnoticed, so each run first checks that every source ``install.sh`` copies + is one setup knows about, and says so loudly if not. + +Then select the overlay and reboot: + +.. code-block:: bash + + sudo /opt/nvidia/jetson-io/jetson-io.py + # Configure Jetson AGX CSI Connector + # -> Configure for compatible hardware + # -> Jetson Sensing SG8A-AGON-G2Y-A1 SIPL GMSL2x8 + sudo nvpmodel -m 0 && sudo jetson_clocks + +.. note:: + + There is **no per-boot step**. The vendor ``install.sh`` is copy-only, so + once the overlay is selected the rig comes up on its own. The old + ``sensing-load.sh`` and ``sensing-camera.service`` no longer exist. + +Groups, not root +~~~~~~~~~~~~~~~~ + +SIPL needs no root, but it does need two supplementary groups beyond ``video``: + +.. list-table:: + :widths: 35 15 50 + :header-rows: 1 + + * - Node + - Group + - Why + * - ``/dev/i2c-9``, ``/dev/i2c-10`` + - ``i2c`` + - the two MAX96712 deserializers + * - ``/dev/gpiochip0``, ``/dev/gpiochip1`` + - ``gpio`` + - deserializer power enable + +.. code-block:: bash + + sudo usermod -aG i2c,gpio $USER # then log out and back in + +Omit either and SIPL fails with exactly one line — ``Master SetPlatformConfig +(Camera HAL) failed. status: 10`` — which names neither the node nor the +permission. ``verify.sh`` diagnoses it properly. + +Container +~~~~~~~~~ + +To build against this rig from inside a container: + +.. code-block:: bash + + src/plugins/sensing/docker/setup_container.sh + +It checks the container can reach the device nodes, then provisions both build +SDKs — public downloads resolved from the version in ``/etc/nv_tegra_release``. +The **Jetson Multimedia API** comes from the apt pool; the **SIPL API** is a +tarball published alongside the L4T release, fetched by URL. To install that +one by hand instead: + +.. code-block:: bash + + sudo tar xf Jetson_SIPL_API_R39.2.1_aarch64.tbz2 -C /usr/src/ + +Running the cameras needs none of that, only what the next two sections cover. + +Validating in a container +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Before adapting your own image, prove the rig works from a throwaway one: + +.. code-block:: bash + + src/plugins/sensing/scripts/run_docker_example.sh + # ... builds, then drops you at a shell in the container: + scripts/validate.sh + +The script builds :code-file:`docker/Dockerfile.example +` — which does nothing but add +the EGL runtime and run ``image_setup.sh`` — and starts it with the flags no +Dockerfile can express. ``--print-args`` and ``--json`` emit those flags for +your own container instead; ``-- scripts/validate.sh`` runs the check and exits rather +than opening a shell. + +Five ingredients, and leaving any one out fails in a way that names something +else: + +.. list-table:: + :widths: 32 68 + :header-rows: 1 + + * - Ingredient + - What its absence looks like + * - ``image_setup.sh`` groups + - ``SetPlatformConfig ... status: 10``, naming no node + * - ``--privileged`` (or ``--device-cgroup-rule``) + - every node unreadable no matter which groups the process holds + * - ``NVIDIA_*`` env with ``--runtime=nvidia`` + - ``libnvsipl.so: cannot open shared object file`` — the runtime injects + the BSP libraries only when the capabilities are requested + * - ``libegl1`` in the image + - ``libEGL.so.1: cannot open shared object file`` + * - the two ``:ro`` mounts + - ``SetPlatformConfig ... status: 10`` again — the CSV list carries no + vendor-added driver or NITO + +Adapting your own image +~~~~~~~~~~~~~~~~~~~~~~~ + +Group membership belongs in the **image**, not in ``runArgs``: Docker resolves +supplementary groups for the container user from the image's own +``/etc/group``, so putting the user in ``i2c``, ``gpio`` and ``video`` at build +time covers PID 1 and ``docker exec`` alike. Verified on this rig — with the +gids in the image and no ``--group-add``, all of ``/dev/i2c-9``, +``/dev/i2c-10``, ``/dev/gpiochip0`` and ``/dev/nvmap`` are readable. The +reverse does not hold: ``--group-add`` *alone* leaves an interactive shell +without them, because ``docker exec`` ignores ``HostConfig.GroupAdd``. + +The gids are host-specific, so read them off the host rather than copying: + +.. code-block:: console + + $ src/plugins/sensing/scripts/run_docker_example.sh --print-gids + 114:i2c 985:gpio 44:video + +The image half is one ``RUN``. Expose the directory as a named build context — +``docker build`` cannot ``COPY`` across into a submodule: + +.. code-block:: bash + + docker build --build-context sensing=path/to/src/plugins/sensing/docker ... + +.. code-block:: dockerfile + + RUN --mount=type=bind,from=sensing,target=/mnt/sensing \ + /mnt/sensing/image_setup.sh "$USERNAME" 114:i2c 985:gpio 44:video + +The bind mount leaves nothing behind, so the script is not baked into a layer. +To layer onto an existing image instead of editing its Dockerfile: + +.. code-block:: bash + + docker build -f docker/Dockerfile.example --build-arg BASE=my-image:latest \ + --build-arg SENSING_USER=me -t my-image:sensing src/plugins/sensing/docker + +The runtime half comes from the same script: + +.. code-block:: bash + + src/plugins/sensing/scripts/run_docker_example.sh --print-args + +Paste the printed flags after ``docker run`` and before your image name. +``--json`` emits the same flags as elements for ``devcontainer.json`` +``runArgs``. Run ``image_setup.sh`` in your image to configure the groups. + +nvsipl_camera +------------- + +The vendor reference application, which setup installs to ``/usr/sbin``. It is +the ground truth for "is the rig itself alive": it drives +the same SIPL API as any other client, so if it cannot stream, nothing else +will either. Reach for it before debugging anything of ours. + +``validate.sh`` wraps it with this rig's arguments and runs the same way on the +host and inside a container: + +.. code-block:: bash + + src/plugins/sensing/scripts/validate.sh # stream both sensors for 10 s + src/plugins/sensing/scripts/validate.sh --query # ask the config, no hardware + src/plugins/sensing/scripts/validate.sh -t 0 # until 'q' + +It resolves the package itself and fails with the reason rather than a status +code when the config or the binary is missing. To drive the tool directly: + +.. code-block:: bash + + cd src/plugins/sensing/.cache/SG8A-AGON-G2Y-A1-JetPack7.2.1 + nvsipl_camera -t query/sg8a_agth_g2a/shw5g.json -c SHW5G_2 \ + -m "0x0000 0x1100" --enable-camera-hal -s -Z -r 10 + +.. list-table:: + :widths: 25 75 + :header-rows: 1 + + * - Flag + - Why this rig needs it + * - ``-t FILE`` + - platform config JSON, resolved against the working directory — hence the ``cd`` + * - ``-c NAME`` + - config within that file; ``SHW5G_2`` for two SHW5G, ``-l`` lists them + * - ``-m MASKS`` + - one mask per deserializer in transport order: nothing on CSI-GH, links 2 and 3 on CSI-CD + * - ``--enable-camera-hal`` + - the camera-HAL path; without it SIPL takes the legacy DevBlk path, which this carrier does not support + * - ``-Z`` + - skip sensor authentication — these modules carry no auth keys + * - ``-s`` + - print FPS every 2 s, which is how you tell streaming from merely starting + * - ``-r N`` + - exit after N seconds, and disable the interactive menu + * - ``-v 3`` + - raise verbosity when a failure names nothing + +Expect ~60 fps per sensor with zero drops. Without ``-r`` it runs until ``q``. + +``nvsipl_query`` answers what the config *declares*, with no hardware and no +capture: + +.. code-block:: bash + + nvsipl_query -t query/sg8a_agth_g2a/shw5g.json -l # config names + nvsipl_query -t query/sg8a_agth_g2a/shw5g.json -c SHW5G_2 + +Troubleshooting +--------------- + +.. list-table:: + :widths: 45 55 + :header-rows: 1 + + * - Symptom + - Cause + * - ``SetPlatformConfig (Camera HAL) failed. status: 10`` + - missing ``i2c`` or ``gpio`` group — run ``verify.sh`` + * - ``no modules left in '' after applying the link masks`` + - ``--link-masks`` does not match the config + * - ``cannot open NITO file .../SHW5G.nito`` + - drivers were never installed — re-run ``setup.sh`` + * - ``Failed to open file .../shw5g.json`` + - the fetched package ships no ``SHW5G_2`` config; ``setup.sh`` drops one in + * - capture hangs, no frames, no error + - another SIPL client holds the hardware — stop ``nvsipl_camera`` + * - ``Could not get EglImage from fd`` / ``Failed to create EGLImage`` + - ``DISPLAY`` names an X server Tegra EGL cannot drive; see below + +DISPLAY and the EGL vendor +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Two X servers usually exist on this box: ``:1`` is a real server the Tegra +driver can drive, ``:99`` is Xvfb and it cannot. With both ``10_nvidia.json`` +and ``50_mesa.json`` installed, GLVND hands out *Mesa's* EGL whenever +``DISPLAY`` names a server Tegra EGL cannot drive, and +``NvBufSurfaceMapEglImage()`` then fails with ``Could not get EglImage from +fd``. + +The container defaults to ``DISPLAY=:99``, so anything that renders needs: + +.. code-block:: bash + + export DISPLAY=:1 # nvsipl_camera --egl-display, or any viewer diff --git a/docs/source/device/cameras/index.rst b/docs/source/device/cameras/index.rst index e94ec7b3d2..de05baeb37 100644 --- a/docs/source/device/cameras/index.rst +++ b/docs/source/device/cameras/index.rst @@ -12,3 +12,4 @@ camera feeds to clients, see :doc:`/references/camera_streaming`. :titlesonly: oak + /device/camera/sensing diff --git a/src/plugins/sensing/.gitignore b/src/plugins/sensing/.gitignore new file mode 100644 index 0000000000..d510e995b6 --- /dev/null +++ b/src/plugins/sensing/.gitignore @@ -0,0 +1,4 @@ +# Cached downloads: the jetson_multimedia_api deb, the Jetson_SIPL_API tarball +# and a sparse clone of SENSING's driver repo (.drivers-repo, reached through +# the flat SG8A-AGON-G2Y-A1-JetPack7.2.1 symlink). ~80 MB, refetched when absent. +.cache/ diff --git a/src/plugins/sensing/AGENTS.md b/src/plugins/sensing/AGENTS.md new file mode 100644 index 0000000000..909a7b9967 --- /dev/null +++ b/src/plugins/sensing/AGENTS.md @@ -0,0 +1,13 @@ + + +# SENSING scripts + +Keep launchers that primarily compose Docker commands in Bash. +Keep machine-readable output on stdout and diagnostics on stderr. + +Maintain one root README for the plugin overview, source map, and config provenance. +Link to the published GitHub Pages user guide, not its RST source. +Setup procedures and troubleshooting belong in that guide, not in both files. diff --git a/src/plugins/sensing/README.md b/src/plugins/sensing/README.md new file mode 100644 index 0000000000..69acc37968 --- /dev/null +++ b/src/plugins/sensing/README.md @@ -0,0 +1,40 @@ + + +# SENSING GMSL cameras + +SENSING connects GMSL cameras to Jetson through the SG8A carrier. This +integration uses NVIDIA SIPL for camera access and provides the tooling to +provision and validate the rig on the host or in a container. + +Start with the [SENSING user guide](https://nvidia.github.io/IsaacCapture/main/device/camera/sensing.html) +for supported hardware, installation, container usage, and troubleshooting. +That guide owns the operating instructions; this README maps the source tree. + +- [`scripts/`](scripts/): host setup, validation, diagnostics, and the Bash + Docker launcher. Each entry point exposes `--help`. +- [`docker/`](docker/): example image, image-side group configuration, and + container SDK provisioning. +- [`configs/`](configs/): the platform configuration and its provenance. + +## Platform configuration provenance + +[`configs/shw5g.json`](configs/shw5g.json) is a byte-identical copy supplied +by SENSING for the two-SHW5G rig, from: + +```text +SG8A_AGON_G2Y_A1_AGX_ORIN_S56C_SHW5G_SHF3L_SIPL_JP7.2.1_L4TR39.2.1 + /query/sg8a_agth_g2a/shw5g.json +``` + +The public driver package includes only the mixed-camera configurations, +`s56c_shf3l_mixed.json` and `shw5g_shf3l_mixed.json`, not this `SHW5G_2` +configuration. Vendoring it makes validation independent of a downloaded +vendor package. + +Keep the file byte-identical to the vendor copy. Compare new vendor drops +before replacing it; do not hand-edit the board wiring. Its CSI ports, I2C +buses, deserializer addresses, GPIOs, and link modes must match the installed +SIPL drivers. diff --git a/src/plugins/sensing/configs/shw5g.json b/src/plugins/sensing/configs/shw5g.json new file mode 100644 index 0000000000..69c122cab9 --- /dev/null +++ b/src/plugins/sensing/configs/shw5g.json @@ -0,0 +1,259 @@ +{ + "deserializers": [ + { + "name": "MAX96712_Fusa_nv_AB", + "description": "Maxim MAX96712 GMSL2 Deserializer - CSI-AB", + "i2cAddress": "0x29" + }, + { + "name": "MAX96712_Fusa_nv_CD", + "description": "Maxim MAX96712 GMSL2 Deserializer - CSI-CD", + "i2cAddress": "0x2d" + } + ], + "serializers": [ + { + "name": "MAX9295", + "description": "Maxim MAX9295D Serializer", + "i2cAddress": "0x40", + "useCDIv2API": true + }, + { + "name": "MAX96717F", + "description": "Maxim MAX96717F Serializer", + "i2cAddress": "0x40", + "useCDIv2API": true + }, + { + "name": "MAX96717", + "description": "Maxim MAX96717 Serializer", + "i2cAddress": "0x40", + "useCDIv2API": true + } + ], + "imageSensors": [ + { + "name": "VB1940", + "description": "SHW5G link2/3 VB1940 physical 0x10", + "i2cAddress": "0x10", + "virtualChannels": [ + { + "cfa": "gbrg", + "embeddedTopLines": 1, + "embeddedBottomLines": 0, + "inputFormat": "raw10", + "width": 2560, + "height": 1984, + "fps": 60.0, + "isEmbeddedDataTypeEnabled": true + } + ], + "isTriggerModeEnabled": true, + "isTPGEnabled": false, + "useCDIv2API": true + } + ], + "platformTransportSettings": [ + { + "name": "jetson-thor", + "description": "Thor - Jetson AGX Thor Devkit (p3834)", + "boardIdPrefix": "p3834", + "enableMasks": ["0x0000", "0x1100"], + "transportSettings": [ + { + "name": "transportSettings_jetson-thor_AB", + "type": "GMSL", + "description": "GMSL CSI-AB with external MAX96712", + "csiPort": "csi-gh", + "phyMode": "dphy", + "desTxPort": 1, + "deserInfo": { + "name": "Max96712GmslDeserializer", + "i2cAddress": "0x2d" + }, + "powerControlInfo": { + "deserializerInfo": { + "name": "TegraDeserPowerDriver", + "gpioPinIndex": 9043968 + } + }, + "i2cDevice": 10, + "desI2CPort": 0, + "groupInitProg": true + }, + { + "name": "transportSettings_jetson-thor_CD", + "type": "GMSL", + "description": "GMSL CSI-CD with external MAX96712", + "csiPort": "csi-cd", + "phyMode": "dphy", + "desTxPort": 1, + "deserInfo": { + "name": "Max96712GmslDeserializer", + "i2cAddress": "0x29" + }, + "powerControlInfo": { + "deserializerInfo": { + "name": "TegraDeserPowerDriver", + "gpioPinIndex": 3211264 + } + }, + "i2cDevice": 9, + "desI2CPort": 0, + "groupInitProg": true + } + ] + } + ], + "cameraConfigs": [ + { + "name": "SHW5G_2", + "type": "GMSL", + "description": "MAX96712 (CSI-CD / CN1): link2/link3 SHW5G mono", + "cameraModules": [ + { + "name": "SHW5G_MIX_L2", + "linkIndex": 2, + "transportIndex": 1 + }, + { + "name": "SHW5G_MIX_L3", + "linkIndex": 3, + "transportIndex": 1 + } + ], + "transportSettings": [ + { + "name": "transportSettings_jetson-thor_AB", + "type": "GMSL", + "csiPort": "csi-gh", + "phyMode": "dphy", + "desTxPort": 1, + "deserInfo": { + "name": "Max96712GmslDeserializer", + "i2cAddress": "0x2d" + }, + "powerControlInfo": { + "deserializerInfo": { + "name": "TegraDeserPowerDriver", + "gpioPinIndex": 9043968 + } + }, + "i2cDevice": 10, + "desI2CPort": 0, + "groupInitProg": false + }, + { + "name": "transportSettings_jetson-thor_CD", + "type": "GMSL", + "csiPort": "csi-cd", + "phyMode": "dphy", + "desTxPort": 1, + "deserInfo": { + "name": "Max96712GmslDeserializer", + "i2cAddress": "0x29" + }, + "powerControlInfo": { + "deserializerInfo": { + "name": "TegraDeserPowerDriver", + "gpioPinIndex": 3211264 + } + }, + "i2cDevice": 9, + "desI2CPort": 0, + "groupInitProg": true + } + ] + }, + { + "name": "SHW5G_MIX_L2", + "type": "GMSL", + "description": "SHW5G mono on physical link2, output VC2", + "moduleDriverName": "SHW5G", + "serInfo": { + "name": "MAX96717", + "i2cAddress": "0x40" + }, + "linkMode": "LINK_MODE_GMSL2_6GBPS", + "fsyncMode": "osc_manual", + "mipiSettings": { + "dphyRate": 2500000, + "phyMode": "dphy" + }, + "sensorInfo": [ + { + "name": "VB1940", + "id": 2, + "description": "SHW5G link2 sensor", + "i2cAddress": "0x10", + "numContext": 1, + "isTriggerModeEnabled": true, + "deviceIndex": 0, + "virtualChannels": [ + { + "vcIdSrc": 2, + "vcIdDst": 2, + "cfa": "gbrg", + "embeddedTopLines": 1, + "embeddedBottomLines": 0, + "inputFormat": "raw10", + "width": 2560, + "height": 1984, + "fps": 60.0, + "isEmbeddedDataTypeEnabled": true + } + ] + } + ], + "eepromInfo": { + "name": "BR24H128", + "i2cAddress": "0x50" + } + }, + { + "name": "SHW5G_MIX_L3", + "type": "GMSL", + "description": "SHW5G mono on physical link3, output VC3", + "moduleDriverName": "SHW5G", + "serInfo": { + "name": "MAX96717", + "i2cAddress": "0x40" + }, + "linkMode": "LINK_MODE_GMSL2_6GBPS", + "fsyncMode": "osc_manual", + "mipiSettings": { + "dphyRate": 2500000, + "phyMode": "dphy" + }, + "sensorInfo": [ + { + "name": "VB1940", + "id": 3, + "description": "SHW5G link3 sensor", + "i2cAddress": "0x10", + "numContext": 1, + "isTriggerModeEnabled": true, + "deviceIndex": 0, + "virtualChannels": [ + { + "vcIdSrc": 3, + "vcIdDst": 3, + "cfa": "gbrg", + "embeddedTopLines": 1, + "embeddedBottomLines": 0, + "inputFormat": "raw10", + "width": 2560, + "height": 1984, + "fps": 60.0, + "isEmbeddedDataTypeEnabled": true + } + ] + } + ], + "eepromInfo": { + "name": "BR24H128", + "i2cAddress": "0x50" + } + } + ] +} diff --git a/src/plugins/sensing/docker/Dockerfile.example b/src/plugins/sensing/docker/Dockerfile.example new file mode 100644 index 0000000000..980c3b8bf8 --- /dev/null +++ b/src/plugins/sensing/docker/Dockerfile.example @@ -0,0 +1,35 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# A container that can reach the SENSING cameras. Built and started by +# run_docker_example.sh; the point is to be read, then borrowed from. +# +# Two things go in the image, and only two: +# - the runtime libraries nvsipl_camera links against but the NVIDIA container +# runtime does not inject (it carries the BSP, not the base OS) +# - the group membership, via image_setup.sh +# +# Everything else -- driver mounts, device access, the NVIDIA capabilities -- +# is a container-create option and lives in run_docker_example.sh. +# +# No SIPL drivers are installed here. They are bind-mounted from the host, so +# a vendor update on the host needs no rebuild. + +ARG BASE=ubuntu:24.04 +FROM ${BASE} + +# nvsipl_camera needs EGL: NvBufSurfaceMapEglImage is how a SIPL buffer reaches +# CUDA, so EGL is on the path even with nothing on screen. Absent, it fails at +# load with "libEGL.so.1: cannot open shared object file". +RUN apt-get update && apt-get install -y --no-install-recommends \ + libegl1 libgles2 libglib2.0-0 \ + && rm -rf /var/lib/apt/lists/* + +# Host gids owning /dev/i2c-*, /dev/gpiochip* and /dev/nvmap. Defaults are this +# rig's; run_docker_example.sh passes the real ones, read off the nodes. +# No --group-add is needed at run time: Docker resolves supplementary groups +# from this /etc/group, which covers PID 1 and `docker exec` alike. +ARG SENSING_USER=root +ARG SENSING_GIDS="114:i2c 985:gpio 44:video" +RUN --mount=type=bind,source=image_setup.sh,target=/tmp/image_setup.sh \ + /tmp/image_setup.sh "${SENSING_USER}" ${SENSING_GIDS} diff --git a/src/plugins/sensing/docker/image_setup.sh b/src/plugins/sensing/docker/image_setup.sh new file mode 100755 index 0000000000..c3cabc1acd --- /dev/null +++ b/src/plugins/sensing/docker/image_setup.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Image-side setup for SIPL capture: put the container user in the groups that +# own the host's camera device nodes. Run as root from a Dockerfile RUN. +# +# This is the whole of it -- no --group-add needed on `docker run`. Docker +# resolves supplementary groups for the container user from the image's own +# /etc/group, so membership set here reaches PID 1 and `docker exec` alike. +# The reverse is not true: --group-add only reaches PID 1, because `docker exec` +# ignores HostConfig.GroupAdd. +# +# Usage: +# image_setup.sh USER GID:NAME [GID:NAME ...] +# +# Get the gids for a specific host with: ./run_docker_example.sh --print-gids + +set -euo pipefail + +[[ $# -ge 2 ]] || { echo "usage: $0 USER GID:NAME [GID:NAME ...]" >&2; exit 2; } +user="$1"; shift + +id "$user" >/dev/null 2>&1 || { echo "$0: no such user: $user" >&2; exit 1; } + +for spec in "$@"; do + gid="${spec%%:*}"; name="${spec##*:}" + [[ "$gid" =~ ^[0-9]+$ ]] || { echo "$0: bad spec: $spec" >&2; exit 2; } + # Adopt whatever group already holds the gid rather than assuming the name + # is free -- base images vary, and the kernel checks the number anyway. + if getent group "$gid" >/dev/null; then + name="$(getent group "$gid" | cut -d: -f1)" + else + groupadd --gid "$gid" "$name" + fi + usermod -aG "$name" "$user" + echo " $user -> $name($gid)" +done diff --git a/src/plugins/sensing/docker/setup_container.sh b/src/plugins/sensing/docker/setup_container.sh new file mode 100755 index 0000000000..b37c874346 --- /dev/null +++ b/src/plugins/sensing/docker/setup_container.sh @@ -0,0 +1,255 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# CONTAINER-side setup for the SENSING SG8A SIPL rig. +# +# SIPL has no daemon: the plugin opens the hardware itself, in-process. So this +# half is about two things only -- can this container reach the device nodes, +# and are the SDK headers here to build against. +# +# Usage: +# ./setup_container.sh [options] +# +# Options: +# --sdk-dir DIR where to unpack the Jetson Multimedia API (default /usr/src) +# --skip-sdk do not fetch anything, just report +# --purge-cache drop the cached SDK downloads first, then fetch fresh +# -y, --yes assume yes for every prompt +# -h, --help this text + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=../scripts/common.sh +source "$SCRIPT_DIR/../scripts/common.sh" + +SDK_ROOT=/usr/src +SKIP_SDK=0 +ASSUME_YES=0 + +usage() { sed -n '4,19p' "${BASH_SOURCE[0]}" | sed 's/^# \?//'; } + +while (( $# )); do + case "$1" in + --sdk-dir) SDK_ROOT="$2"; MMAPI_DIR="$2/jetson_multimedia_api"; shift 2 ;; + --skip-sdk) SKIP_SDK=1; shift ;; + --purge-cache) [[ -n "$SENSING_CACHE_DIR" ]] && rm -rf "${SENSING_CACHE_DIR:?}"; shift ;; + -y|--yes) ASSUME_YES=1; shift ;; + -h|--help) usage; exit 0 ;; + *) die "unknown argument: $1" "Run $0 --help" ;; + esac +done +export ASSUME_YES + +in_container || die "this script is for the CONTAINER side." \ + "You appear to be on the host — run ../scripts/setup.sh instead." + +printf '%sSENSING container setup (SIPL)%s\n' "$C_BOLD" "$C_RESET" + +BLOCKED=0 + +# --- 1. Device nodes and groups -------------------------------------------- +# Everything SIPL touches is 0660 root:. Root is NOT required -- the +# plugin runs as the ordinary container user -- but the supplementary groups +# are, and a missing one surfaces only as +# Master SetPlatformConfig (Camera HAL) failed. status: 10 +# which names neither the node nor the permission. Hence this check. +step "Device nodes" +missing_nodes=() +while IFS= read -r entry; do + path="${entry%%:*}"; desc="${entry#*:}" + [[ -e "$path" ]] || { missing_nodes+=("$path"); bad "$path absent — $desc"; continue; } + [[ -r "$path" ]] && ok "$path" || bad "$path present but not readable — $desc" +done < <(sensing_required_nodes) + +if [[ "${#missing_nodes[@]}" -gt 0 ]]; then + hint "Nodes absent entirely: this container needs the host /dev." + hint ' docker run: -v /dev:/dev (or --device for each node)' + BLOCKED=1 +fi + +mapfile -t MISSING_GIDS < <(sensing_missing_gids) +if [[ "${#MISSING_GIDS[@]}" -gt 0 ]]; then + bad "this user is missing ${#MISSING_GIDS[@]} supplementary group(s)" + # --group-add reaches PID 1 only; `docker exec` resolves supplementary groups + # from the container's own /etc/group, so both halves are required. + printf '\n %sBoth of these, then REBUILD (a restart will not re-apply runArgs):%s\n' "$C_BOLD" "$C_RESET" + printf ' %s1. devcontainer.json runArgs%s\n' "$C_BOLD" "$C_RESET" + for gid in "${MISSING_GIDS[@]}"; do + hint "\"--group-add\", \"$gid\"," + done + printf ' %s2. Dockerfile - the same gids must exist in the image%s\n' "$C_BOLD" "$C_RESET" + for gid in "${MISSING_GIDS[@]}"; do + hint "RUN groupadd -g $gid && usermod -aG \$USERNAME" + done + BLOCKED=1 +else + ok "all required groups present (uid $(id -u), groups: $(id -G | tr ' ' ','))" +fi + +# --- 2. SIPL runtime -------------------------------------------------------- +# Supplied by the NVIDIA container runtime's CSV mounts plus the vendor +# install.sh on the host. Nothing in here can install them. +step "SIPL runtime" +# Snapshot once: `grep -q` SIGPIPEs ldconfig, and `set -o pipefail` then reports +# a successful match as a failure. +LDCACHE="$(ldconfig -p 2>/dev/null || true)" +if grep -q 'libnvsipl\.so' <<<"$LDCACHE"; then + ok "libnvsipl.so resolvable" +else + bad "libnvsipl.so not found — the NVIDIA container runtime should provide it" + BLOCKED=1 +fi +if [[ -d "$SIPL_DRV_DIR" ]] && compgen -G "$SIPL_DRV_DIR/libnvuddf_*" >/dev/null; then + ok "$SIPL_DRV_DIR ($(find "$SIPL_DRV_DIR" -name 'libnv*' | wc -l) driver libs)" +else + bad "$SIPL_DRV_DIR is empty or absent" + hint "On the HOST: run the vendor install.sh — see ../scripts/setup.sh" + BLOCKED=1 +fi +if [[ -r "$SIPL_NITO_DIR/$SENSING_NITO" ]]; then + ok "$SIPL_NITO_DIR/$SENSING_NITO" +else + bad "$SENSING_NITO missing from $SIPL_NITO_DIR — the ISP has no tuning to load" + hint "On the HOST: run the vendor install.sh" + BLOCKED=1 +fi + +# --- 3. Jetson Multimedia API ---------------------------------------------- +# NvVideoEncoder sources and nvbufsurface.h. Fetchable: the deb is in the public +# pool, and the version is parsed out of /etc/nv_tegra_release so it matches the +# running BSP by construction. +step "Jetson Multimedia API" +if mmapi="$(find_mmapi)"; then + ok "$mmapi" +elif [[ "$SKIP_SDK" -eq 1 ]]; then + bad "not found (--skip-sdk given, not fetching)" + BLOCKED=1 +else + rel="$(l4t_release || true)" + [[ -n "$rel" ]] || die "cannot read the L4T release from /etc/nv_tegra_release." + info "L4T R$rel — resolving nvidia-l4t-jetson-multimedia-api from the public pool" + have curl || die "curl is required to fetch the SDK." "apt-get install -y curl" + + # The flat pool index carries absolute URLs for every deb. Note the package + # lives under common/, not t234/ -- the t234 path 404s. + pool_index="$(mktemp)"; trap 'rm -f "$pool_index"' EXIT + curl -fsSL --retry 3 https://repo.download.nvidia.com/jetson/ -o "$pool_index" \ + || die "could not reach repo.download.nvidia.com." + url="$(grep -oE "https://[^\"]*nvidia-l4t-jetson-multimedia-api_${rel}-[0-9]+_arm64\.deb" \ + "$pool_index" | sort -u | tail -1)" + [[ -n "$url" ]] || die "no nvidia-l4t-jetson-multimedia-api build for L4T R$rel in the pool." \ + "Install it by hand and re-run, or pass --skip-sdk." + + info "$(basename "$url")" + require_sudo "unpack the Jetson Multimedia API into $SDK_ROOT" + if confirm "Fetch and unpack it now?"; then + fetch_cached "$url" valid_deb || die "could not fetch $(basename "$url")" + stage="$(mktemp -d)" + # dpkg-deb -x unpacks files without registering the package, which is + # what a container wants: no dependency resolution, no dpkg database + # churn, and it is idempotent. Stage first so --sdk-dir can redirect it + # -- the deb's own layout is ./usr/src/jetson_multimedia_api. + dpkg-deb -x "$FETCHED_PATH" "$stage" || die "could not unpack $(basename "$url")" + sudo mkdir -p "$SDK_ROOT" + sudo cp -a "$stage/usr/src/jetson_multimedia_api" "$SDK_ROOT/" + rm -rf "$stage" + if mmapi="$(find_mmapi)"; then ok "$mmapi"; else bad "unpacked but headers still not found"; BLOCKED=1; fi + else + warn "declined — the plugin cannot build without it" + BLOCKED=1 + fi +fi + +# --- 4. SIPL API headers ---------------------------------------------------- +# Jetson_SIPL_API__aarch64.tbz2, published alongside the L4T release. Not +# in any apt pool, so it is fetched by URL rather than by package -- but it is +# a public download, no developer login. +step "SIPL API headers" +if sipl="$(find_sipl_api)"; then + ok "$sipl" +elif [[ "$SKIP_SDK" -eq 1 ]]; then + bad "not found (--skip-sdk given, not fetching)" + BLOCKED=1 +else + rel="$(l4t_release || true)" + [[ -n "$rel" ]] || die "cannot read the L4T release from /etc/nv_tegra_release." + have curl || die "curl is required to fetch the SDK." "apt-get install -y curl" + + # 39.2.1 -> r39_Release_v2.1 / Jetson_SIPL_API_R39.2.1_aarch64.tbz2 + major="${rel%%.*}"; rest="${rel#*.}" + url="https://developer.nvidia.com/downloads/embedded/L4T/r${major}_Release_v${rest}/release/Jetson_SIPL_API_R${rel}_aarch64.tbz2" + if ! curl -fsIL "$url" >/dev/null 2>&1; then + # Naming has changed before; fall back to whatever the release page links. + info "derived URL not reachable, checking the Jetson Linux release page" + url="$(curl -fsSL https://developer.nvidia.com/embedded/jetson-linux 2>/dev/null \ + | grep -oiE 'https://[^"'"'"'<> ]*SIPL_API[^"'"'"'<> ]*\.tbz2' | sort -u | tail -1)" + fi + + if [[ -z "$url" ]]; then + bad "could not locate the SIPL API tarball for L4T R$rel" + hint "Find Jetson_SIPL_API_*.tbz2 on https://developer.nvidia.com/embedded/jetson-linux" + hint "then: sudo tar xf -C /usr/src/" + BLOCKED=1 + else + info "$(basename "$url")" + require_sudo "unpack the SIPL API headers into /usr/src" + if confirm "Fetch and unpack them now?"; then + fetch_cached "$url" valid_tbz2 || die "could not fetch $(basename "$url")" + # The tarball's own layout is ./usr/src/jetson_sipl_api. + sudo tar xf "$FETCHED_PATH" -C / + if sipl="$(find_sipl_api)"; then ok "$sipl"; else bad "unpacked but headers still not found"; BLOCKED=1; fi + else + warn "declined — the plugin cannot build without it" + BLOCKED=1 + fi + fi +fi + +if [[ -n "${sipl:-}" ]]; then + for sub in sipl/include sipl/include/query/include sipl/include/nvsci; do + [[ -d "$sipl/$sub" ]] && ok " $sub" || { bad " $sub missing"; BLOCKED=1; } + done +fi + +# --- 5. Build prerequisites ------------------------------------------------- +step "Build prerequisites" +if have nvcc || [[ -x /usr/local/cuda/bin/nvcc ]]; then + ok "nvcc: $("${CUDA_PATH:-/usr/local/cuda}/bin/nvcc" --version 2>/dev/null \ + | sed -n 's/.*release \([0-9.]*\).*/CUDA \1/p' | tail -1)" +else + bad "nvcc not found"; hint "Expected at /usr/local/cuda/bin/nvcc"; BLOCKED=1 +fi +for lib in nvscibuf nvscisync nvbufsurface; do + if grep -q "lib${lib}\.so" <<<"$LDCACHE"; then ok "lib${lib}.so" + else bad "lib${lib}.so not resolvable"; BLOCKED=1; fi +done + +# --- 6. Vendor package and smoke-test tool ---------------------------------- +step "Vendor package" +if pkg="$(find_sensing_pkg)" && [[ -n "$pkg" ]]; then + ok "$pkg" + [[ -f "$pkg/$SENSING_CONFIG_JSON_REL" ]] \ + && ok "platform config: $SENSING_CONFIG_JSON_REL" \ + || { bad "$SENSING_CONFIG_JSON_REL missing from the package"; BLOCKED=1; } +else + info "not visible in this container — only needed for the vendor smoke test" + hint "Mount it, or set SENSING_PKG_DIR" +fi + +if have nvsipl_camera; then + ok "nvsipl_camera on PATH (vendor smoke test)" +else + info "nvsipl_camera not on PATH — installed by the vendor install.sh on the host" +fi + +step "Verifying" +"$SCRIPT_DIR/verify.sh" || true + +printf '\n' +if [[ "$BLOCKED" -eq 0 ]]; then + printf '%s%sContainer setup complete.%s\n' "$C_GREEN" "$C_BOLD" "$C_RESET" +else + printf '%s%sContainer setup incomplete — see the actions above.%s\n' "$C_YELLOW" "$C_BOLD" "$C_RESET" +fi diff --git a/src/plugins/sensing/scripts/common.sh b/src/plugins/sensing/scripts/common.sh new file mode 100644 index 0000000000..8c60d0b016 --- /dev/null +++ b/src/plugins/sensing/scripts/common.sh @@ -0,0 +1,489 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Shared helpers for the SENSING setup scripts. Sourced, never executed. + +# Colour is opt-out (NO_COLOR) and auto-disabled when stdout is not a terminal, +# so piped/CI output stays greppable. +if [[ -t 1 && -z "${NO_COLOR:-}" ]]; then + C_RESET=$'\033[0m'; C_BOLD=$'\033[1m'; C_DIM=$'\033[2m' + C_RED=$'\033[31m'; C_GREEN=$'\033[32m'; C_YELLOW=$'\033[33m' + C_BLUE=$'\033[34m'; C_CYAN=$'\033[36m' +else + C_RESET=''; C_BOLD=''; C_DIM='' + C_RED=''; C_GREEN=''; C_YELLOW=''; C_BLUE=''; C_CYAN='' +fi + +step() { printf '\n%s==>%s %s%s%s\n' "$C_BLUE$C_BOLD" "$C_RESET" "$C_BOLD" "$1" "$C_RESET"; } +ok() { printf ' %s✓%s %s\n' "$C_GREEN" "$C_RESET" "$1"; } +info() { printf ' %s·%s %s\n' "$C_DIM" "$C_RESET" "$1"; } +warn() { printf ' %s!%s %s\n' "$C_YELLOW$C_BOLD" "$C_RESET" "$1" >&2; } +bad() { printf ' %s✗%s %s\n' "$C_RED$C_BOLD" "$C_RESET" "$1" >&2; } +hint() { printf ' %s%s%s\n' "$C_CYAN" "$1" "$C_RESET"; } + +die() { + printf '\n%sError:%s %s\n' "$C_RED$C_BOLD" "$C_RESET" "$1" >&2 + [[ -z "${2:-}" ]] || printf '%sAction:%s %s\n' "$C_CYAN$C_BOLD" "$C_RESET" "$2" >&2 + exit 1 +} + +have() { command -v "$1" >/dev/null 2>&1; } + +# --------------------------------------------------------------------------- +# Download cache +# +# The SDK artifacts total ~73 MB and are re-fetched on every container rebuild, +# because /usr/src does not survive one. The cache does: it lives in the +# worktree, which is bind-mounted from the host. Gitignored. +# --------------------------------------------------------------------------- +COMMON_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SENSING_CACHE_DIR="${SENSING_CACHE_DIR:-$COMMON_DIR/../.cache}" + +# fetch_cached URL VALIDATOR -> sets FETCHED_PATH +# +# Filenames are version-stamped, so the basename is the cache key and a new BSP +# fetches a new file rather than invalidating this one. VALIDATOR is a command +# run on the path; a cached file that fails it is re-fetched, so a truncated or +# half-written download cannot poison a later build. +FETCHED_PATH="" +fetch_cached() { + local url="$1" validate="$2" + local name path tmp + name="$(basename "$url")" + path="$SENSING_CACHE_DIR/$name" + FETCHED_PATH="" + + if [[ -f "$path" ]]; then + if "$validate" "$path" >/dev/null 2>&1; then + ok "cached: $name ($(du -h "$path" | cut -f1))" + FETCHED_PATH="$path" + return 0 + fi + warn "cached $name failed validation — re-fetching" + rm -f "$path" + fi + + mkdir -p "$SENSING_CACHE_DIR" || return 1 + # Download to .part and rename, so an interrupted fetch never leaves a + # plausible-looking cache entry behind. + tmp="$path.part" + info "downloading $name" + # A bar on a terminal, silence when piped -- curl's default meter is three + # lines of carriage-return noise in a log. + local progress=(--progress-bar); [[ -t 1 ]] || progress=(-sS) + curl -fL "${progress[@]}" --retry 3 "$url" -o "$tmp" || { rm -f "$tmp"; return 1; } + if ! "$validate" "$tmp" >/dev/null 2>&1; then + rm -f "$tmp" + return 1 + fi + mv -f "$tmp" "$path" + ok "cached: $name ($(du -h "$path" | cut -f1))" + FETCHED_PATH="$path" +} + +# Validators for fetch_cached. Cheap structural checks -- NVIDIA publishes no +# checksums for these artifacts, so "is it the right kind of archive" is as far +# as verification goes. +valid_deb() { dpkg-deb --info "$1"; } +valid_tbz2() { tar tjf "$1"; } + +in_container() { + [[ -f /.dockerenv ]] || grep -qE '(docker|containerd|kubepods)' /proc/1/cgroup 2>/dev/null +} + +# Loud, coloured notice naming every privileged action before the first prompt, +# then pre-authenticate so the password prompt lands here rather than midway +# through an install. `sudo -n true` first: on NOPASSWD hosts `sudo -v` still +# demands a password and would break unattended runs. +require_sudo() { + local width=72 line reason + + # Full banner once per run; later privileged actions get a one-liner so the + # notice stays visible without turning into wallpaper. + if [[ "${_SUDO_BANNER_SHOWN:-0}" == "1" ]]; then + for reason in "$@"; do + printf ' %ssudo:%s %s\n' "$C_YELLOW$C_BOLD" "$C_RESET" "$reason" + done + sudo -n true 2>/dev/null && return 0 + sudo -v || die "sudo authentication failed." "Re-run as a user with sudo privileges." + return 0 + fi + _SUDO_BANNER_SHOWN=1 + + printf -v line '%*s' "$width" ''; line=${line// /═} + + printf '\n%s%s╔%s╗%s\n' "$C_YELLOW" "$C_BOLD" "$line" "$C_RESET" + printf '%s%s║%s SUDO REQUIRED %*s║%s\n' \ + "$C_YELLOW" "$C_BOLD" "$C_RED" "$((width - 15))" '' "$C_RESET" + printf '%s%s╚%s╝%s\n' "$C_YELLOW" "$C_BOLD" "$line" "$C_RESET" + printf '%sThis script needs root privileges for:%s\n' "$C_BOLD" "$C_RESET" + local reason + for reason in "$@"; do + printf ' %s•%s %s\n' "$C_YELLOW$C_BOLD" "$C_RESET" "$reason" + done + printf '%sYou will be asked again before each optional action. Ctrl-C now to abort.%s\n\n' \ + "$C_DIM" "$C_RESET" + + if sudo -n true 2>/dev/null; then + ok "sudo already authenticated" + return 0 + fi + printf '%sEnter your password to continue.%s\n' "$C_CYAN" "$C_RESET" + sudo -v || die "sudo authentication failed." "Re-run as a user with sudo privileges." +} + +# Yes/no prompt. ASSUME_YES=1 auto-accepts; a non-interactive stdin declines, +# so unattended runs never silently take a privileged optional action. +confirm() { + local prompt="$1" reply + if [[ "${ASSUME_YES:-0}" == "1" ]]; then + info "$prompt ${C_DIM}[auto-yes]${C_RESET}" + return 0 + fi + if [[ ! -t 0 ]]; then + warn "$prompt — declined (non-interactive; pass --yes to accept)" + return 1 + fi + printf '%s%s%s [y/N] ' "$C_YELLOW$C_BOLD" "$prompt" "$C_RESET" + read -r reply + [[ "$reply" =~ ^[Yy]$ ]] +} + +# --------------------------------------------------------------------------- +# Rig facts +# +# SENSING SG8A-AGON-G2Y-A1 carrier, two SHW5G modules on CN1 CAM2/CAM3, driven +# through SIPL. There are no kernel sensor drivers and no /dev/video* nodes: +# SIPL owns the sensors from userspace. Everything below is read off the vendor +# platform config, not guessed -- run `nvsipl_query -t -c ` to +# re-derive it. +# --------------------------------------------------------------------------- +SENSING_PKG_GLOB='SG8A_AGON_G2Y_A1_AGX_ORIN_S56C_SHW5G_SHF3L_SIPL_JP7.2.1_L4TR39.2.1' +SENSING_CONFIG_JSON_REL='query/sg8a_agth_g2a/shw5g.json' +SENSING_PLATFORM_CONFIG='SHW5G_2' +# One mask per deserializer, in transport order: nothing on CSI-GH, links 2 and +# 3 on CSI-CD. +SENSING_LINK_MASKS='0x0000 0x1100' +SENSING_NITO='SHW5G.nito' +SENSING_OVERLAY_NAME='Jetson Sensing SG8A-AGON-G2Y-A1 SIPL GMSL2x8' + +# SENSING publishes the driver packages as directories in a public git repo -- +# no releases, no tarballs, so there is nothing to curl. +SENSING_PKG_REPO='https://github.com/SENSING-Technology/nvidia-jetson-camera-drivers.git' +SENSING_PKG_REPO_DIR='Jetson AGX Orin Devkit/SG8A-AGON-G2Y-A1/JetPack7.2.1' +# The clone must keep the repo's own layout, so it lives out of the way and the +# package is reached through a flat symlink. Board names are unique across the +# repo's four platform directories, so the platform level carries nothing the +# board name does not. +SENSING_PKG_REPO_NAME='.drivers-repo' +SENSING_PKG_NAME='SG8A-AGON-G2Y-A1-JetPack7.2.1' + +# The published package has no SHW5G_2 config -- SENSING ships shw5g.json to +# customers with a 2x SHW5G rig, and it is not in the repo. setup.sh drops +# this copy into the fetched package so nvsipl_camera has one to load. +SENSING_CONFIGS_DIR="$COMMON_DIR/../configs" + +# The two MAX96712 deserializer buses, from `i2cDevice` in the platform config. +SENSING_I2C_BUSES=(9 10) + +SIPL_DRV_DIR=/usr/lib/nvsipl_drv +SIPL_NITO_DIR=/var/nvidia/nvcam/settings/sipl +SIPL_CAMERAHAL=/usr/lib/aarch64-linux-gnu/nvidia/libnvcamerahal.so +SIPL_SBIN_DIR=/usr/sbin +SIPL_BOOT_DIR=/boot +SENSING_DTBO=tegra234-camera-sipl-camera-overlay.dtbo +SIPL_API_DIR="${SIPL_API_DIR:-/usr/src/jetson_sipl_api}" +MMAPI_DIR="${MMAPI_DIR:-/usr/src/jetson_multimedia_api}" + +# BSP version as it appears in the apt pool, e.g. 39.2.1-20260806224157. +# Parsed rather than hardcoded so the SDK fetch is version-matched by +# construction: "# R39 (release), REVISION: 2.1, ... DATE: ..." plus the +# matching pool timestamp is not derivable, so only MAJOR.MINOR is returned and +# the caller resolves the full version from the pool index. +l4t_release() { + local rel + [[ -r /etc/nv_tegra_release ]] || return 1 + rel="$(sed -n '1s/^# R\([0-9]*\).*REVISION: \([0-9.]*\).*/\1.\2/p' /etc/nv_tegra_release)" + [[ -n "$rel" ]] || return 1 + printf '%s\n' "$rel" +} + +# Locate the vendor driver package. $SENSING_PKG_DIR wins; otherwise search the +# usual drop points. Prints the path, empty if not found. +# +# Identified by install.sh + the query/ tree, since the SIPL package has no +# load_modules.sh -- there is nothing to load. +find_sensing_pkg() { + if [[ -n "${SENSING_PKG_DIR:-}" ]]; then + printf '%s\n' "$SENSING_PKG_DIR" + return 0 + fi + local d + for d in "$SENSING_CACHE_DIR/$SENSING_PKG_NAME" \ + "$HOME/$SENSING_PKG_GLOB" \ + "$HOME/Sensing/$SENSING_PKG_GLOB" \ + /home/*/"$SENSING_PKG_GLOB" \ + /home/*/Sensing/"$SENSING_PKG_GLOB" \ + /opt/sensing/"$SENSING_PKG_GLOB"; do + [[ -f "$d/install.sh" && -d "$d/query" ]] && { printf '%s\n' "$d"; return 0; } + done + printf '\n' +} + +# Sparse-clone the vendor package into the download cache. Prints its path. +# +# Blobless and sparse because the repo carries every board and JetPack +# combination -- ~700 MB, of which this rig needs 3.6 MB. Progress goes to +# stderr so the path is the only thing on stdout. +fetch_sensing_pkg() { + local root="$SENSING_CACHE_DIR/$SENSING_PKG_REPO_NAME" pkg + have git || { warn "git is required to fetch the vendor package"; return 1; } + + if [[ -d "$root/.git" ]]; then + info "refreshing $SENSING_PKG_REPO_NAME" >&2 + git -C "$root" fetch -q --depth 1 origin HEAD && git -C "$root" checkout -q FETCH_HEAD \ + || warn "refresh failed, using the cached copy" + else + info "cloning $SENSING_PKG_REPO_NAME (sparse)" >&2 + rm -rf "$root" + mkdir -p "$SENSING_CACHE_DIR" + git clone --filter=blob:none --no-checkout --depth 1 -q "$SENSING_PKG_REPO" "$root" \ + || { rm -rf "$root"; return 1; } + # --no-cone: the path has spaces in it, which cone mode cannot express. + git -C "$root" sparse-checkout set --no-cone "/$SENSING_PKG_REPO_DIR/*" || return 1 + git -C "$root" checkout -q || return 1 + fi + + # Find the package by its install.sh rather than by name, so a renamed or + # re-versioned vendor drop still resolves. + pkg="$(find "$root/$SENSING_PKG_REPO_DIR" -maxdepth 2 -name install.sh -printf '%h\n' 2>/dev/null | head -1)" + [[ -n "$pkg" ]] || { warn "no install.sh under $SENSING_PKG_REPO_DIR"; return 1; } + + # Flat entry point. The repo nests the package four deep behind a directory + # with spaces in it; nobody should have to type that. + ln -sfn "${pkg#"$SENSING_CACHE_DIR/"}" "$SENSING_CACHE_DIR/$SENSING_PKG_NAME" + printf '%s\n' "$SENSING_CACHE_DIR/$SENSING_PKG_NAME" +} + +# SIPL API headers (jetson_sipl_api.tbz2). Prints the include root. +find_sipl_api() { + local d + for d in "$SIPL_API_DIR" /usr/src/jetson_sipl_api /opt/nvidia/jetson_sipl_api; do + [[ -f "$d/sipl/include/NvSIPLCamera.hpp" ]] && { printf '%s\n' "$d"; return 0; } + done + return 1 +} + +# Jetson Multimedia API tree -- NvVideoEncoder sources and nvbufsurface.h. +find_mmapi() { + local d + for d in "$MMAPI_DIR" /usr/src/jetson_multimedia_api; do + [[ -f "$d/include/NvVideoEncoder.h" ]] && { printf '%s\n' "$d"; return 0; } + done + return 1 +} + +# Camera modules this rig actually populates. Everything else in the package -- +# S56C and SHF3L drivers and their NITOs -- is dead weight here, so it is +# neither compared nor installed. +SENSING_MODULES=(SHW5G) + +# Where the installed package is recorded, so a run can name the upstream commit +# it is on. SENSING reuses the package name across drops and does not bump +# Version.md, so this is the only place a version exists. +SENSING_STAMP=/var/lib/sensing/installed.json + +# Classify a package file by camera module: prints the module name, or "shared" +# for anything every module needs. Naming is the vendor's: one +# libnvuddf__cameramodule_library.so and one .nito per module. +sensing_file_module() { + local base module + base="$(basename "$1")" + case "$base" in + libnvuddf_*_cameramodule_library.so) + module="${base#libnvuddf_}"; module="${module%_cameramodule_library.so}" + printf '%s\n' "${module^^}" ;; + *.nito) printf '%s\n' "${base%.nito}" ;; + *) printf 'shared\n' ;; + esac +} + +# True if this rig uses that module, or if the file is shared. +sensing_module_used() { + local m + [[ "$1" == shared ]] && return 0 + for m in "${SENSING_MODULES[@]}"; do + [[ "$1" == "$m" ]] && return 0 + done + return 1 +} + +# What the vendor install.sh copies, as "sourcedestinationmodule" +# triples. Tab separated because the package path contains spaces. +# +# Mirrors install.sh, which globs -- keep sensing_check_installer honest about +# that rather than trusting this list to stay in step by itself. +sensing_payload() { + local pkg="$1" f + for f in "$pkg"/driver/libnvuddf* "$pkg"/driver/libnvsipl* "$pkg"/driver/libnvcamerahal.so; do + [[ -e "$f" ]] || continue + if [[ "$(basename "$f")" == libnvcamerahal.so ]]; then + printf '%s\t%s\t%s\n' "$f" "$SIPL_CAMERAHAL" shared + else + printf '%s\t%s\t%s\n' "$f" "$SIPL_DRV_DIR/$(basename "$f")" "$(sensing_file_module "$f")" + fi + done + for f in "$pkg"/nito/*; do + [[ -e "$f" ]] && printf '%s\t%s\t%s\n' \ + "$f" "$SIPL_NITO_DIR/$(basename "$f")" "$(sensing_file_module "$f")" + done + for f in nvsipl_camera nvsipl_query; do + [[ -e "$pkg/$f" ]] && printf '%s\t%s\t%s\n' "$pkg/$f" "$SIPL_SBIN_DIR/$f" shared + done + [[ -e "$pkg/dts/$SENSING_DTBO" ]] && printf '%s\t%s\t%s\n' \ + "$pkg/dts/$SENSING_DTBO" "$SIPL_BOOT_DIR/$SENSING_DTBO" shared + return 0 +} + +# Payload restricted to the modules this rig uses. +sensing_used_payload() { + local src dst module + while IFS=$'\t' read -r src dst module; do + sensing_module_used "$module" && printf '%s\t%s\t%s\n' "$src" "$dst" "$module" + done < <(sensing_payload "$1") + return 0 +} + +# Basenames of used files that differ from what is installed. Empty means the +# host already matches the package. +# +# Compared by content, not by version: a drop that only rebuilds drivers and +# retunes the ISP is invisible to anything that looks at names or timestamps. +sensing_stale_files() { + local src dst module + while IFS=$'\t' read -r src dst module; do + cmp -s "$src" "$dst" || printf '%s\n' "$(basename "$dst")" + done < <(sensing_used_payload "$1") + return 0 +} + +# Digest of the used payload, per module. Prints "modulesha256" lines. +# This is the version, since the vendor supplies none. +sensing_module_digests() { + local pkg="$1" module + for module in shared "${SENSING_MODULES[@]}"; do + local -a files=() + local src dst m + while IFS=$'\t' read -r src dst m; do + [[ "$m" == "$module" ]] && files+=("$src") + done < <(sensing_used_payload "$pkg") + [[ "${#files[@]}" -gt 0 ]] || continue + # Sorted so the digest depends on content, not on glob order. + printf '%s\t%s\n' "$module" \ + "$(printf '%s\n' "${files[@]}" | sort | tr '\n' '\0' | xargs -0 cat | sha256sum | cut -d' ' -f1)" + done +} + +# Every source install.sh copies, one per line, so we can prove our payload map +# covers it. Reimplementing the copy is what buys per-module scope; this is the +# check that stops a new vendor artifact from being silently dropped. +sensing_installer_sources() { + # Every argument of every `cp` except the flags and the trailing destination. + awk '/^[[:space:]]*sudo[[:space:]]+cp[[:space:]]/ { + for (i = 3; i < NF; i++) if ($i !~ /^-/) print $i + }' "$1/install.sh" +} + +# Fail if install.sh copies something sensing_payload does not model. We do our +# own copying to get per-module scope, so a new vendor artifact would otherwise +# be dropped in silence -- which is how a missing camera-module driver turns +# into a bare "SetPlatformCfg status: 10". +sensing_check_installer() { + local pkg="$1" src f missing=0 + local -A modelled=() + while IFS=$'\t' read -r f _ _; do modelled["$(basename "$f")"]=1; done < <(sensing_payload "$pkg") + while IFS= read -r src; do + # Expand the vendor's globs against the package to compare like for like. + for f in "$pkg"/$src; do + [[ -e "$f" ]] || continue + [[ -n "${modelled[$(basename "$f")]:-}" ]] && continue + warn "install.sh copies $(basename "$f"), which setup does not model" + missing=1 + done + done < <(sensing_installer_sources "$pkg") + return "$missing" +} + +# Copy the used payload into place. Replaces the vendor install.sh so that +# modules this rig does not have stay untouched; install.sh wipes +# /usr/lib/nvsipl_drv wholesale and copies all of them. +sensing_install() { + local pkg="$1" src dst module + while IFS=$'\t' read -r src dst module; do + sudo install -D -m "$(stat -c %a "$src")" "$src" "$dst" || return 1 + printf ' %s [%s]\n' "$(basename "$dst")" "$module" + done < <(sensing_used_payload "$pkg") +} + +# Record what was installed, per module. The commit is what to quote at SENSING, +# since the package name and Version.md are the same across drops. +sensing_write_stamp() { + local pkg="$1" name="$2" commit="$3" module digest + local -a lines=() + while IFS=$'\t' read -r module digest; do + lines+=(" \"$module\": \"sha256:$digest\"") + done < <(sensing_module_digests "$pkg") + local body + body="$(printf '%s,\n' "${lines[@]}")"; body="${body%,}" + sudo mkdir -p "$(dirname "$SENSING_STAMP")" || return 1 + { + printf '{\n "package": "%s",\n "commit": "%s",\n "installed": "%s",\n' \ + "$name" "$commit" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" + printf ' "modules": {\n%s\n }\n}\n' "$body" + } | sudo tee "$SENSING_STAMP" >/dev/null +} + +# Read one top-level string from the stamp. Empty if absent -- a host set up by +# hand has no stamp, which is why content comparison stays authoritative. +sensing_read_stamp() { + [[ -r "$SENSING_STAMP" ]] || return 1 + sed -n "s/.*\"$1\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\".*/\1/p" "$SENSING_STAMP" | head -1 +} + +# Commit the cached clone is on, short form. Empty if the package did not come +# from a clone (--pkg pointing at a hand-unpacked copy). +sensing_pkg_commit() { + git -C "$1" rev-parse --short HEAD 2>/dev/null || printf 'unknown\n' +} + +# Device nodes SIPL opens, as "path:description" pairs. Readability of each is +# the whole container-side health check: everything here is 0660 root:, +# so a missing supplementary group is the only realistic failure. +sensing_required_nodes() { + local bus + for bus in "${SENSING_I2C_BUSES[@]}"; do + printf '/dev/i2c-%s:MAX96712 deserializer bus\n' "$bus" + done + printf '/dev/gpiochip0:deserializer power enable\n' + printf '/dev/gpiochip1:deserializer power enable\n' + printf '/dev/capture-vi-channel0:VI capture channel\n' + printf '/dev/capture-isp-channel0:ISP channel\n' + printf '/dev/nvhost-ctrl-isp:ISP control\n' + printf '/dev/nvmap:GPU memory\n' + printf '/dev/v4l2-nvenc:H.264 encoder\n' +} + +# GIDs of nodes this user cannot read, deduplicated. Empty output means the +# process can reach everything SIPL needs. +sensing_missing_gids() { + local entry path gid seen="" + while IFS= read -r entry; do + path="${entry%%:*}" + [[ -e "$path" ]] || continue + [[ -r "$path" ]] && continue + gid="$(stat -c %g "$path" 2>/dev/null)" || continue + [[ " $seen " == *" $gid "* ]] && continue + seen="$seen $gid" + printf '%s\n' "$gid" + done < <(sensing_required_nodes) +} diff --git a/src/plugins/sensing/scripts/run_docker_example.sh b/src/plugins/sensing/scripts/run_docker_example.sh new file mode 100755 index 0000000000..bc9f97c006 --- /dev/null +++ b/src/plugins/sensing/scripts/run_docker_example.sh @@ -0,0 +1,136 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Build and start a container that can reach the cameras, then drop you at a +# shell in it. Run validate.sh from there to stream; it is the same script that +# runs on the host. +# +# Run this on the HOST: it reads the device nodes to derive what to mount, and +# passes host paths to `docker run`. +# +# The image side is Dockerfile.example plus image_setup.sh. The run side is the +# part no Dockerfile can express -- bind mounts, --device, and the NVIDIA +# runtime capabilities that make the BSP libraries visible. +# +# Usage: +# ./run_docker_example.sh # build, then an interactive shell +# ./run_docker_example.sh -- validate.sh # build, run that, exit +# +# Options: +# --build-only build the image and stop +# --no-build reuse an existing image +# --print-args print the `docker run` flags for your own container and stop +# --json the same flags as devcontainer.json runArgs elements +# --print-gids print "GID:NAME ..." for image_setup.sh and stop +# --tag NAME image tag (default: sensing-example) +# -h, --help this text + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=./common.sh +source "$SCRIPT_DIR/common.sh" + +PLUGIN_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +TAG=sensing-example +MODE=run +BUILD=1 +CMD=() + +usage() { sed -n '4,27p' "${BASH_SOURCE[0]}" | sed 's/^# \?//'; } + +while (( $# )); do + case "$1" in + --build-only) MODE=build; shift ;; + --no-build) BUILD=0; shift ;; + --print-args) MODE=args; shift ;; + --json) MODE=json; shift ;; + --print-gids) MODE=gids; shift ;; + --tag) TAG="$2"; shift 2 ;; + -h|--help) usage; exit 0 ;; + --) shift; CMD=("$@"); break ;; + *) die "unknown argument: $1" "Run $0 --help" ;; + esac +done + +in_container && die "run this on the HOST." \ + "It reads host device nodes and hands host paths to docker run." + +# GIDs owning the nodes SIPL opens. Only group-gated nodes matter: a +# world-readable one needs no supplementary group. Deduplicated, in first-seen +# order so the output is stable across runs. +node_gids() { + local entry path gid seen="" + while IFS= read -r entry; do + path="${entry%%:*}" + [[ -e "$path" ]] || continue + # o+r set means any uid can open it; no --group-add needed. + [[ "$(stat -c %A "$path")" == ???????r?? ]] && continue + gid="$(stat -c %g "$path" 2>/dev/null)" || continue + [[ " $seen " == *" $gid "* ]] && continue + seen="$seen $gid" + printf '%s\n' "$gid" + done < <(sensing_required_nodes) +} + +mapfile -t GIDS < <(node_gids) + +mapfile -t GIDS < <(node_gids) + +if [[ "$MODE" == gids ]]; then + out=() + for gid in "${GIDS[@]}"; do + out+=("$gid:$(getent group "$gid" | cut -d: -f1 || echo "grp$gid")") + done + printf '%s\n' "${out[*]}" + exit 0 +fi + +ARGS=() +# The NVIDIA container runtime mounts a fixed CSV file list, so it only carries +# what shipped with the BSP. The vendor install.sh ADDS files -- the +# libnvuddf_*_cameramodule_library.so drivers and the per-module .nito -- which +# appear on no CSV and are therefore invisible inside the container. Without +# them SIPL cannot instantiate the camera module and every client, NVIDIA's own +# nvsipl_camera included, fails at SetPlatformCfg with a bare "status: 10". +# Mount the directories, not the files, so a later vendor drop needs no change. +for dir in "$SIPL_DRV_DIR" "$SIPL_NITO_DIR"; do + [[ -d "$dir" ]] || { warn "$dir missing on this host — run setup.sh"; continue; } + ARGS+=(-v "$dir:$dir:ro") +done + +# The nvidia runtime injects the BSP libraries -- libnvsipl.so among them -- only +# when the capabilities are actually requested. Without these the binary loads +# and then dies on a missing shared object, naming nothing about cameras. +ARGS+=(--runtime=nvidia -e NVIDIA_VISIBLE_DEVICES=all -e NVIDIA_DRIVER_CAPABILITIES=all) +# --privileged opens the device cgroup. Group membership is necessary but not +# sufficient: with the cgroup closed every node is unreadable regardless. +ARGS+=(--privileged -v /dev:/dev) +# The plugin directory, at the same path, so validate.sh finds .cache/ and the +# vendored config exactly as it does on the host. +ARGS+=(-v "$PLUGIN_DIR:$PLUGIN_DIR:ro" -w "$PLUGIN_DIR") + +if [[ "$MODE" == args ]]; then + printf '%s\n' "${ARGS[*]}"; exit 0 +elif [[ "$MODE" == json ]]; then + for arg in "${ARGS[@]}"; do printf '"%s",\n' "$arg"; done; exit 0 +fi + +if [[ "$BUILD" -eq 1 ]]; then + step "Building $TAG" + docker build --network=host -t "$TAG" \ + --build-arg "SENSING_GIDS=$("$0" --print-gids)" \ + -f "$SCRIPT_DIR/../docker/Dockerfile.example" "$SCRIPT_DIR/../docker" \ + || die "build failed" + ok "$TAG" +fi +[[ "$MODE" != build ]] || exit 0 + +step "Starting $TAG" +if [[ "${#CMD[@]}" -eq 0 ]]; then + info "run ./validate.sh to stream both sensors" + CMD=(bash) + ARGS+=(-it) +fi +exec docker run --rm "${ARGS[@]}" "$TAG" "${CMD[@]}" diff --git a/src/plugins/sensing/scripts/setup.sh b/src/plugins/sensing/scripts/setup.sh new file mode 100755 index 0000000000..7f3cdd5778 --- /dev/null +++ b/src/plugins/sensing/scripts/setup.sh @@ -0,0 +1,265 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# HOST-side setup for the SENSING SG8A GMSL rig (two SHW5G modules) on an +# AGX Orin running JetPack 7.2.1 / L4T R39.2.1. +# +# Installing the vendor drivers and selecting the device-tree overlay are host +# operations, and the only ones needed to run the cameras. Containers need +# device access on top; see docker/setup_container.sh. +# +# There is NO per-boot step. The vendor install.sh copies userspace SIPL +# drivers, a DTBO and ISP tuning files, and that is all -- there are no kernel +# modules to insmod and no /dev/video* nodes to configure. +# +# Usage: +# ./setup.sh [options] +# +# Options: +# --pkg DIR vendor driver package dir (default: fetch from SENSING's repo) +# --no-fetch do not download the vendor package; fail if it is not on disk +# --install-drivers run the vendor install.sh unattended (otherwise it is offered) +# --groups add $USER to the i2c and gpio groups +# --smoke-test stream both sensors for a few seconds via nvsipl_camera +# -y, --yes assume yes for every prompt +# -h, --help this text + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=./common.sh +source "$SCRIPT_DIR/common.sh" + +PKG_DIR="" +NO_FETCH=0 +INSTALL_DRIVERS=0 +DO_GROUPS=ask +SMOKE_TEST=0 +ASSUME_YES=0 + +usage() { sed -n '4,27p' "${BASH_SOURCE[0]}" | sed 's/^# \?//'; } + +while (( $# )); do + case "$1" in + --pkg) PKG_DIR="$2"; shift 2 ;; + --no-fetch) NO_FETCH=1; shift ;; + --install-drivers) INSTALL_DRIVERS=1; shift ;; + --groups) DO_GROUPS=yes; shift ;; + --smoke-test) SMOKE_TEST=1; shift ;; + -y|--yes) ASSUME_YES=1; shift ;; + -h|--help) usage; exit 0 ;; + *) die "unknown argument: $1" "Run $0 --help" ;; + esac +done +export ASSUME_YES + +# --- Preconditions ---------------------------------------------------------- +if in_container; then + die "this script must run on the HOST, not inside a container." \ + "Open a host terminal and run: $SCRIPT_DIR/setup.sh + Inside a container, run $SCRIPT_DIR/../docker/setup_container.sh instead." +fi +[[ "$(uname -m)" == "aarch64" ]] || die "this rig is Jetson-only (found $(uname -m) )." + +# Refresh unconditionally unless told otherwise. SENSING reuses the package +# name across drops, so a copy already on disk says nothing about whether it is +# current -- skipping the fetch when one exists is how you end up installing +# last month's ISP tuning. +if [[ -n "$PKG_DIR" ]]; then + info "using $PKG_DIR (--pkg)" +elif [[ "$NO_FETCH" -eq 1 ]]; then + PKG_DIR="$(find_sensing_pkg)" +else + step "Vendor driver package" + PKG_DIR="$(fetch_sensing_pkg)" || { + warn "fetch failed — falling back to a copy already on disk" + PKG_DIR="$(find_sensing_pkg)" + } +fi +[[ -n "$PKG_DIR" && -f "$PKG_DIR/install.sh" ]] || die \ + "SENSING driver package not found." \ + "Check network access to $SENSING_PKG_REPO, + or obtain the package by hand and pass --pkg /path/to/package." +PKG_DIR="$(cd "$PKG_DIR" && pwd)" + +printf '%sSENSING host setup (SIPL)%s\n' "$C_BOLD" "$C_RESET" +info "package: $PKG_DIR" +info "config: $SENSING_PLATFORM_CONFIG ($SENSING_CONFIG_JSON_REL)" +info "L4T: R$(l4t_release || echo unknown)" + +# --- Platform config -------------------------------------------------------- +# The published package has no SHW5G_2 config: SENSING ships shw5g.json only to +# customers with a 2x SHW5G rig. Without it nvsipl_camera has nothing to load +# for this population, so drop the vendored copy in. +step "Platform config" +CFG_DST="$PKG_DIR/$SENSING_CONFIG_JSON_REL" +CFG_SRC="$SENSING_CONFIGS_DIR/$(basename "$SENSING_CONFIG_JSON_REL")" +if [[ -f "$CFG_DST" ]]; then + ok "$SENSING_CONFIG_JSON_REL (shipped in this package)" +elif [[ -r "$CFG_SRC" ]]; then + mkdir -p "$(dirname "$CFG_DST")" 2>/dev/null || true + if cp "$CFG_SRC" "$CFG_DST" 2>/dev/null; then + ok "installed $SENSING_CONFIG_JSON_REL from configs/" + else + warn "could not write $CFG_DST" + hint "nvsipl_camera needs -t pointing at a config that defines $SENSING_PLATFORM_CONFIG" + fi +else + bad "no $SENSING_PLATFORM_CONFIG config available" + hint "Expected $CFG_SRC" +fi + +# --- Installed vs package --------------------------------------------------- +step "Installed vs package (modules: ${SENSING_MODULES[*]})" +PKG_COMMIT="$(sensing_pkg_commit "$PKG_DIR")" +INSTALLED_COMMIT="$(sensing_read_stamp commit || true)" +if [[ -z "$INSTALLED_COMMIT" ]]; then + info "package $SENSING_PKG_NAME @ $PKG_COMMIT; nothing recorded as installed" +elif [[ "$INSTALLED_COMMIT" == "$PKG_COMMIT" ]]; then + info "package $SENSING_PKG_NAME @ $PKG_COMMIT (installed)" +else + warn "installed @ $INSTALLED_COMMIT, package is @ $PKG_COMMIT" +fi +sensing_check_installer "$PKG_DIR" || \ + warn "the package copies files this script does not know about; run install.sh by hand" + +STALE=() +while IFS=$'\t' read -r src dst module; do + base="$(basename "$dst")" + if ! sensing_module_used "$module"; then + printf ' %s-%s %-46s [%s] not on this rig\n' "$C_DIM" "$C_RESET" "$base" "$module" + elif cmp -s "$src" "$dst"; then + printf ' %s✓%s %-46s [%s]\n' "$C_GREEN" "$C_RESET" "$base" "$module" + else + printf ' %s!%s %-46s [%s] %sstale%s\n' "$C_YELLOW$C_BOLD" "$C_RESET" "$base" "$module" "$C_YELLOW" "$C_RESET" + STALE+=("$base") + fi +done < <(sensing_payload "$PKG_DIR") + +NEED_INSTALL=0 +if [[ "$INSTALL_DRIVERS" -eq 1 ]]; then + NEED_INSTALL=1 +elif [[ "${#STALE[@]}" -gt 0 ]]; then + NEED_INSTALL=1 +fi +[[ "$NEED_INSTALL" -eq 1 ]] || ok "up to date" + +SUDO_REASONS=() +[[ "$NEED_INSTALL" -eq 0 ]] || SUDO_REASONS+=( + "installing the vendor SIPL drivers, device-tree overlay and ISP tuning files") +[[ "$DO_GROUPS" == no ]] || SUDO_REASONS+=( + "adding $USER to the i2c and gpio groups (you will be asked first)") +[[ "${#SUDO_REASONS[@]}" -eq 0 ]] || require_sudo "${SUDO_REASONS[@]}" + +# --- 1. Install the vendor artifacts ---------------------------------------- +# Not the vendor install.sh: that wipes $SIPL_DRV_DIR and copies every module's +# driver, so a rig with one module carries three. sensing_check_installer above +# is what keeps this copy in step with theirs. +if [[ "$NEED_INSTALL" -eq 1 ]]; then + step "Installing SIPL drivers, overlay and ISP tuning" + warn "This overwrites $SIPL_CAMERAHAL and the ${SENSING_MODULES[*]} files in $SIPL_DRV_DIR." + if confirm "Proceed?"; then + sensing_install "$PKG_DIR" || die "install failed" "Re-run with sudo available." + sensing_write_stamp "$PKG_DIR" "$SENSING_PKG_NAME" "$(sensing_pkg_commit "$PKG_DIR")" \ + && ok "recorded in $SENSING_STAMP" + # install.sh only copies files. The overlay is the sole artifact the + # kernel reads at boot, so a drop that leaves it alone -- a driver + # rebuild or an ISP retune -- takes effect on the next process start. + if [[ " ${STALE[*]} " == *" $SENSING_DTBO "* ]]; then + printf '\n%s%sReboot required.%s Select the overlay first:\n' "$C_YELLOW" "$C_BOLD" "$C_RESET" + hint "sudo /opt/nvidia/jetson-io/jetson-io.py" + hint " Configure Jetson AGX CSI Connector" + hint " -> Configure for compatible hardware" + hint " -> $SENSING_OVERLAY_NAME" + hint " -> Save pin changes -> Save and reboot to reconfigure pins" + printf '\nAfter the reboot, set maximum performance and re-run this script:\n' + hint "sudo nvpmodel -m 0 && sudo jetson_clocks" + exit 0 + fi + ok "overlay unchanged — no reboot needed" + STALE=() + fi + warn "skipped vendor install.sh" +fi + +# --- 2. Vendor artifacts in place ------------------------------------------- +step "Vendor artifacts" +[[ -f /boot/tegra234-camera-sipl-camera-overlay.dtbo ]] \ + && ok "/boot/tegra234-camera-sipl-camera-overlay.dtbo" \ + || { bad "device-tree overlay not installed"; hint "Re-run with --install-drivers"; } +if compgen -G "$SIPL_DRV_DIR/libnvuddf_*" >/dev/null; then + ok "$SIPL_DRV_DIR ($(find "$SIPL_DRV_DIR" -name 'libnv*' | wc -l) driver libs)" +else + bad "$SIPL_DRV_DIR is empty"; hint "Re-run with --install-drivers" +fi +[[ -r "$SIPL_NITO_DIR/$SENSING_NITO" ]] \ + && ok "$SIPL_NITO_DIR/$SENSING_NITO" \ + || { bad "$SENSING_NITO missing — the ISP has no tuning to load"; hint "Re-run with --install-drivers"; } + +# --- 3. Group membership ---------------------------------------------------- +# SIPL needs no root, but it does need the groups owning the deserializer I2C +# buses and the GPIO chips that gate deserializer power. +step "Group membership" +mapfile -t MISSING_GIDS < <(sensing_missing_gids) +if [[ "${#MISSING_GIDS[@]}" -eq 0 ]]; then + ok "$USER can already reach every node SIPL opens" +else + names=() + for gid in "${MISSING_GIDS[@]}"; do + names+=("$(getent group "$gid" | cut -d: -f1 || echo "$gid")") + done + info "missing: ${names[*]}" + add_groups=0 + case "$DO_GROUPS" in + yes) add_groups=1 ;; + no) info "skipped" ;; + ask) confirm "Add $USER to ${names[*]}?" && add_groups=1 ;; + esac + if [[ "$add_groups" -eq 1 ]]; then + sudo usermod -aG "$(IFS=,; echo "${names[*]}")" "$USER" + ok "added — log out and back in for it to take effect" + warn "Group changes do not apply to this shell. Re-login before the smoke test." + else + warn "without these, SIPL fails with: Master SetPlatformConfig (Camera HAL) failed. status: 10" + fi +fi + +# --- 4. Performance mode ---------------------------------------------------- +step "Performance mode" +if have nvpmodel; then + mode="$(nvpmodel -q 2>/dev/null | sed -n 's/^NV Power Mode: //p' | head -1)" + info "power mode: ${mode:-unknown}" + hint "Two sensors at 2560x1984@60 want: sudo nvpmodel -m 0 && sudo jetson_clocks" +else + info "nvpmodel not found" +fi + +# --- 5. Optional smoke test ------------------------------------------------- +# The honest overlay check. If the DTBO is not selected, or the modules are on +# the wrong ports, this is what says so. +if [[ "$SMOKE_TEST" -eq 1 ]]; then + step "Vendor smoke test" + if ! have nvsipl_camera; then + bad "nvsipl_camera not on PATH — installed by the vendor install.sh" + else + info "streaming $SENSING_PLATFORM_CONFIG for 5s" + ( cd "$PKG_DIR" && nvsipl_camera \ + -t "$SENSING_CONFIG_JSON_REL" -c "$SENSING_PLATFORM_CONFIG" \ + -m "$SENSING_LINK_MASKS" --enable-camera-hal -s -Z -r 5 ) \ + && ok "both sensors streamed" \ + || bad "smoke test failed — check cabling on CN1 CAM2/CAM3 and the selected overlay" + fi +fi + +step "Verifying" +"$SCRIPT_DIR/verify.sh" || true + +printf '\n%s%sHost setup complete.%s\n' "$C_GREEN" "$C_BOLD" "$C_RESET" +# Validation is the point of the whole exercise, so hand over the exact command +# rather than a pointer to the docs. +printf '\nValidate the rig:\n' +hint "$SCRIPT_DIR/validate.sh" +printf '\nTo build against this rig, run %sdocker/setup_container.sh%s in your container,\n' \ + "$C_BOLD" "$C_RESET" +printf 'or %srun_docker_example.sh%s for a ready-made one.\n' "$C_BOLD" "$C_RESET" diff --git a/src/plugins/sensing/scripts/validate.sh b/src/plugins/sensing/scripts/validate.sh new file mode 100755 index 0000000000..233a4bf767 --- /dev/null +++ b/src/plugins/sensing/scripts/validate.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Stream both sensors with the vendor's nvsipl_camera. This is the ground truth +# for "is the rig alive": same SIPL API as any other client, so if it cannot +# stream, nothing else will either. +# +# Runs unchanged on the host and inside a container -- in a container it is what +# proves the device access, the driver mounts and the group membership are all +# right, before any of our code is involved. +# +# Usage: +# ./validate.sh [options] [-- extra nvsipl_camera args] +# +# Options: +# -t, --seconds N stream for N seconds (default 10; 0 runs until 'q') +# -q, --query only ask the config what exists; needs no hardware +# -h, --help this text + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=./common.sh +source "$SCRIPT_DIR/common.sh" + +SECONDS_ARG=10 +QUERY_ONLY=0 +EXTRA=() + +usage() { sed -n '4,19p' "${BASH_SOURCE[0]}" | sed 's/^# \?//'; } + +while (( $# )); do + case "$1" in + -t|--seconds) SECONDS_ARG="$2"; shift 2 ;; + -q|--query) QUERY_ONLY=1; shift ;; + -h|--help) usage; exit 0 ;; + --) shift; EXTRA=("$@"); break ;; + *) die "unknown argument: $1" "Run $0 --help" ;; + esac +done + +have nvsipl_camera || die "nvsipl_camera is not on PATH." \ + "On the host, run $SCRIPT_DIR/setup.sh. + In a container, the host's /usr/sbin is not visible -- use + $SCRIPT_DIR/run_docker_example.sh, which mounts the package." + +PKG_DIR="$(find_sensing_pkg)" +[[ -n "$PKG_DIR" ]] || die "vendor package not found." "Run $SCRIPT_DIR/setup.sh first." + +# nvsipl_camera resolves -t against the working directory, and the config lives +# in the package. +cd "$PKG_DIR" +[[ -f "$SENSING_CONFIG_JSON_REL" ]] || die \ + "$SENSING_CONFIG_JSON_REL missing from $PKG_DIR." \ + "The published package has no $SENSING_PLATFORM_CONFIG config; + $SCRIPT_DIR/setup.sh drops the vendored copy in." + +if [[ "$QUERY_ONLY" -eq 1 ]]; then + step "Querying $SENSING_PLATFORM_CONFIG (no hardware needed)" + exec nvsipl_query -t "$SENSING_CONFIG_JSON_REL" -c "$SENSING_PLATFORM_CONFIG" +fi + +# -Z: these modules carry no auth keys. --enable-camera-hal: the legacy DevBlk +# path does not support this carrier. +ARGS=(-t "$SENSING_CONFIG_JSON_REL" -c "$SENSING_PLATFORM_CONFIG" + -m "$SENSING_LINK_MASKS" --enable-camera-hal -s -Z) +[[ "$SECONDS_ARG" -eq 0 ]] || ARGS+=(-r "$SECONDS_ARG") + +step "Streaming $SENSING_PLATFORM_CONFIG" +info "$PKG_DIR" +warn "SIPL is exclusive: stop any other client first." +nvsipl_camera "${ARGS[@]}" "${EXTRA[@]}" +ok "streamed without error" diff --git a/src/plugins/sensing/scripts/verify.sh b/src/plugins/sensing/scripts/verify.sh new file mode 100755 index 0000000000..59e0ba4620 --- /dev/null +++ b/src/plugins/sensing/scripts/verify.sh @@ -0,0 +1,133 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Read-only health report for the SENSING SG8A SIPL rig. Takes no action, needs +# no sudo, and runs identically on the host and in a container. +# +# Exit status: 0 if the rig looks capturable from here, 1 otherwise. +# +# Usage: +# ./verify.sh [-q|--quiet] + +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=./common.sh +source "$SCRIPT_DIR/common.sh" + +QUIET=0 +case "${1:-}" in + -q|--quiet) QUIET=1 ;; + -h|--help) sed -n '4,12p' "${BASH_SOURCE[0]}" | sed 's/^# \?//'; exit 0 ;; +esac +[[ "$QUIET" -eq 0 ]] || { ok() { :; }; info() { :; }; step() { :; }; } + +FAIL=0 +note_fail() { FAIL=1; } + +printf '%sSENSING rig report%s %s(%s)%s\n' "$C_BOLD" "$C_RESET" \ + "$C_DIM" "$(in_container && echo container || echo host)" "$C_RESET" + +# --- Platform --------------------------------------------------------------- +step "Platform" +rel="$(l4t_release || true)" +if [[ -n "$rel" ]]; then + ok "L4T R$rel" + [[ "$rel" == 39.* ]] || { warn "this plugin targets L4T R39.x (JetPack 7.x)"; } +else + bad "cannot read /etc/nv_tegra_release"; note_fail +fi +[[ -r /proc/device-tree/model ]] \ + && info "$(tr -d '\0' < /proc/device-tree/model)" + +# --- Device nodes ----------------------------------------------------------- +step "Device nodes" +while IFS= read -r entry; do + path="${entry%%:*}"; desc="${entry#*:}" + if [[ ! -e "$path" ]]; then + bad "$path absent — $desc"; note_fail + elif [[ ! -r "$path" ]]; then + bad "$path unreadable (gid $(stat -c %g "$path")) — $desc"; note_fail + else + ok "$path" + fi +done < <(sensing_required_nodes) + +mapfile -t MISSING_GIDS < <(sensing_missing_gids) +if [[ "${#MISSING_GIDS[@]}" -gt 0 ]]; then + bad "missing supplementary group(s): ${MISSING_GIDS[*]}" + hint "SIPL will fail with: Master SetPlatformConfig (Camera HAL) failed. status: 10" + if in_container; then + # Group membership comes from the image: Docker resolves supplementary + # groups for the container user from its own /etc/group, which covers + # PID 1 and `docker exec` alike. --group-add on the run adds nothing. + hint "Add the gids to the IMAGE and rebuild — a restart will not do it:" + hint " RUN groupadd -g && usermod -aG \$USERNAME" + hint "docker/image_setup.sh does exactly that, and another Dockerfile" + hint "can bind-mount and run it. Gids for this host:" + for gid in "${MISSING_GIDS[@]}"; do hint " $gid ($(getent group "$gid" | cut -d: -f1))"; done + else + hint "sudo usermod -aG i2c,gpio \$USER (then log out and back in)" + fi +fi + +# --- SIPL runtime ----------------------------------------------------------- +step "SIPL runtime" +# Snapshot once: `grep -q` exits at the first match, which SIGPIPEs ldconfig, +# and under `set -o pipefail` that makes a *successful* match look like failure. +LDCACHE="$(ldconfig -p 2>/dev/null || true)" +for lib in nvsipl nvsipl_query nvscibuf nvscisync nvbufsurface; do + if grep -q "lib${lib}\.so" <<<"$LDCACHE"; then ok "lib${lib}.so" + else bad "lib${lib}.so not resolvable"; note_fail; fi +done +if compgen -G "$SIPL_DRV_DIR/libnvuddf_*" >/dev/null; then + ok "$SIPL_DRV_DIR ($(find "$SIPL_DRV_DIR" -name 'libnv*' 2>/dev/null | wc -l) driver libs)" +else + bad "$SIPL_DRV_DIR empty or absent — run the vendor install.sh on the host"; note_fail +fi +[[ -r "$SIPL_NITO_DIR/$SENSING_NITO" ]] \ + && ok "$SIPL_NITO_DIR/$SENSING_NITO" \ + || { bad "$SENSING_NITO missing — the ISP has no tuning"; note_fail; } + +# --- Overlay ---------------------------------------------------------------- +step "Device-tree overlay" +if [[ -f /boot/tegra234-camera-sipl-camera-overlay.dtbo ]]; then + ok "dtbo installed" + info "whether it is *selected* only shows up when capture is attempted" + hint "validate.sh" +else + if in_container; then + info "/boot not visible from here" + else + bad "overlay not installed — setup.sh --install-drivers"; note_fail + fi +fi + +# --- Build SDKs ------------------------------------------------------------- +step "Build SDKs" +if mmapi="$(find_mmapi)"; then ok "multimedia api: $mmapi" +else bad "jetson_multimedia_api not found — docker/setup_container.sh fetches it"; note_fail; fi +if sipl="$(find_sipl_api)"; then ok "sipl api: $sipl" +else bad "jetson_sipl_api not found — docker/setup_container.sh fetches it"; note_fail; fi + +# --- Vendor package --------------------------------------------------------- +step "Vendor package" +pkg="$(find_sensing_pkg)" +if [[ -n "$pkg" ]]; then + ok "$pkg" + [[ -f "$pkg/$SENSING_CONFIG_JSON_REL" ]] \ + && ok "$SENSING_CONFIG_JSON_REL" \ + || { bad "platform config missing from the package"; note_fail; } +else + info "not found (only needed for the vendor smoke test)" +fi +have nvsipl_camera && ok "nvsipl_camera on PATH" || info "nvsipl_camera not on PATH" + +printf '\n' +if [[ "$FAIL" -eq 0 ]]; then + printf '%s%sRig looks capturable from here.%s\n' "$C_GREEN" "$C_BOLD" "$C_RESET" +else + printf '%s%sRig is not ready — see the failures above.%s\n' "$C_YELLOW" "$C_BOLD" "$C_RESET" +fi +exit "$FAIL"