Skip to content

rpicam-vid: add Unix Domain Socket runtime control and companion tools#917

Open
Kletternaut wants to merge 2 commits into
raspberrypi:mainfrom
Kletternaut:feature/rpicam-ctrl
Open

rpicam-vid: add Unix Domain Socket runtime control and companion tools#917
Kletternaut wants to merge 2 commits into
raspberrypi:mainfrom
Kletternaut:feature/rpicam-ctrl

Conversation

@Kletternaut

Copy link
Copy Markdown
Contributor

This patch adds a Unix Domain Socket interface to rpicam-vid that allows camera parameters to be adjusted at runtime — without stopping and restarting the process. Two companion tools are included: rpicam-ctrl (Qt6/Qt5 GUI) and rpicam-ctrl-cli (Python terminal UI).

rpicam-ctrl1

Demo Video

A demo video showing rpicam-ctrl-cli (terminal UI) and rpicam-ctrl (Qt GUI) controlling live rpicam-vid streams in real time.

rpicam-ctrl.mp4

Motivation

Several camera controls — brightness, contrast, saturation, exposure compensation, AWB mode, AWB gains, ROI/digital zoom and HDR mode — are currently only applied at startup. Changing any of these values requires stopping and restarting rpicam-vid, interrupting the stream. This patch eliminates that requirement by allowing parameters to be adjusted at runtime without restarting the process.

For parameters like shutter speed, AWB gains, and ROI/zoom, finding the optimal value is only practical when the result is visible in the live stream — a stop/restart cycle makes iterative tuning tedious or impossible.

This is especially true for manual focus: in microscopy, macroscopy, or astrophotography, focus can only be set correctly while observing the live image. The autofocus controls (lens_position) are already implemented in this patch but currently hidden in the UI and untested — AF-capable Raspberry Pi camera modules are required to validate and complete that feature, which is planned for a follow-up contribution.

Implementation

A new ControlSocket class (apps/control_socket.hpp) listens on a UNIX stream socket. The socket path is derived automatically from the --camera index: camera 0 uses /tmp/rpicam-vid0.sock, camera 1 uses /tmp/rpicam-vid1.sock. This allows two rpicam-vid instances to run simultaneously with independent control sockets.

Commands are plain text, newline-terminated, in the form key:value:

brightness:0.3
contrast:1.5
roi:0.25,0.25,0.5,0.5
awb:daylight
...

Multiple commands can be sent in a single write. The socket is SOCK_NONBLOCK throughout — it never blocks the capture loop. Inside event_loop() in rpicam_vid.cpp, AcceptConnections() and ReadControls() are called once per frame and the resulting ControlList is passed to the existing SetControls() API.

A SupportsScalerCrops() helper is added to RPiCamApp to detect at runtime whether the attached camera supports the PISP multi-channel crop control (rpi::ScalerCrops). This is more reliable than platform detection — sensors like imx477 on Pi 5 use the legacy ScalerCrop path even though the platform is PISP. ROI commands use whichever control the camera actually supports.

Supported commands

Command Value Range / options
brightness float -1.0 … 1.0
ev float -3.0 … 3.0
contrast float 0.0 … 3.0
saturation float 0.0 … 3.0
sharpness float 0.0 … 16.0
gain float 1.0 … 16.0 (sets AnalogueGainModeManual automatically)
awb string auto | incandescent | tungsten | fluorescent | indoor | daylight | cloudy
awbgains float,float manual colour gains r,b — disables auto AWB
roi float,float,float,float x,y,w,h relative 0.0–1.0
metering string centre | spot | average | custom
exposure string normal | sport
denoise string auto | off | cdn_off | cdn_fast | cdn_hq
hdr string off | auto | sensor | single-exp
shutter integer (µs) Exposure time in microseconds; 0 = auto
framerate integer (fps) Target frame rate; 0 = auto (sensor maximum)

Caps protocol (server → client)

When a client connects, rpicam-vid immediately sends a single line describing the active sensor mode:

