From ab2a8bfa21b6014fec8ffcfe00b3383d849b7be3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Jun 2026 15:56:43 +0000 Subject: [PATCH 1/4] Initial plan From ea4a6042352c389e0d10a210402716a5c878f729 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Jun 2026 16:04:16 +0000 Subject: [PATCH 2/4] Add rpicam-* application naming support to Camera binding --- src/devices/Camera/Camera/Camera.csproj | 7 ++ .../Camera/Settings/ProcessSettingsFactory.cs | 116 ++++++++++++++++++ src/devices/Camera/CameraInsights.md | 20 ++- src/devices/Camera/README.md | 8 +- .../Camera/samples/Camera.Samples/Program.cs | 28 ++++- .../TestCamera/ProcessSettingsFactoryTests.cs | 70 +++++++++++ 6 files changed, 237 insertions(+), 12 deletions(-) create mode 100644 src/devices/Camera/tests/TestCamera/ProcessSettingsFactoryTests.cs diff --git a/src/devices/Camera/Camera/Camera.csproj b/src/devices/Camera/Camera/Camera.csproj index 673ec0371a..0f8028d56c 100644 --- a/src/devices/Camera/Camera/Camera.csproj +++ b/src/devices/Camera/Camera/Camera.csproj @@ -10,4 +10,11 @@ + + + + <_Parameter1>TestCamera + + + diff --git a/src/devices/Camera/Camera/Settings/ProcessSettingsFactory.cs b/src/devices/Camera/Camera/Settings/ProcessSettingsFactory.cs index 012f856ea3..4b7eb1cdc0 100644 --- a/src/devices/Camera/Camera/Settings/ProcessSettingsFactory.cs +++ b/src/devices/Camera/Camera/Settings/ProcessSettingsFactory.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -36,6 +37,29 @@ public static class ProcessSettingsFactory /// public const string LibcameraVid = "libcamera-vid"; + /// + /// The process name of the rpicam application used to capture still pictures on the Raspbian OS. + /// This is the new name for the application introduced with Raspberry Pi OS Bookworm. + /// + public const string RpicamStill = "rpicam-still"; + + /// + /// The process name of the rpicam application used to capture video streams on the Raspbian OS. + /// This is the new name for the application introduced with Raspberry Pi OS Bookworm. + /// + public const string RpicamVid = "rpicam-vid"; + + /// + /// Returns true when the new rpicam-apps (rpicam-still / rpicam-vid) are available on the system path. + /// Starting with Raspberry Pi OS Bookworm the libcamera-* applications have been renamed to rpicam-*. + /// + /// True if the rpicam-apps are installed, otherwise false. + public static bool IsRpicamAppsInstalled() + => IsRpicamAppsInstalled(IsApplicationOnPath); + + internal static bool IsRpicamAppsInstalled(Func applicationExists) + => applicationExists(RpicamStill) || applicationExists(RpicamVid); + /// /// Creates a ProcessSettings instance targeting raspistill. /// @@ -91,4 +115,96 @@ public static ProcessSettings CreateForLibcameravid() settings.Filename = LibcameraVid; return settings; } + + /// + /// Creates a ProcessSettings instance targeting rpicam-still and capturing stderr. + /// + /// An instance of the class + public static ProcessSettings CreateForRpicamstillAndStderr() + { + var settings = new ProcessSettings(); + settings.Filename = RpicamStill; + settings.CaptureStderrInsteadOfStdout = true; + return settings; + } + + /// + /// Creates a ProcessSettings instance targeting rpicam-still. + /// + /// An instance of the class + public static ProcessSettings CreateForRpicamstill() + { + var settings = new ProcessSettings(); + settings.Filename = RpicamStill; + return settings; + } + + /// + /// Creates a ProcessSettings instance targeting rpicam-vid. + /// + /// An instance of the class + public static ProcessSettings CreateForRpicamvid() + { + var settings = new ProcessSettings(); + settings.Filename = RpicamVid; + return settings; + } + + /// + /// Creates a ProcessSettings instance for capturing still pictures using the libcamera/rpicam stack + /// and capturing stderr. The new rpicam-still application is used when available, otherwise the + /// legacy libcamera-still name is used (which remains available as a symbolic link on Bookworm). + /// + /// An instance of the class + public static ProcessSettings CreateForStillAndStderr() + => IsRpicamAppsInstalled() ? CreateForRpicamstillAndStderr() : CreateForLibcamerastillAndStderr(); + + /// + /// Creates a ProcessSettings instance for capturing still pictures using the libcamera/rpicam stack. + /// The new rpicam-still application is used when available, otherwise the legacy libcamera-still name + /// is used (which remains available as a symbolic link on Bookworm). + /// + /// An instance of the class + public static ProcessSettings CreateForStill() + => IsRpicamAppsInstalled() ? CreateForRpicamstill() : CreateForLibcamerastill(); + + /// + /// Creates a ProcessSettings instance for capturing video streams using the libcamera/rpicam stack. + /// The new rpicam-vid application is used when available, otherwise the legacy libcamera-vid name + /// is used (which remains available as a symbolic link on Bookworm). + /// + /// An instance of the class + public static ProcessSettings CreateForVid() + => IsRpicamAppsInstalled() ? CreateForRpicamvid() : CreateForLibcameravid(); + + private static bool IsApplicationOnPath(string fileName) + { + var pathVariable = Environment.GetEnvironmentVariable("PATH"); + if (string.IsNullOrEmpty(pathVariable)) + { + return false; + } + + foreach (var directory in pathVariable!.Split(Path.PathSeparator)) + { + if (string.IsNullOrWhiteSpace(directory)) + { + continue; + } + + try + { + if (File.Exists(Path.Combine(directory.Trim(), fileName))) + { + return true; + } + } + catch (ArgumentException) + { + // Ignore invalid entries in the PATH variable + } + } + + return false; + } } diff --git a/src/devices/Camera/CameraInsights.md b/src/devices/Camera/CameraInsights.md index 4eb0e82a4d..3d44ec251a 100644 --- a/src/devices/Camera/CameraInsights.md +++ b/src/devices/Camera/CameraInsights.md @@ -87,13 +87,25 @@ The `OS` versions are listed here: [Raspbian OS versions](https://www.raspberryp The utilities to capture pictures or videos are fully [described in the documentation](https://www.raspberrypi.com/documentation/computers/camera_software.html). The two main utilities are the following: -| Feature | `raspicam` utilities | `libcamera` utilities | -| -------------- | -------------------- | --------------------- | -| Still pictures | `raspistill` | `libcamera-still` | -| video | `raspivid` | `libcamera-vid` | +| Feature | `raspicam` utilities | `libcamera` utilities | `rpicam` utilities | +| -------------- | -------------------- | --------------------- | ------------------ | +| Still pictures | `raspistill` | `libcamera-still` | `rpicam-still` | +| video | `raspivid` | `libcamera-vid` | `rpicam-vid` | The command line described in the documentation is very similar for both the stacks. +### The `libcamera-*` to `rpicam-*` rename + +Starting with `Raspberry Pi OS Bookworm`, the `libcamera-apps` have been [renamed to `rpicam-apps`](https://github.com/raspberrypi/rpicam-apps/blob/main/README.md) (for example `libcamera-still` becomes `rpicam-still` and `libcamera-vid` becomes `rpicam-vid`). Users are encouraged to adopt the new application names as soon as possible. + +For backward compatibility, the older `libcamera-*` names are still provided on `Bookworm` as symbolic links pointing to the new `rpicam-*` executables, so existing code keeps working. + +The `ProcessSettingsFactory` exposes dedicated methods for each naming: + +- `CreateForLibcamerastill` / `CreateForLibcameravid` always use the legacy `libcamera-*` names. +- `CreateForRpicamstill` / `CreateForRpicamvid` always use the new `rpicam-*` names. +- `CreateForStill` / `CreateForVid` (and the related `*AndStderr` variants) automatically pick the `rpicam-*` applications when they are available on the system path, falling back to the `libcamera-*` names otherwise. These are the recommended methods because they keep working across all the supported OS releases. + From `Bullseye` on, the `raspi-config` app allows to re-enable or disable the legacy camera stack. ```bash diff --git a/src/devices/Camera/README.md b/src/devices/Camera/README.md index 479860b8c8..ae5b26b5ea 100644 --- a/src/devices/Camera/README.md +++ b/src/devices/Camera/README.md @@ -94,9 +94,11 @@ Once the `ProcessRunner` has been created and the command line has been configur The `ProcessSettingsFactory` exposes a few methods to prepare an instance of the `ProcessSettings` class with the correct application name. ```csharp -var processSettings = ProcessSettingsFactory.CreateForLibcamerastill(); +var processSettings = ProcessSettingsFactory.CreateForStill(); ``` +> **The `libcamera-*` applications have been renamed to `rpicam-*`** starting with `Raspberry Pi OS Bookworm`. The `CreateForStill`, `CreateForVid` and `CreateForStillAndStderr` methods automatically select the new `rpicam-*` applications when they are installed, falling back to the legacy `libcamera-*` names otherwise. If you want to force a specific naming, use `CreateForRpicamstill`/`CreateForRpicamvid` or `CreateForLibcamerastill`/`CreateForLibcameravid` respectively. + In addition to the application name, the `ProcessSettings` has other two parameters: - `BufferSize` is the size of the buffer used when copying data from `stdin` and the target stream @@ -174,10 +176,10 @@ The `try/catch` block is necessary because the `Dispose` method also cancel the ### Listing the available cameras -This code is only supported with the `libcamera` stack and can be used with the `libcamera-still` or `libcamera-vid` applications, but remember that those two apps send the text output on `stderr` and not `stdout`. +This code is only supported with the `libcamera`/`rpicam` stack and can be used with the `libcamera-still`/`rpicam-still` or `libcamera-vid`/`rpicam-vid` applications, but remember that those apps send the text output on `stderr` and not `stdout`. ```csharp -var processSettings = ProcessSettingsFactory.CreateForLibcamerastillAndStderr(); +var processSettings = ProcessSettingsFactory.CreateForStillAndStderr(); using var proc = new ProcessRunner(processSettings); var text = await proc.ExecuteReadOutputAsStringAsync(string.Empty); IEnumerable cameras = await CameraInfo.From(text); diff --git a/src/devices/Camera/samples/Camera.Samples/Program.cs b/src/devices/Camera/samples/Camera.Samples/Program.cs index ac5c407581..62702c781f 100644 --- a/src/devices/Camera/samples/Camera.Samples/Program.cs +++ b/src/devices/Camera/samples/Camera.Samples/Program.cs @@ -16,6 +16,12 @@ internal class Program private const string CmdStillLibcamera = "still-libcamera"; private const string CmdVideoLibcamera = "video-libcamera"; private const string CmdLapseLibcamera = "lapse-libcamera"; + private const string CmdStillRpicam = "still-rpicam"; + private const string CmdVideoRpicam = "video-rpicam"; + private const string CmdLapseRpicam = "lapse-rpicam"; + private const string CmdStill = "still"; + private const string CmdVideo = "video"; + private const string CmdLapse = "lapse"; private static async Task Main(string[] args) { @@ -27,13 +33,19 @@ private static async Task Main(string[] args) var arg = args[0]; ProcessSettings? processSettings = arg switch { - CmdList => ProcessSettingsFactory.CreateForLibcamerastillAndStderr(), + CmdList => ProcessSettingsFactory.CreateForStillAndStderr(), CmdStillLegacy => ProcessSettingsFactory.CreateForRaspistill(), CmdVideoLegacy => ProcessSettingsFactory.CreateForRaspivid(), CmdLapseLegacy => ProcessSettingsFactory.CreateForRaspistill(), CmdStillLibcamera => ProcessSettingsFactory.CreateForLibcamerastill(), CmdVideoLibcamera => ProcessSettingsFactory.CreateForLibcameravid(), CmdLapseLibcamera => ProcessSettingsFactory.CreateForLibcamerastill(), + CmdStillRpicam => ProcessSettingsFactory.CreateForRpicamstill(), + CmdVideoRpicam => ProcessSettingsFactory.CreateForRpicamvid(), + CmdLapseRpicam => ProcessSettingsFactory.CreateForRpicamstill(), + CmdStill => ProcessSettingsFactory.CreateForStill(), + CmdVideo => ProcessSettingsFactory.CreateForVid(), + CmdLapse => ProcessSettingsFactory.CreateForStill(), _ => null, }; @@ -55,7 +67,7 @@ private static async Task Main(string[] args) return 0; } - if (arg == CmdStillLegacy || arg == CmdStillLibcamera) + if (arg == CmdStillLegacy || arg == CmdStillLibcamera || arg == CmdStillRpicam || arg == CmdStill) { var filename = await capture.CaptureStill(); Console.WriteLine($"Captured the picture: {filename}"); @@ -63,7 +75,7 @@ private static async Task Main(string[] args) return 0; } - if (arg == CmdVideoLegacy || arg == CmdVideoLibcamera) + if (arg == CmdVideoLegacy || arg == CmdVideoLibcamera || arg == CmdVideoRpicam || arg == CmdVideo) { var filename = await capture.CaptureVideo(); Console.WriteLine($"Captured the video: {filename}"); @@ -71,7 +83,7 @@ private static async Task Main(string[] args) return 0; } - if (arg == CmdLapseLegacy || arg == CmdLapseLibcamera) + if (arg == CmdLapseLegacy || arg == CmdLapseLibcamera || arg == CmdLapseRpicam || arg == CmdLapse) { await capture.CaptureTimelapse(); Console.WriteLine($"The time-lapse images have been saved to disk"); @@ -85,13 +97,19 @@ private static async Task Main(string[] args) private static int Usage() { Console.WriteLine($"Camera.Samples supports one the following arguments:"); - Console.WriteLine($"{CmdList} print the cameras available on (libcamera stack only)"); + Console.WriteLine($"{CmdList} print the cameras available on (libcamera/rpicam stack only)"); Console.WriteLine($"{CmdStillLegacy} capture a still using raspistill"); Console.WriteLine($"{CmdVideoLegacy} capture a 10s video using raspivid"); Console.WriteLine($"{CmdLapseLegacy} capture a time lapse (5 images for 10s) using raspistill"); Console.WriteLine($"{CmdStillLibcamera} capture a still using libcamera-still"); Console.WriteLine($"{CmdVideoLibcamera} capture a 10s video using libcamera-vid"); Console.WriteLine($"{CmdLapseLibcamera} capture a time lapse (5 images for 10s) using libcamera-still"); + Console.WriteLine($"{CmdStillRpicam} capture a still using rpicam-still"); + Console.WriteLine($"{CmdVideoRpicam} capture a 10s video using rpicam-vid"); + Console.WriteLine($"{CmdLapseRpicam} capture a time lapse (5 images for 10s) using rpicam-still"); + Console.WriteLine($"{CmdStill} capture a still auto-selecting rpicam-still or libcamera-still"); + Console.WriteLine($"{CmdVideo} capture a 10s video auto-selecting rpicam-vid or libcamera-vid"); + Console.WriteLine($"{CmdLapse} capture a time lapse auto-selecting rpicam-still or libcamera-still"); return -1; } } diff --git a/src/devices/Camera/tests/TestCamera/ProcessSettingsFactoryTests.cs b/src/devices/Camera/tests/TestCamera/ProcessSettingsFactoryTests.cs new file mode 100644 index 0000000000..545a779481 --- /dev/null +++ b/src/devices/Camera/tests/TestCamera/ProcessSettingsFactoryTests.cs @@ -0,0 +1,70 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +using Iot.Device.Camera.Settings; + +namespace TestCamera; + +/// +/// Tests for the ProcessSettingsFactory class, in particular the migration +/// from the libcamera-* applications to the new rpicam-* applications. +/// +public class ProcessSettingsFactoryTests +{ + /// + /// The rpicam-* constants must use the new application naming. + /// + [Fact] + public void RpicamConstantsUseTheNewNaming() + { + Assert.Equal("rpicam-still", ProcessSettingsFactory.RpicamStill); + Assert.Equal("rpicam-vid", ProcessSettingsFactory.RpicamVid); + } + + /// + /// The rpicam factory methods must target the new application names. + /// + [Fact] + public void RpicamFactoryMethodsTargetTheNewApplications() + { + Assert.Equal(ProcessSettingsFactory.RpicamStill, ProcessSettingsFactory.CreateForRpicamstill().Filename); + Assert.Equal(ProcessSettingsFactory.RpicamVid, ProcessSettingsFactory.CreateForRpicamvid().Filename); + + var stderr = ProcessSettingsFactory.CreateForRpicamstillAndStderr(); + Assert.Equal(ProcessSettingsFactory.RpicamStill, stderr.Filename); + Assert.True(stderr.CaptureStderrInsteadOfStdout); + } + + /// + /// When the rpicam-apps are installed, detection must report them as available. + /// + [Fact] + public void IsRpicamAppsInstalledDetectsTheNewApplications() + { + Assert.True(ProcessSettingsFactory.IsRpicamAppsInstalled( + name => name == ProcessSettingsFactory.RpicamStill)); + Assert.True(ProcessSettingsFactory.IsRpicamAppsInstalled( + name => name == ProcessSettingsFactory.RpicamVid)); + Assert.False(ProcessSettingsFactory.IsRpicamAppsInstalled(_ => false)); + } + + /// + /// The auto-detecting factory methods must prefer the rpicam-* applications + /// when they are available, otherwise fall back to the libcamera-* names. + /// + [Fact] + public void AutoFactoryMethodsPreferRpicamWhenAvailable() + { + // The auto methods rely on detection through the system PATH. The + // test agents do not ship the rpicam-apps, so the libcamera-* names + // are expected as the backward compatible fallback. + Assert.Equal(ProcessSettingsFactory.LibcameraStill, ProcessSettingsFactory.CreateForStill().Filename); + Assert.Equal(ProcessSettingsFactory.LibcameraVid, ProcessSettingsFactory.CreateForVid().Filename); + + var stderr = ProcessSettingsFactory.CreateForStillAndStderr(); + Assert.Equal(ProcessSettingsFactory.LibcameraStill, stderr.Filename); + Assert.True(stderr.CaptureStderrInsteadOfStdout); + } +} From f3a5e2b9c16c377c9995c61a265daf2b6d19686c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Jun 2026 16:08:06 +0000 Subject: [PATCH 3/4] Address review: simplify PATH probing in ProcessSettingsFactory --- .../Camera/Camera/Settings/ProcessSettingsFactory.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/devices/Camera/Camera/Settings/ProcessSettingsFactory.cs b/src/devices/Camera/Camera/Settings/ProcessSettingsFactory.cs index 4b7eb1cdc0..436b4d969a 100644 --- a/src/devices/Camera/Camera/Settings/ProcessSettingsFactory.cs +++ b/src/devices/Camera/Camera/Settings/ProcessSettingsFactory.cs @@ -185,7 +185,7 @@ private static bool IsApplicationOnPath(string fileName) return false; } - foreach (var directory in pathVariable!.Split(Path.PathSeparator)) + foreach (var directory in pathVariable.Split(Path.PathSeparator)) { if (string.IsNullOrWhiteSpace(directory)) { @@ -199,9 +199,9 @@ private static bool IsApplicationOnPath(string fileName) return true; } } - catch (ArgumentException) + catch (Exception) { - // Ignore invalid entries in the PATH variable + // Ignore invalid or inaccessible entries in the PATH variable } } From 505bf9c880cc7e5da0e9a560338b683ded9a3265 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Jun 2026 16:51:38 +0000 Subject: [PATCH 4/4] Fix SA1412 build failure: store ProcessSettingsFactoryTests.cs as UTF-8 with BOM --- .../Camera/tests/TestCamera/ProcessSettingsFactoryTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/devices/Camera/tests/TestCamera/ProcessSettingsFactoryTests.cs b/src/devices/Camera/tests/TestCamera/ProcessSettingsFactoryTests.cs index 545a779481..da67823215 100644 --- a/src/devices/Camera/tests/TestCamera/ProcessSettingsFactoryTests.cs +++ b/src/devices/Camera/tests/TestCamera/ProcessSettingsFactoryTests.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System;