---
title: Headless boards
description: Install, configure, update, and supervise a dartpilot board with no screen, on a Raspberry Pi, a small server, or a Mac.
---

A **headless board** runs the same camera and detection engine as the desktop app
but has no window. It serves the **Board Console** over HTTP so you configure and
calibrate it from any browser on the same network: a phone, a TV, or a laptop.
This is the recommended setup for venues and for machines with no display
attached: a Raspberry Pi, a small Linux server, or a Mac.

## Install

```bash
curl -fsSL https://get.dartpilot.app/headless.sh | sh
```

This downloads the latest build for your platform to `~/.local/share/dartpilot`
and symlinks `~/.local/bin/dartpilot`. It also installs a service that starts on
boot and restarts on failure:

| Platform | Service |
| --- | --- |
| Linux | systemd **user** unit `dartpilot`. Camera access is granted to the service automatically (`SupplementaryGroups=video`). |
| macOS | LaunchAgent `com.dartpilot.board` in `~/Library/LaunchAgents`. No `sudo`, and no camera-group setup. |

If you are unsure whether the machine is a desktop or a board, run the installer
with no flags; it detects a graphical session and asks if it cannot.

## First start and pairing

On first start the board prints a sign-in code to its logs:

```bash
# Linux
journalctl --user -u dartpilot -f

# macOS
tail -f ~/Library/Logs/dartpilot/dartpilot.out.log
```

Enter the code at [play.dartpilot.app/device](https://play.dartpilot.app/device) to approve it.
The board then stays connected. On macOS, allow camera access when macOS asks.
Until you do, it cannot see the board.

The same log prints the **Board Console URL**:

```
Board Console: http://192.168.1.42:3000/#key=…
```

Open that URL (including the `#key=…` fragment) in a browser on the same network.
The fragment key is exchanged for a cookie, so MJPEG and event streams authenticate
too. The key lives at `~/.local/share/com.dartpilot.desktop/console_key` on Linux
and `~/Library/Application Support/com.dartpilot.desktop/console_key` on macOS;
delete it to rotate.

:::caution
The Board Console can recalibrate cameras and stop detection. Keep it on a trusted
network, and do not share the `#key=…` URL.
:::

## Board Console

From the console you can:

- select cameras and resolution, and set the frame rate;
- run calibration and watch the live camera feeds;
- pair/re-pair the board and reset its identity;
- start/stop detection and preview;
- export a board image for the web scoreboard.

## Configuration

Everything is set with CLI flags or environment variables. There is no config
file to edit.

| Flag | Env var | Default | Purpose |
| --- | --- | --- | --- |
| `--bind <ADDR>` | `DARTPILOT_BIND_ADDR` | `0.0.0.0:3000` | Address the local API/console listens on. |
| `--log-level <LEVEL>` | `DARTPILOT_LOG` | `info` | `trace`/`debug`/`info`/`warn`/`error`. |
| `--log-format <FMT>` | `DARTPILOT_LOG_FORMAT` | `text` | `text` or `json` (structured logs). |
| `--check-update` | – | – | Print whether a newer release is available, then exit. |
| `--update` | – | – | Download, verify, and install the latest release, then exit. |
| `-V`, `--version` | – | – | Print the version and exit. |
| `-h`, `--help` | – | – | Print usage and exit. |

## Updating

Update in place:

```bash
dartpilot --update
```

The board does not leave a stopped or broken build behind. `--update` stops the
service, swaps in the new build, checks that it actually runs, and starts the
service again, restoring the previous version if the check fails. It reads
`https://get.dartpilot.app/headless.json` and verifies the archive SHA-256 first.
If the restart fails it prints the command to run yourself:

```bash
systemctl --user restart dartpilot                       # Linux
launchctl kickstart -k gui/$(id -u)/com.dartpilot.board # macOS
```

A venue can roll a fleet by issuing the `update` command from the dashboard; the
board updates and restarts itself.

## Operations

```bash
# Linux
systemctl --user status dartpilot                        # service state
systemctl --user restart dartpilot                       # restart the board
journalctl --user -u dartpilot -f                        # follow logs
journalctl --user -u dartpilot | jq                      # parse JSON logs (--log-format json)

# macOS
launchctl print gui/$(id -u)/com.dartpilot.board         # service state
launchctl kickstart -k gui/$(id -u)/com.dartpilot.board  # restart the board
tail -f ~/Library/Logs/dartpilot/dartpilot.out.log       # follow logs
```

Readiness is exposed at `GET http://<board>:3000/api/health` (no key required) for
the service manager and LAN dashboards. It reports `ready`, `phase`
(`starting`/`calibrating`/`ready`/`error`), `board_state`, per-camera calibration,
detection status, and the version. See the
[health contract](https://github.com/ihs7/dartpilot/blob/main/apps/desktop/AGENTS.md)
for every field.

## Recipes

### Raspberry Pi with no display (venue rack)

1. Flash Raspberry Pi OS, boot it, and join the venue network.
2. Run the headless installer and approve the sign-in code.
3. Open the printed Board Console URL on your phone with the board's cameras
   mounted, select them, and calibrate.
4. Point a TV or tablet at the scoreboard; the board itself needs no screen.

### Small server / NUC

The same installer works on `x86_64` Linux. Bind to a specific interface and use
JSON logs if you ship them to a log collector:

```bash
DARTPILOT_BIND_ADDR=0.0.0.0:3000 DARTPILOT_LOG_FORMAT=json dartpilot
```

### Mac

The installer works on Apple Silicon and Intel Macs. A LaunchAgent starts when
the user logs in rather than at boot, so keep the board's user logged in (enable
automatic login if the Mac reboots unattended). Everything else is the same as
Linux.

## Troubleshooting

- **No cameras in the Console.** Confirm they are UVC USB cameras. On Linux, check
  the service has the `video` group
  (`systemctl --user show dartpilot -p SupplementaryGroups`). On macOS, open
  System Settings → Privacy & Security → Camera and allow dartpilot. Then read the
  logs (see [Operations](#operations)).
- **Console asks for an access key.** Open the full URL printed at boot, including
  the `#key=…` fragment. If you lost it, read the `console_key` file listed above.
- **`ready: false` in `/api/health`.** Read `phase` and `cameras.items[].calibration`. The board stays up and explains the failure rather than exiting.