caps:maxfps=40,hasaf=0
Field Meaning
maxfps Maximum frame rate of the active mode (integer fps)
hasaf 1 if the camera supports autofocus hardware, 0 otherwise

Both reference tools parse this line on connect and clamp the framerate slider maximum accordingly. When the framerate is changed and the shutter is not on auto, the new shutter µs (recalculated from the updated one-frame period) is also sent automatically — so the camera always reflects the slider display.

Extensibility

ControlSocket is implemented as a self-contained header (apps/control_socket.hpp) with no dependency on rpicam-vid-specific code. Integrating it into any other rpicam-app (rpicam-still, rpicam-jpeg, rpicam-raw, …) requires only three lines in that app's event loop: construct the socket with the desired path, call AcceptConnections() and ReadControls() once per frame, and pass the returned ControlList to SetControls(). The command vocabulary and wire format are identical regardless of which app hosts the socket.

Per-camera state persistence

Both reference implementations (rpicam-ctrl and rpicam-ctrl-cli) maintain independent state per camera index and persist it to /tmp/rpicam-vid{N}.state (JSON). This ensures:

  • No state bleeding when switching cameras at runtime — each camera remembers its own last settings.
  • GUI/CLI interoperability — both tools share the same state file format, so switching between rpicam-ctrl-cli and rpicam-ctrl preserves the last values.
  • Correct display after reconnect — on reconnect the GUI re-reads the state file; if the file is absent (because rpicam-vid started a new session), defaults are shown and sent.

Session isolation

ControlSocket deletes the companion .state file both on construction (before bind()) and in its destructor (alongside the .sock file). This guarantees that every new rpicam-vid session starts from hardware defaults, independent of what a previous session had set.

rpicam_vid.cpp now catches SIGTERM (in addition to the existing SIGINT) and routes it through the normal event-loop exit path. This ensures the ControlSocket destructor runs — and both .sock and .state files are cleaned up — when rpicam-vid is stopped via systemctl stop, pkill, or kill. (SIGKILL cannot be caught; the constructor-time delete of any stale files serves as a fallback.)

State file format

{
  "brightness": 0, "ev": 0, "contrast": 100, "saturation": 100,
  "zoom": 10, "gain": 10, "sharpness": 10,
  "awbGainR": 150, "awbGainB": 120,
  "shutter": 0, "framerateIdx": 0,
  "awbIdx": 0, "meteringIdx": 0, "exposureIdx": 0,
  "denoiseIdx": 0, "hdrIdx": 0
}

shutter is the log-scale slider position (0 = auto, 1–200 maps to 100 µs … one-frame period). The upper bound of the shutter range (one-frame period) is derived from framerateIdx: when the framerate slider is non-zero, the maximum shutter duration is 1 000 000 / framerateIdx µs; when framerate is 0 (auto), the sensor mode's maxfps from the caps: line is used instead. framerateIdx is the target fps directly (0 = auto, 1–N fps).

Compatibility

  • No breaking changes. All existing CLI options continue to work identically.
  • Fully additive. No existing code path is modified; the socket is purely additional logic layered on top of the existing SetControls() API.
  • Graceful degradation. If bind() fails, a warning is logged and rpicam-vid continues normally without socket support.
  • Locale-safe. Float parsing uses std::locale::classic() explicitly.
  • Non-blocking. SOCK_NONBLOCK and accept4() throughout — the capture loop is never stalled.

AWB correctness

awb:<mode> explicitly sends AwbEnable=true alongside the mode, and awbgains:<r>,<b> sends AwbEnable=false. This prevents libcamera from retaining stale ColourGains state when switching between manual and auto AWB.

ev:<value> explicitly sends AeEnable=true to ensure the exposure compensation is applied even when the auto-exposure pipeline is in a particular state.

HDR modes

Only HdrModeOff and HdrModeSingleExposure are functional on imx477 at runtime. The hdr: command accepts off|auto|sensor|single-exp, matching the rpicam-apps CLI convention (both auto and sensor map to HdrModeSingleExposure).

Multi-camera support

Two rpicam-vid instances can run simultaneously on separate cameras. Each gets its own socket derived from the --camera index:

