Operating the Daemon
Install, run, configure, and troubleshoot aprilcamd — the process that owns the cameras and does all vision.
Operating the AprilCam Daemon
aprilcamd is the single long-running process that owns every camera, runs
AprilTag/ArUco detection and homography, and serves per-frame data to all
clients (the MCP server, the DaemonControl
library, and the aprilcam view window). It is the sole
camera opener and the sole vision authority — clients never touch hardware or
OpenCV.
This page is about running the daemon. For the gRPC/stream contract, see the Daemon Wire Protocol.
Install
The daemon needs OpenCV, so install it with the daemon extra on the host the
cameras are plugged into:
pipx install 'aprilcam[daemon]'
Thin clients elsewhere use the base install (no OpenCV). See install tiers.
Run
aprilcam daemon start # start in the background
aprilcam daemon status # running/stopped + open cameras
aprilcam daemon stop
aprilcam daemon restart
aprilcam daemon start -v # foreground, INFO logs (-vv for DEBUG)
python -m aprilcam.daemon # run in the foreground directly
You start the daemon explicitly — on the camera host (aprilcam daemon
start) or as a systemd service (below). Clients do not auto-spawn it: an MCP
server, DaemonControl, the CLI, or the viewer that can’t find a daemon fails
fast with DaemonNotFoundError (“No aprilcam daemon found …”) instead of
launching one. The daemon refuses to start a second instance (exclusive flock
on its pidfile) and runs until Shutdown or a kill signal — it does not exit
when idle.
Connecting (local and remote)
The daemon listens on three things at once:
- a Unix socket at
<socket_dir>/control.sock(local clients), - TCP
0.0.0.0:5280(remote clients across the network), and - an mDNS / Bonjour advertisement (
_aprilcam._tcp, TXThost=<hostname>) so clients can find it with no configuration.
Every client (the MCP server, the CLI, the
DaemonControl library, the viewer) resolves a target in
this order — and never spawns a daemon:
- an explicit
--host/--portflag, orAPRILCAM_DAEMON_HOST/APRILCAM_DAEMON_PORT; - a running local daemon on the default Unix socket;
- mDNS discovery — exactly one daemon → use it; several → pick one with
APRILCAM_DAEMON_HOST; none → a clearNo aprilcam daemon founderror.
--host / --port are global flags, accepted before or after the
subcommand. They also accept a host letter once you have run aprilcam probe
(below). .local mDNS names resolve transparently (the client resolves them to
an IP before dialing gRPC):
aprilcam cameras # auto-discover the daemon on the LAN
aprilcam --host vidar.local cameras # talk to a specific remote daemon
aprilcam cameras --host 192.168.1.40 # …by hostname or IP
Set APRILCAM_DAEMON_HOST=vidar.local (in ~/.aprilcam, /etc/aprilcam.env,
or .env) to make every client target a remote daemon by default.
aprilcam probe and short codes
aprilcam probe discovers every reachable daemon (local + mDNS), enumerates
their cameras, and saves a host store at <data_dir>/hosts.json with stable
host and camera numbers. Each camera then has a compact base-26 code: a
single letter for a local camera (C = local camera 3), or host-letter +
camera-letter for a remote one (FB = host F, camera 2).
aprilcam probe # A vidar [remote] -> AA imx296-88000, AB imx296-80000
aprilcam cameras --host vidar.local # [6] AA imx296-88000 [7] AB imx296-80000
aprilcam --host A cameras # address a host by its letter
aprilcam view AB # open vidar's 2nd camera by its code
Cameras
The daemon assigns each camera a persistent enumeration number on first
sight and stores it in <data_dir>/cameras/registry.json. That number — not
the volatile OS device index — is the stable handle shown by aprilcam cameras
and accepted everywhere a camera number is taken:
aprilcam cameras # [1] Brio 501 [3] Arducam OV9782 [4] Samizdat
aprilcam cameras --details # also shows slug + os-index for debugging
aprilcam view 3 # open the live view for camera 3
Run aprilcam probe to also tag each camera with a short alpha code (see
Connecting) — convenient for addressing cameras
across multiple hosts.
Each camera gets a directory <data_dir>/cameras/<slug>/ holding:
| File | Owner | Contents |
|---|---|---|
config.json |
developer (static) | device name, resolution, UVC settings, camera_position, static_marker_ids, linked playfield |
calibration.json |
daemon (regenerable) | homography, corner_pixels, static_markers, tags_used, intrinsics |
info.json |
daemon | runtime pointers (e.g. the paths file) |
paths.json |
MCP server | persistent waypoint paths for the viewer |
The split matters: config.json is hand-owned and never overwritten by
calibration; calibration.json is regenerated by aprilcam calibrate /
calibrate_playfield and must not carry the config-owned keys.
Calibrate
Calibration maps pixels → real-world cm and is normally done once per camera on the daemon host:
aprilcam calibrate 3 # calibrate camera 3 from its linked playfield
After that, clients get world coordinates automatically (open_camera →
playfield_id). Agents can also calibrate via the MCP calibrate_playfield
tool. Field dimensions come from the linked playfield definition, not from
arguments.
Configuration
All paths and tunables come from Config (src/aprilcam/config.py), resolved
in priority order (highest wins):
| Priority | Source |
|---|---|
| highest | APRILCAM_* environment variables |
.env (walking up from CWD) |
|
.aprilcam project dotfile (walking up from CWD) |
|
~/.aprilcam user dotfile |
|
| lowest | /etc/aprilcam.env, /etc/aprilcam/aprilcam.env |
Key variables:
| Key | Default | Description |
|---|---|---|
APRILCAM_DATA_DIR |
FHS /var/lib/aprilcam · XDG ~/.local/share/aprilcam |
State: calibration.json, registry.json, hosts.json, info.json, paths.json. |
APRILCAM_CONFIG_DIR |
FHS /etc/aprilcam · XDG ~/.config/aprilcam |
Admin-owned config: per-camera config.json, playfields/*.json. Falls back to reading from APRILCAM_DATA_DIR if unset and legacy (pre-sprint-019) config files are found there — see aprilcam migrate-dirs. |
APRILCAM_SOCKET_DIR |
FHS /run/aprilcam · XDG $XDG_RUNTIME_DIR/aprilcam (falls back to a per-user temp dir, e.g. $TMPDIR/aprilcam-<uid> on macOS) |
Control socket, stream sockets, pidfile. |
APRILCAM_LOG_DIR |
FHS /var/log/aprilcam · XDG ~/.local/state/aprilcam |
aprilcamd.log. |
APRILCAM_LOG_LEVEL |
INFO |
DEBUG / INFO / WARNING / ERROR. |
APRILCAM_DETECTION_FPS |
10 |
Detection loop frame-rate cap. |
APRILCAM_STATIC_DESKEW |
1 |
Homography-derived static-camera deskew (0 to disable). |
APRILCAM_SYSTEM |
auto |
Force FHS (1) or XDG (0) layout. Auto = FHS when running as root. |
Directory layout (FHS vs XDG)
AprilCam picks a layout from whether it runs as root (euid == 0) or
APRILCAM_SYSTEM=1:
| Path | FHS (root / APRILCAM_SYSTEM=1) |
XDG (user) |
|---|---|---|
| Data (state) | /var/lib/aprilcam |
~/.local/share/aprilcam |
| Config (admin-owned) | /etc/aprilcam |
~/.config/aprilcam |
| Socket / runtime | /run/aprilcam |
$XDG_RUNTIME_DIR/aprilcam (or per-user temp) |
| Logs | /var/log/aprilcam |
~/.local/state/aprilcam |
All clients and the daemon must agree on
APRILCAM_SOCKET_DIR. A client and daemon that resolve different socket directories cannot find each other. If you launch the daemon and clients from different environments, setAPRILCAM_SOCKET_DIRexplicitly in/etc/aprilcam.envor a shared.env.
systemd
Under DynamicUser=yes the daemon is not root, but systemd prepares the FHS
directories via StateDirectory=, RuntimeDirectory=, and LogsDirectory=.
Set APRILCAM_SYSTEM=1 (in the unit’s Environment= or /etc/aprilcam.env)
so the config loader uses those FHS paths.
Logs
Daemon stderr goes to <log_dir>/aprilcamd.log. For interactive debugging run
it in the foreground: aprilcam daemon start -vv or python -m aprilcam.daemon.
Troubleshooting
- A client prints
No aprilcam daemon found— no daemon was discovered and none was configured. Start one on the camera host (aprilcam daemon start, or the systemd service) or point the client at it with--host/APRILCAM_DAEMON_HOST. Clients never start a daemon for you. aprilcam daemon startreports “did not start within 20 seconds” — the client’s readiness probe could not reach the socket. Most often a socket-directory mismatch (client and daemon resolved differentAPRILCAM_SOCKET_DIR) or a slow cold start. Confirm withaprilcam config | grep socket_diron both sides; checkaprilcamd.log.- A second
daemon startright afterstopbails with “already running” — shutdown is asynchronous (it stops camera pipelines first). Wait for the process to exit (pollaprilcam daemon status) before starting again. - A camera that was unplugged keeps returning a dead handle — the capture
thread exits but the camera stays registered.
close_camerathenopen_camera, or restart the daemon. ModuleNotFoundError: No module named 'cv2'from the daemon — the daemon host was installed without thedaemonextra. Reinstall withpipx install 'aprilcam[daemon]'. (The MCP server and other thin clients correctly run without OpenCV — that error from a client means a tool is doing pixel work it should delegate to the daemon.)- After a code change, behavior doesn’t update — a running daemon/MCP/view process holds imported code in memory; restart it to pick up changes.