Add camera module: drive UVC webcam controls via cameractrls

New `camera` module in modules.example.yaml (and the embedded copy) with
four functions: `set` (any control=value list, incl. preset/colour buttons),
`toggle` (flip between two values), `adjust` (step a numeric control by a
delta, clamped to the min/max parsed from `cameractrls -l`, with optional
`extra` controls applied first), and `is` (poll helper, exit 0 on match).

Device comes from CAMERA_DEVICE in .env (default /dev/video0) or a per-key
`device` param. cameractrls exits 0 for unknown controls, so polls must
use `is`; stderr is discarded to drop Python deprecation warnings.

README: Camera module section with setup, function table, a folder
example, and the oksvg viewBox-offset centring gotcha for SVG icons.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017iodiNMYL9iuT7f6WCe5iv
This commit is contained in:
Levi Woodard
2026-10-08 14:02:29 -06:00
parent 4158165410
commit 6d0748e4c5
3 changed files with 230 additions and 0 deletions

View File

@@ -57,6 +57,30 @@ modules:
curl -s -X POST https://slack.com/api/dnd.endSnooze \
-H "Authorization: Bearer {{env "SLACK_TOKEN"}}"
# Pomodoro / focus timer — the `flow` CLI.
#
# Absolute path required (minimal service PATH). Apple Silicon Homebrew installs
# live at /opt/homebrew/bin/flow — set that in ~/.config/streamdeck-go/.env:
# FLOW_CMD=/opt/homebrew/bin/flow
#
# Control verbs (start/pause/resume/toggle/skip/stop/reset/gui) run on key press.
# `status` prints one line like `state=running phase=work remaining=24:12` for
# icon-swap poll blocks (match: "state=running").
#
# The LIVE countdown label uses a key's `text_command` field directly (not a
# module), e.g. text_command: "/usr/local/bin/flow status --short" which prints
# a two-line label like WORK\n24:12 (an actual newline between the lines).
flow:
start: { exec: '{{envDefault "FLOW_CMD" "/usr/local/bin/flow"}} start' }
pause: { exec: '{{envDefault "FLOW_CMD" "/usr/local/bin/flow"}} pause' }
resume: { exec: '{{envDefault "FLOW_CMD" "/usr/local/bin/flow"}} resume' }
toggle: { exec: '{{envDefault "FLOW_CMD" "/usr/local/bin/flow"}} toggle' }
skip: { exec: '{{envDefault "FLOW_CMD" "/usr/local/bin/flow"}} skip' }
stop: { exec: '{{envDefault "FLOW_CMD" "/usr/local/bin/flow"}} stop' }
reset: { exec: '{{envDefault "FLOW_CMD" "/usr/local/bin/flow"}} reset' }
gui: { exec: '{{envDefault "FLOW_CMD" "/usr/local/bin/flow"}} gui' }
status: { exec: '{{envDefault "FLOW_CMD" "/usr/local/bin/flow"}} status' }
# OBS Studio — media player, streaming, and scene/transition control via obs-cmd
#
# Requires: obs-cmd (https://github.com/grigio/obs-cmd)
@@ -182,3 +206,67 @@ modules:
{{envDefault "OBS_CMD" "/usr/local/bin/obs-cmd"}} --websocket obsws://{{envDefault "OBS_HOST" "localhost"}}:{{envDefault "OBS_PORT" "4455"}}/{{env "OBS_WEBSOCKET_PASSWORD"}} scene transition-set "{{.transition}}" && \
{{envDefault "OBS_CMD" "/usr/local/bin/obs-cmd"}} --websocket obsws://{{envDefault "OBS_HOST" "localhost"}}:{{envDefault "OBS_PORT" "4455"}}/{{env "OBS_WEBSOCKET_PASSWORD"}} scene transition-duration {{.duration}} && \
{{envDefault "OBS_CMD" "/usr/local/bin/obs-cmd"}} --websocket obsws://{{envDefault "OBS_HOST" "localhost"}}:{{envDefault "OBS_PORT" "4455"}}/{{env "OBS_WEBSOCKET_PASSWORD"}} scene switch "{{.scene}}"
# ---------------------------------------------------------------------------
# Camera — cameractrls (https://github.com/soyersoyer/cameractrls), Linux only.
#
# Drives any UVC webcam's V4L2 controls (zoom, pan/tilt, focus, exposure, white
# balance, brightness/contrast/...). Install: pacman -S cameractrls / pip.
#
# Device: set CAMERA_DEVICE in ~/.config/streamdeck-go/.env to the stable path, e.g.
# CAMERA_DEVICE=/dev/v4l/by-id/usb-046d_Logitech_BRIO_XXXX-video-index0
# (defaults to /dev/video0; any function also takes a per-key `device` param).
#
# Discover control names/ranges: cameractrls -d /dev/video0 -l
# Note: cameractrls exits 0 even for unknown controls, so poll blocks must use
# the `is` function (which greps `-l` output) rather than exit codes.
camera:
# Set one or more controls, comma-separated (eg. "zoom_absolute=150,contrast=140").
# Buttons work too: "preset=save_1", "preset=load_1", "color_preset=vivid".
set:
params:
device: ""
controls: "zoom_absolute=100"
exec: /usr/bin/cameractrls -d {{if .device}}{{.device}}{{else}}{{envDefault "CAMERA_DEVICE" "/dev/video0"}}{{end}} -c "{{.controls}}" 2>/dev/null
# Flip a control between two values (a <-> b). Works for 0/1 and menu controls.
toggle:
params:
device: ""
control: auto_exposure
a: aperture_priority_mode
b: manual_mode
exec: |
cur=$(/usr/bin/cameractrls -d {{if .device}}{{.device}}{{else}}{{envDefault "CAMERA_DEVICE" "/dev/video0"}}{{end}} -l 2>/dev/null | sed -n 's/^ {{.control}} = \([^[:space:]]*\).*/\1/p')
if [ "$cur" = "{{.a}}" ]; then v="{{.b}}"; else v="{{.a}}"; fi
/usr/bin/cameractrls -d {{if .device}}{{.device}}{{else}}{{envDefault "CAMERA_DEVICE" "/dev/video0"}}{{end}} -c "{{.control}}=$v" 2>/dev/null
# Step a numeric control by delta (negative to decrease), clamped to its min/max.
# `extra` is applied first on the same call — eg. "auto_exposure=manual_mode"
# so stepping exposure also takes the camera out of auto.
adjust:
params:
device: ""
control: contrast
delta: "16"
extra: ""
exec: |
line=$(/usr/bin/cameractrls -d {{if .device}}{{.device}}{{else}}{{envDefault "CAMERA_DEVICE" "/dev/video0"}}{{end}} -l 2>/dev/null | grep '^ {{.control}} = ')
cur=$(printf '%s\n' "$line" | sed -n 's/^ [a-z0-9_]* = \([-0-9]*\).*/\1/p')
min=$(printf '%s\n' "$line" | sed -n 's/.*min: \([-0-9]*\).*/\1/p')
max=$(printf '%s\n' "$line" | sed -n 's/.*max: \([-0-9]*\).*/\1/p')
[ -n "$cur" ] || exit 1
v=$((cur + {{.delta}}))
[ -n "$min" ] && [ "$v" -lt "$min" ] && v=$min
[ -n "$max" ] && [ "$v" -gt "$max" ] && v=$max
ctl="{{.control}}=$v"
[ -n "{{.extra}}" ] && ctl="{{.extra}},$ctl"
/usr/bin/cameractrls -d {{if .device}}{{.device}}{{else}}{{envDefault "CAMERA_DEVICE" "/dev/video0"}}{{end}} -c "$ctl" 2>/dev/null
# Poll helper for toggle keys: exit 0 when control == value.
is:
params:
device: ""
control: auto_exposure
value: aperture_priority_mode
exec: /usr/bin/cameractrls -d {{if .device}}{{.device}}{{else}}{{envDefault "CAMERA_DEVICE" "/dev/video0"}}{{end}} -l 2>/dev/null | grep -q '^ {{.control}} = {{.value}}[[:space:]]'