DIY Wi-Fi security camera system. Cheap, stateless Pi-based camera nodes stream RTSP/H.264 to a single old PC running Frigate as the NVR. All storage, motion detection, and UI lives on the mothership — camera nodes are replaceable.
![]() |
![]() |
|---|---|
| Cam 1 snapshot | Cam 2 snapshot |
[Cam Node 1] --Wi-Fi/RTSP--> \
[Cam Node 2] --Wi-Fi/RTSP--> --> [Mothership PC: Frigate + HDD]
[Cam Node N] --Wi-Fi/RTSP--> /
Edge nodes: capture + stream. Mothership: record, retain, view, manage.
- One sensor, one compute platform across all nodes (easy to mass-produce later).
- No hard drives at the edge. Storage lives centrally.
- Edge nodes are stateless — replace one by flashing an SD card.
- Push complexity into software on the mothership, not into each node.
- RTSP/H.264, not MJPEG. Wi-Fi bandwidth matters once you have 3+ cameras.
| Part | Choice | Notes | Price (USD) | Buy |
|---|---|---|---|---|
| Compute | Raspberry Pi Zero 2 W | Built-in 2.4 GHz Wi-Fi, CSI-2, H.264 encode @ 1080p30 | $16.35 | Canakit |
| Camera | Arducam IMX219 8MP (Pi Camera V2 equivalent) | Ships with 22-22pin Zero 2W ribbon in the box. Upgrade to Camera Module 3 NoIR only on nodes that need night vision. | $12.99 | Amazon |
| Storage | 32 GB SanDisk Ultra microSDHC (A1, 2-pack) | OS + app only, no footage | ~$16 (= $32 / 2) | Amazon |
| Power | Canakit 5V 2.5A Micro USB supply | Cheap bricks cause instability | $9.95 | Canakit |
| CSI cable | Zero 22-22pin ribbon | Included with the Arducam camera above — don't buy separately | $0 | — |
| Enclosure | 3D printed, 2-piece, PETG/ASA | ~40–60 g filament per unit | ~$2 (filament) | self-print |
| Optional | IR LED ring + light sensor | Only for NoIR night-vision units | ~$5 | — |
| Per-node total | ~$57 (day) |
| Part | Choice | Price (USD) |
|---|---|---|
| PC | Any old desktop / mini PC you already own | $0 |
| OS | Linux (Debian / Ubuntu LTS) | $0 |
| Storage | Surveillance-rated HDD (WD Purple / Seagate Skyhawk), 4–8 TB if buying new | ~$90–$180 (4–8 TB) / $0 if reusing |
| Network | Wired Gigabit Ethernet to the router (critical — don't put the NVR on Wi-Fi) | $0 |
| Optional | Google Coral USB TPU (for object detection in Frigate) | ~$60 |
- 5 GHz capable access point (even though Zero 2 W is 2.4 GHz only, this keeps the 2.4 band less congested for the cameras)
- Budget ~2–4 Mbps per 1080p camera
- Put cameras on a dedicated VLAN/SSID if your router supports it
libcamera / rpicam-vid --> RTSP stream
|
v
MediaMTX (RTSP server) ----------> rtsp://<cam-ip>:8554/cam
(H.264, 1080p30, 2 Mbps)
FastAPI control API --------------> http://<cam-ip>:8000
- GET /health uptime, temp, free disk
- GET /snapshot grab a JPEG frame
- POST /stream/restart restart mediamtx
- POST /reboot reboot the Pi
- POST /settings update resolution/fps/bitrate
systemd (auto-restart both services)
- OS: Raspberry Pi OS Lite (64-bit, Trixie / Debian 13)
- RTSP server: MediaMTX — small Go binary, uses the native
rpiCamerasource (hardware H.264, no ffmpeg pipe) - Control API: FastAPI + uvicorn (small Python service)
- Process management: systemd (one unit per service,
Restart=always)
- NVR: Frigate in Docker — handles recording, retention, live view, motion/object detection, clip export
- Reverse proxy (optional): Caddy or nginx for HTTPS on your LAN
- Monitoring (optional): Uptime Kuma pinging each node's
/health
Connect the CSI ribbon cable between the Pi Zero 2 W and the Arducam IMX219.
Parts:
| Pi Zero 2 W | Arducam IMX219 (back) | CSI ribbon cable |
|---|---|---|
![]() |
![]() |
![]() |
Cable orientation:
- The cable has two sides: a contacts side (exposed metal strips) and a plain side (white/blue stiffener tab).
- Both the Pi and the Arducam have a small plastic CSI connector with a flip-up latch.
![]() |
![]() |
|
|---|---|---|
| Contacts side (metal strips) | Plain side (blue stiffener tab) |
Steps:
- Gently flip up the plastic latch on the Pi's CSI connector (near the HDMI port).
- Slide the ribbon cable in with contacts facing the board (toward the PCB).
- Press the latch back down to lock.
- Repeat on the Arducam — flip latch, insert cable with contacts facing AWAY from the board (opposite of the Pi side), close latch.
- Insert the microSD card into the Pi's card slot.
Result:
| Assembled nodes |
|---|
![]() |
![]() |
- Download and install Raspberry Pi Imager (Windows).
- Run Imager and select:
- Device → Raspberry Pi Zero 2 W
- OS → Raspberry Pi OS (other) → Raspberry Pi OS Lite (64-bit)
- Storage → your microSD card
- Click Customisation in the sidebar and configure:
- Hostname:
cam01 - Enable SSH (use password or public key)
- Wi-Fi SSID + password
- Username + password
- Hostname:
- Hit Write and wait for it to finish.
- Before removing the SD card, image it back to your PC as a golden
.imgfile for future offline use:- Windows: Use Win32 Disk Imager:
- Set Device to your SD card drive (e.g.
D:\). - Click the folder icon next to Image File, pick a save location and name (e.g.
golden-cam.img). - Click Read and wait for it to finish.
- Set Device to your SD card drive (e.g.
- Linux/macOS:
sudo dd if=/dev/sdX of=golden-cam.img bs=4M status=progress
- Windows: Use Win32 Disk Imager:
- Put the SD card in the Pi, plug in HDMI before power, then power on.
- SSH in:
ssh cam01.local, thensudo apt update && sudo apt full-upgrade -y. - Confirm camera works:
rpicam-hello --timeout 2000(should detect the IMX219).
Trixie (Debian 13) note: If
rpicam-helloreports "No cameras available" even withcamera_auto_detect=1in/boot/firmware/config.txt, you also needdtoverlay=imx219. Add it manually toconfig.txtand reboot.
- In Raspberry Pi Imager: Device → Raspberry Pi Zero 2 W, OS → Use custom → select your
golden-cam.imgfile, Storage → your SD card. - Hit Write. No network needed.
- Boot the Pi and change the hostname (
sudo hostnamectl set-hostname cam02, etc.).
From your workstation, copy your SSH public key to the Pi so you don't need passwords:
# On the Pi, run:
mkdir -p ~/.ssh
cat >> ~/.ssh/authorized_keys
# Paste your public key (from ~/.ssh/id_ed25519.pub), then Ctrl+D
chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keysSet up passwordless sudo (needed for service management):
echo "YOUR_USERNAME ALL=(ALL) NOPASSWD: ALL" | sudo tee /etc/sudoers.d/YOUR_USERNAME# Find the latest version
curl -sI https://github.com/bluenviron/mediamtx/releases/latest 2>&1 | grep -i location
# Download and install (replace version as needed)
curl -sL https://github.com/bluenviron/mediamtx/releases/download/v1.17.1/mediamtx_v1.17.1_linux_arm64.tar.gz \
-o /tmp/mediamtx.tar.gz
sudo tar xzf /tmp/mediamtx.tar.gz -C /usr/local/bin mediamtxsudo mkdir -p /etc/mediamtx
sudo tee /etc/mediamtx/mediamtx.yml > /dev/null << 'EOF'
logLevel: info
logDestinations: [stdout]
api: yes
apiAddress: 127.0.0.1:9997
rtsp: yes
rtspAddress: :8554
paths:
cam:
source: rpiCamera
rpiCameraWidth: 1920
rpiCameraHeight: 1080
rpiCameraFPS: 30
rpiCameraBitrate: 2000000
rpiCameraIDRPeriod: 60
EOFsudo tee /etc/systemd/system/mediamtx.service > /dev/null << 'EOF'
[Unit]
Description=MediaMTX RTSP server
After=network-online.target
Wants=network-online.target
[Service]
ExecStart=/usr/local/bin/mediamtx /etc/mediamtx/mediamtx.yml
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable mediamtx
sudo systemctl start mediamtxVerify from the mothership:
ffplay rtsp://<cam-ip>:8554/cam
# or
vlc rtsp://<cam-ip>:8554/camsudo apt install -y python3-pip python3-venv ffmpeg
sudo mkdir -p /opt/camctl
sudo chown $USER:$USER /opt/camctl
python3 -m venv /opt/camctl/venv
/opt/camctl/venv/bin/pip install fastapi 'uvicorn[standard]' pyyamlCopy edge/camctl/main.py to /opt/camctl/main.py, then create the service:
sudo tee /etc/systemd/system/camctl.service > /dev/null << 'EOF'
[Unit]
Description=camctl FastAPI control service
After=mediamtx.service
Wants=mediamtx.service
[Service]
ExecStart=/opt/camctl/venv/bin/uvicorn main:app --host 0.0.0.0 --port 8000
WorkingDirectory=/opt/camctl
Restart=always
RestartSec=5
User=YOUR_USERNAME
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable camctl
sudo systemctl start camctlVerify: curl http://<cam-ip>:8000/health
- Install Docker Desktop (Windows) or Docker + docker compose (Linux) on the mothership PC.
- Configure cameras in
mothership/frigate/config.yml— point eachpathatrtsp://camNN.local:8554/camand tune retention. - Adjust the
TZenv var inmothership/docker-compose.ymlif you're not onAmerica/New_York. - Boot the mothership view:
Recordings, clips, exports, and snapshots land under
cd mothership docker compose up -dmothership/storage/(move that to a larger drive when you outgrow it). - Open the Frigate UI at http://localhost:5000 (or
http://mothership.local:5000from another LAN machine). The API is on port8971. - Useful commands:
docker compose logs -f frigate # tail logs docker compose restart frigate # reload after config edits docker compose down # stop the stack
- Confirm recording writes to
mothership/storage/recordings/and retention prunes correctly.
A YuNet sidecar container runs alongside Frigate, sampling each camera's live frame
every 2 seconds and saving cropped face regions to mothership/storage/faces/.
A separate Python toolkit in face-detection/ handles labeling and classifier training.
face-detector (Docker sidecar)
polls /api/{camera}/latest.jpg every 2s
└─ YuNet (OpenCV, 230 KB model, ~13 ms/frame)
└─ padded face crops → storage/faces/YYYY-MM-DD/*.jpg + *.json
![]() |
![]() |
![]() |
|---|---|---|
| Face crop | Face crop | Face crop |
cd face-detection
poetry run python classify_faces.pyOpens a two-tab Tkinter GUI:
- Labels tab — add person names (matthew, korrah, etc.)
- Classify tab — flip through face crops, assign labels with number keys (1–9), skip with S, undo with Backspace
Results saved to face-detection/face-labels.json.
poetry run python train-face-classifier.pyExtracts 512-d ArcFace embeddings (InsightFace buffalo_sc) for every labeled crop,
trains an RBF SVM on top, and saves the model to face-detection/models/face-classifier-{N}.pkl.
Re-run any time you add more labels — the index auto-increments.
Aim for 20–30 labeled crops per person for reliable accuracy.
| Var | Default | Purpose |
|---|---|---|
SAMPLE_INTERVAL |
2 |
Seconds between live frame grabs per camera |
SAVE_COOLDOWN |
3 |
Min seconds between saves per camera (prevents duplicate floods) |
MIN_FACE_SCORE |
0.6 |
YuNet confidence threshold |
FACE_PADDING |
0.2 |
Fractional padding added around each face bbox before crop |
- Design 3D-printed 2-piece case:
- Lens cutout aligned to camera module
- Cable strain relief
- Vent slots (no fan)
- Mounting tab / standard tripod thread
- Print in PETG or ASA (weather resistance if outdoor; add silicone gasket).
- Build 1–2 units, deploy, watch for a week for thermal / Wi-Fi / SD issues.
- Bake a "golden" SD image after node is configured →
ddclone for new nodes. - Set predictable hostnames
cam01..camNN+ mDNS. - Static DHCP reservations on the router.
- Add Uptime Kuma or simple cron script on mothership pinging each
/health.
- Swap Zero 2 W → CM5 + custom carrier.
- Swap Camera Module 3 → Camera Module 3 Sensor Assembly.
- Carrier adds PoE, eMMC, IR driver.
- Keep the software image nearly identical — that's the whole point.
| Cameras | Resolution | Bitrate each | Total |
|---|---|---|---|
| 4 | 1080p @ 15fps | 2.5 Mbps | 10 Mbps |
| 8 | 1080p @ 15fps | 2.5 Mbps | 20 Mbps |
| 8 | 720p @ 15fps | 1.5 Mbps | 12 Mbps |
A single cheap Wi-Fi router handles this fine. If you go past ~12 cameras on Wi-Fi, move some to Ethernet or PoE.
security-cam-setup/
├── docs/
│ └── images/ photos and screenshots
├── edge/
│ ├── camctl/ FastAPI control service
│ └── systemd/ *.service files
├── face-detection/
│ ├── classify_faces.py Tkinter labeling GUI
│ ├── train-face-classifier.py ArcFace + SVM trainer
│ ├── models/ trained classifier .pkl files (gitignored)
│ └── pyproject.toml
├── mothership/
│ ├── docker-compose.yml Frigate + face-detector sidecar
│ ├── face-detector/ YuNet Docker sidecar
│ └── frigate/
│ └── config.yml
├── enclosure/
│ └── *.stl / *.step 3D-print files
└── scripts/
├── flash-node.sh provision a fresh SD card
└── clone-golden.sh