rpicam-vid --camera 0 -o /dev/null &   # listens on /tmp/rpicam-vid0.sock
rpicam-vid --camera 1 -o /dev/null &   # listens on /tmp/rpicam-vid1.sock

Both rpicam-ctrl-cli and rpicam-ctrl support switching between cameras at runtime without restarting the tool.

Files changed

File Change
apps/control_socket.hpp New — self-contained ControlSocket class; HasNewClient/SendToClient for caps push; deletes companion .state file on init/destroy
apps/rpicam_vid.cpp +55 lines — socket path from --camera index; per-frame poll; SIGTERM handler; sends caps:maxfps=N,hasaf=0|1 to new clients after ConfigureVideo()
core/rpicam_app.hpp +2 lines — SupportsScalerCrops() declaration
core/rpicam_app.cpp +21 lines — SupportsScalerCrops() implementation
meson.build +1 line — utils/ subdir added
meson_options.txt +5 lines — enable_rpicam_ctrl option
utils/meson.build +3 lines — rpicam_ctrl subdir
utils/rpicam_ctrl/meson.build New — Qt6/Qt5 build rules for rpicam-ctrl
utils/rpicam_ctrl/main.cpp New — Qt6/Qt5 control panel with per-camera state
utils/rpicam-ctrl-cli New — terminal ASCII slider interface (Python 3) with per-camera state
utils/README.md New — documentation for rpicam-ctrl and rpicam-ctrl-cli
README.md +62 lines — per-camera state and runtime control documentation

Usage

# Camera 0 (default)
rpicam-vid --camera 0 -o /dev/null &

echo "brightness:0.5"            | nc -U /tmp/rpicam-vid0.sock
echo "awb:daylight"              | nc -U /tmp/rpicam-vid0.sock
echo "roi:0.25,0.25,0.5,0.5"    | nc -U /tmp/rpicam-vid0.sock
echo "awbgains:1.6,1.2"          | nc -U /tmp/rpicam-vid0.sock
printf "contrast:1.4\nev:-1.0\n" | nc -U /tmp/rpicam-vid0.sock

Reference implementations

Qt control panel — utils/rpicam_ctrl/ (rpicam-ctrl)

A Qt6/Qt5 GUI with sliders for all continuous parameters, dropdowns for AWB / metering / exposure / denoise / HDR, and manual AWB R/B gain sliders.

Slider details:

  • Shutter: logarithmic scale, 0 = auto, 1–200 positions spanning 100 µs to one full frame period. The upper bound updates live from the caps: message.
  • Framerate: direct integer steps (1 fps resolution), 0 = auto. Maximum clamped to the active sensor mode's maxfps received on connect.
  • Shutter auto-resend: when the framerate slider is moved and shutter is not on auto, the recalculated µs value is sent automatically so the camera always matches the displayed value.

Camera selection: a camera selector switches between camera 0 and camera 1 at runtime. If camera 0's socket is absent at startup but camera 1's is present, camera 1 is selected automatically — useful when camera 0 is unavailable.

Autofocus (AF) controls are implemented in code but hidden in the UI — untested due to missing hardware (no AF-capable lens available).

ROI: the Zoom slider applies a symmetric centre crop (roi:x,y,w,h with equal margins on all sides), effectively implementing digital zoom. Interactive ROI selection — allowing the user to drag an arbitrary region on the preview — is planned as a follow-up PR.

Connects automatically and reconnects if rpicam-vid is restarted. Keyboard shortcuts: R reset, Q quit, C switch camera.

Build via Meson (requires Qt6 or Qt5 Widgets + Network):

meson configure build -Denable_rpicam_ctrl=enabled
ninja -C build

Terminal UI (CLI) — utils/rpicam-ctrl-cli

ASCII slider interface in pure Python 3 (no dependencies beyond stdlib curses). Includes all the same controls as the GUI (brightness, EV, shutter, framerate, contrast, saturation, gain, sharpness, AWB gains, zoom, and all mode combos) in the same order as the Qt layout.

  • Shutter/framerate: identical log-scale shutter and direct-integer framerate logic as the GUI; shutter is re-sent on framerate change.
  • Caps polling: reads caps: lines from the socket in the main loop; adjusts framerate slider maximum and shows [max N fps] in the status bar.
  • Auto-select cam1: same socket-existence check as the GUI.
