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:
78
README.md
78
README.md
@@ -862,6 +862,84 @@ keys:
|
||||
|
||||
**Note — absolute paths in modules:** The example templates call `/usr/local/bin/obs-cmd` rather than just `obs-cmd`. This is because launchd (macOS) and systemd (Linux) give the service a minimal `PATH` that doesn't include `/usr/local/bin` or Homebrew. Use the absolute path returned by `which obs-cmd` in your own module templates, or set `PATH` in the launchd plist / systemd unit.
|
||||
|
||||
#### Camera module (Linux)
|
||||
|
||||
The example `modules.yaml` includes a `camera` module that drives any UVC webcam's V4L2 controls — zoom, pan/tilt, focus, exposure, white balance, brightness/contrast and so on — through [`cameractrls`](https://github.com/soyersoyer/cameractrls).
|
||||
|
||||
**Setup:**
|
||||
|
||||
1. Install `cameractrls` (Arch: `pacman -S cameractrls`; other distros: see the project README). The module calls `/usr/bin/cameractrls`.
|
||||
2. Point the module at your camera's stable device path (it defaults to `/dev/video0`, which renumbers across reboots):
|
||||
```bash
|
||||
ls -l /dev/v4l/by-id/ # pick the *-video-index0 entry for your camera
|
||||
echo 'CAMERA_DEVICE=/dev/v4l/by-id/usb-046d_Logitech_BRIO_XXXX-video-index0' >> ~/.config/streamdeck-go/.env
|
||||
```
|
||||
Any function also accepts a per-key `device` param for multi-camera setups.
|
||||
3. List the control names and ranges your camera exposes:
|
||||
```bash
|
||||
cameractrls -d /dev/video0 -l
|
||||
```
|
||||
|
||||
**Functions:**
|
||||
|
||||
| Function | Purpose | Params |
|
||||
|---|---|---|
|
||||
| `set` | Set one or more controls (comma-separated). Also fires button controls such as `preset=save_1` / `preset=load_1` or `color_preset=vivid` | `controls` |
|
||||
| `toggle` | Flip a control between two values — 0/1 switches or menu controls | `control`, `a`, `b` |
|
||||
| `adjust` | Step a numeric control by `delta` (negative to decrease), clamped to the min/max the camera reports. `extra` is applied first on the same call, e.g. `auto_exposure=manual_mode` so stepping exposure also leaves auto | `control`, `delta`, `extra` |
|
||||
| `is` | Poll helper: exits 0 when `control == value` | `control`, `value` |
|
||||
|
||||
**Example — a camera folder with FOV presets, a stepper pair and a status toggle:**
|
||||
|
||||
```yaml
|
||||
keys:
|
||||
18:
|
||||
icon: camera.svg
|
||||
text: "Camera"
|
||||
folder: camera
|
||||
|
||||
folders:
|
||||
camera:
|
||||
keys:
|
||||
1:
|
||||
icon: fov-wide.svg
|
||||
text: "Wide"
|
||||
module: camera
|
||||
function: set
|
||||
params: { controls: "logitech_brio_fov=90" }
|
||||
3:
|
||||
icon: zoom-out.svg
|
||||
text: "Zoom −"
|
||||
module: camera
|
||||
function: adjust
|
||||
params: { control: zoom_absolute, delta: "-25" }
|
||||
4:
|
||||
icon: zoom-in.svg
|
||||
text: "Zoom +"
|
||||
module: camera
|
||||
function: adjust
|
||||
params: { control: zoom_absolute, delta: "25" }
|
||||
14:
|
||||
icon_true: af-on.svg # green = autofocus on
|
||||
icon_false: af-off.svg # grey = manual focus
|
||||
text: "Autofocus"
|
||||
module: camera
|
||||
function: toggle
|
||||
params: { control: focus_automatic_continuous, a: "1", b: "0" }
|
||||
poll:
|
||||
module: camera
|
||||
function: is
|
||||
params: { control: focus_automatic_continuous, value: "1" }
|
||||
interval: 3s
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
|
||||
- `cameractrls` exits 0 even for an unknown control name, so poll blocks must use the `is` function — never rely on a bare exit code.
|
||||
- Each call starts a Python process (~100 ms). A 3 s poll interval is comfortable; polls only run while the folder is open.
|
||||
- `cameractrls -l` prints Python deprecation warnings on stderr; the module templates discard stderr so the output stays clean.
|
||||
- SVG icons are rasterised by `oksvg`, which does **not** honour a viewBox x/y offset for centring (it applies the offset in pixels before scaling). To centre a glyph and leave room for a text label, keep `viewBox="0 0 S S"` and wrap the paths in `<g transform="translate(x y)">`.
|
||||
|
||||
#### Using modules in config.yaml
|
||||
|
||||
Reference a module function instead of writing inline commands:
|
||||
|
||||
@@ -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:]]'
|
||||
|
||||
@@ -206,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:]]'
|
||||
|
||||
Reference in New Issue
Block a user