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;