./utils/rpicam-ctrl-cli        # camera 0 (auto-selects cam1 if cam0 absent)
./utils/rpicam-ctrl-cli 1      # camera 1
Key Action
/ Move between parameters
/ Decrease / increase value (or switch camera when Camera row is selected)
C Cycle to next camera
R Reset all parameters to defaults

Enables real-time adjustment of camera parameters while rpicam-vid
is running — without interrupting the video stream or restarting the
process. Parameters such as brightness, EV, shutter, framerate,
contrast, saturation, gain, sharpness, AWB gains, zoom, AWB mode,
metering, exposure mode, denoise and HDR can be changed via a Unix
Domain Socket.

Changes to rpicam-apps core:

- apps/control_socket.hpp (new): self-contained Unix Domain Socket
  server; one socket per camera index (/tmp/rpicam-vid{N}.sock);
  HasNewClient/SendToClient for pushing caps to new clients; deletes
  the companion .state file on init/destroy for a clean session start.

- apps/rpicam_vid.cpp: ControlSocket integrated into the capture
  loop; socket path derived from the --camera index; after
  ConfigureVideo() the active sensor mode's caps are pushed to new
  clients: caps:maxfps=N,hasaf=0|1

- core/rpicam_app.hpp/.cpp: SupportsScalerCrops() added to detect
  whether the active camera supports autofocus.

Two companion tools connect to that socket:

rpicam-ctrl (utils/rpicam_ctrl/): Qt6/Qt5 graphical control panel
with sliders for brightness, EV, shutter (log-scale), framerate,
contrast, saturation, gain, sharpness, AWB gains and zoom, plus
dropdowns for AWB, metering, exposure, denoise and HDR modes.
Includes a .desktop launcher and XDG icon.

rpicam-ctrl-cli (utils/rpicam-ctrl-cli): equivalent terminal ASCII
slider interface in pure Python 3 (stdlib curses, no dependencies).

Both tools:
- connect automatically and reconnect on rpicam-vid restart
- parse caps: to clamp the framerate slider maximum
- maintain per-camera state in /tmp/rpicam-vid{N}.state
- auto-select camera 1 if camera 0 socket is absent on startup
- re-send recalculated shutter us when framerate is changed

Meson: new enable_rpicam_ctrl feature option (default: disabled)
builds rpicam-ctrl; requires Qt6 or Qt5 Widgets + Network.

Signed-off-by: Kletternaut <tomge68@gmail.com>
The option previously defaulted to 'auto', which caused rpicam-ctrl to
be built automatically whenever Qt was present on the build system.
This broke the Debian package build: rpicam-ctrl.desktop and
rpicam-ctrl.svg were installed to debian/tmp but are not listed in any
debian/*.install file, causing dh_missing to abort.

Change the default to 'disabled' so rpicam-ctrl is never built unless
explicitly requested:

  meson setup build -Denable_rpicam_ctrl=enabled

Also guard the entire Qt dependency search inside a
'not enable_rpicam_ctrl.disabled()' block so that Qt is not even
probed when the feature is disabled, preventing any inadvertent
side-effects in the build summary.

Signed-off-by: Kletternaut <tomge68@gmail.com>
@Kletternaut

Copy link
Copy Markdown
Contributor Author

@naushir Hi Naushir, since it’s been a while, I just wanted to check if there is any technical criticism or architectural feedback regarding this implementation. I took great care to keep changes to the existing codebase to a minimum in order to avoid side effects. However, if there is anything I should change, improve, or adjust to meet your standards, please let me know. I would really appreciate your feedback! I understand, of course, if a lack of time is the reason for the delay. Also, just a quick heads-up: it looks like there is currently 1 workflow awaiting approval from a maintainer to let the final checks finish. Thanks, Tom

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant