Files
remote-rig/CONTEXT.md
T
Joshua King 7c07338707
Build (Dev) / build (push) Successful in 13s
CI / quality (push) Successful in 12s
CI / quality (pull_request) Successful in 11s
docs: update CONTEXT.md — control-path wiring, dashboard, decisions 18-21
- §11: dashboard now renders live (SSE/seed/kiosk); GoPro monitoring works;
  flag that camera CONTROL is blocked by the faulty XIAO->ESP command wire
  (status RX works, command TX doesn't). Dedupe the token/default-branch lines.
- §9: add decisions for the MQTT control path, SSE longevity + REST seed,
  nullable status JSON (NaN% fix), and UART being two independent wires.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 21:57:07 -04:00

269 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# RemoteRig — Project Working Context
> **Purpose of this file:** a living, high-signal context + decision log for the
> RemoteRig project. It's the primary onboarding doc for humans and for any LLM
> working on the repo. Keep it updated as decisions are made.
> **Last updated:** 2026-06-05.
>
> Deeper references: `docs/CONTEXT.md` (system architecture detail),
> `docs/MQTT_CONTRACT.md` (MQTT topics/payloads), `docs/design/` (design notes),
> `hardware/README.md` (case/wiring/BOM), `README.md` (hub build/deploy).
---
## 1. What this is
RemoteRig is a multi-camera **GoPro Hero 3 monitoring & control** system for field
recording (large concerts in auditoriums, high-school marching band at stadiums).
Founder: **Joshua / CubeCraft Creations**.
Scope: **monitor status (battery, recording, link) and start/stop multiple GoPros**
over a closed, self-contained travel-router network. **No video flows through the
hub** — footage records locally to each GoPro's SD card. This keeps the hub a
lightweight control plane.
## 2. Architecture
```
GoPro Hero 3 ──Wi-Fi(10.5.5.1)── ESP-01S ──UART(JSON)── XIAO ESP32-C6 ──Wi-Fi/MQTT── Pi hub ── Dashboard
(per camera) (GoPro bridge) (MQTT bridge + OLED/LED) (Mosquitto+Go+SQLite)
```
**Camera node** (one per GoPro), two boards:
- **XIAO ESP32-C6** — main MCU / MQTT bridge. Joins the RemoteRig Wi-Fi, talks MQTT
to the hub, drives the OLED + RGB status LED, reads camera status from the ESP-01S
over UART. Powered from the 5V rail.
- **ESP-01S (ESP8266)** — GoPro Wi-Fi bridge. Joins the GoPro's AP (`10.5.5.1`),
relays status/commands to the XIAO over UART. Powered from its **own 3.3V buck**
(not the XIAO 3V3 pin — Wi-Fi TX spikes ~300 mA).
**Hub** — Raspberry Pi Zero 2 W (hostname `remote-rig-hub`, user `overseer`):
- **Mosquitto** MQTT broker (`:1883`, anonymous, listens on `0.0.0.0`).
- **Go controller** (`remoterig`, systemd service) — MQTT subscriber → SQLite,
REST API + SSE, serves the embedded React dashboard on `:8080`.
- **SQLite** (`/opt/remoterig/remoterig.db`).
- Decided to **stay on the Zero 2 W** (workload is tiny; a Pi 5 only makes sense if
video preview/ingest is ever added — not planned).
## 3. Network
- **RemoteRig travel router**, subnet **`192.168.8.0/24`**, gateway `192.168.8.1`,
has a WAN uplink (internet available — used for the Pi to pull builds).
- **Hub static IP: `192.168.8.56`** (`:1883` MQTT, `:8080` dashboard/API).
- Cameras get DHCP `192.168.8.x`.
- The GoPro AP network (`10.5.5.1`) is separate and only the ESP-01S touches it.
- **History:** the project was originally designed around `10.60.1.0/24`; it was
re-addressed to `192.168.8.0/24` to match the actual travel router (commit
`b0062f1`). Wi-Fi SSID `RemoteRig` (creds in `firmware/data/config.json`).
## 4. Repository & workflow
- **Gitea:** `ssh://sc-gitea@code.cubecraftcreations.com:2288/CubeCraft-Creations/remote-rig`
(web/API on `https://code.cubecraftcreations.com`, private repo).
- **Default/integration branch: `dev`** (work lands here; merges from `main`).
- Layout: `firmware/` (PlatformIO), `cmd/`,`internal/`,`pkg/` (Go hub),
`src/` (React/Vite/TS dashboard), `scripts/` (Pi setup/deploy), `.gitea/workflows/`
(CI), `hardware/` (CAD/wiring), `docs/`.
## 5. Tech stack
| Area | Choice |
|------|--------|
| Camera firmware | PlatformIO / Arduino |
| C6 env | `seeed_xiao_esp32c6`**pioarduino** platform fork, **LittleFS**, U8g2 |
| ESP-01S env | `esp8266-camera` — board `esp01_1m`, flash `dout` |
| Hub | **Go 1.25** (single static binary, `//go:embed` frontend) |
| Dashboard | React + Vite + TypeScript + Tailwind (Vitest) |
| Storage | **SQLite** (not Postgres) |
| Broker | **Mosquitto** |
| CI/CD | **Gitea Actions** (pull-based deploy) |
## 6. Camera node hardware (XIAO ESP32-C6 pin map)
| Pin | Use |
|-----|-----|
| 5V/VIN | rocker → 5V rail |
| D4/SDA, D5/SCL | 1.3" **SH1106** OLED (I2C @ `0x3C`) |
| D0 / D1 / D2 | RGB STAT LED R/G/B (220Ω each), **common-anode** |
| D6 (TX) / D7 (RX) | UART (`Serial1`) to ESP-01S (crossed) |
| D8 / D10 | **reserved** for ESP-01S UART-OTA control (RST / GPIO0) — not driven yet |
| 5V rail (330Ω) | PWR LED (not an MCU pin) |
Canonical wiring: Notion "XIAO ESP32-C6 Pin-to-Pin Wiring Diagram" + `hardware/README.md`.
## 7. Firmware behavior
**XIAO ESP32-C6 (`firmware/src/esp32-mqtt-bridge.cpp`)** — fw `0.4.0`:
- Loads config from LittleFS `/config.json` (Wi-Fi, broker, camera_id, battery cal).
- **Self-assigned camera_id** = device id `rig-<last3 MAC>` (e.g. `rig-86d978`) — see
decision #7. Subscribes `remoterig/cameras/<id>/command`, announces on
`remoterig/cameras/<id>/announce`, publishes `.../status`.
- OLED status panel (CAM id / REC + session timer / BAT / LINK / CAM reachability).
- RGB STAT LED health colors: blue=boot, red=offline, magenta=Wi-Fi-no-hub,
yellow=hub-no-camera, green=healthy.
- Battery calibration: two-point linear (raw offset-57 → %), persisted; `battery_pct`
emitted only when calibrated.
- No-reflash config: `set_camera_config` (MQTT) → forwarded to ESP-01S as `set_config`.
**ESP-01S (`firmware/src/esp8266-camera-bridge.cpp`)**:
- Joins GoPro AP, polls status, relays JSON over UART; `set_config` persists to LittleFS.
- No status LED (GPIO1 is the UART TX).
- ⚠️ **Known bug:** `fetchStatus()` GETs the **shutter** endpoint
(`/bacpac/SH?...&p=%01`) instead of a real status read — would *start recording*
each poll. Needs the GoPro Hero 3 protocol corrected + validated against a real
camera (also verify password/SSID). **Do not point at a live GoPro until fixed.**
**Provisioning:** `firmware/data/config.json` is flashed to the C6's LittleFS via
`pio run -e seeed_xiao_esp32c6 -t uploadfs`. Per maintainer decision the real Wi-Fi
password lives in this tracked file (private repo, low-sensitivity closed network).
## 8. Hub & CI/CD (pull-based deploy)
**Flow:** `push to dev` → Gitea Actions `build-dev.yaml` builds the React frontend
(into `cmd/server/src/dist`, embedded) + cross-compiles the **arm64** Go binary →
publishes a rolling **`dev-latest`** release (binary + `sha256` + `version.txt`) via
a Node script (`.gitea/scripts/publish-release.mjs`). The Pi's
`remoterig-update.timer` runs `scripts/pi-update.sh` every ~5 min → compares
`version.txt`, downloads + checksum-verifies, **atomically replaces** the binary,
restarts, health-checks (rolls back on failure).
**Why pull, not push:** the Pi is on a closed travel-router LAN the CI runner can't
reach; the Pi pulls instead.
- `ci.yaml` — frontend quality gates (lint/typecheck/test/build), single job.
- First-time Pi setup: `sudo bash scripts/setup-pi.sh --config config.yaml` (installs
Mosquitto, the service, the updater timer, static IP).
- Pi files: `/opt/remoterig/{remoterig, config.yaml, update.env, VERSION, deploy.sh, pi-update.sh}`.
- Future ESP-01S firmware OTA: `docs/design/esp01s-uart-ota.md` ("XIAO as flasher").
### Gitea Actions runner notes (important)
- Runner `remote-rig-runner`, label **`go-react`** (Dockerized act_runner). Workflows
must use `runs-on: go-react`.
- The `go-react` image has **Node but not Go** → use `setup-go` (its static binary
runs); get Node from the image (**don't** use `setup-node` — its dynamically-linked
Node won't execute here, "cannot execute: required file not found").
- Gitea doesn't support `actions/upload-artifact@v4`. No `curl`/`jq`/`sudo` on the
runner — the release publish is done in Node.
- The runner's network to github.com/Gitea is flaky (ECONNRESET) → keep few action
clones; `publish-release.mjs` retries.
- Rolling release tag is **`dev-latest`**, NOT `dev` (a `dev` tag collides with the
`dev` branch → ambiguous refs).
- Inspect CI from a dev machine with the **`tea` CLI**: `tea actions runs list|view|logs`,
`tea release list` (note "completed" ≠ success — check Conclusion).
## 9. Key decisions & gotchas (log)
1. **MCU:** ESP32-C3 Super Mini → **XIAO ESP32-C6**; C6 needs the **pioarduino**
platform fork. USB-CDC-on-boot for `Serial` over native USB.
2. **Mac build toolchain:** use `~/.platformio/penv/bin/pio` (Python 3.11), **not**
the pyenv 3.9.21 shim (too old for pioarduino).
3. **C6 filesystem = LittleFS** (pioarduino `uploadfs` builds LittleFS, not SPIFFS) —
the firmware reads `/config.json` (data file must be named `config.json`).
4. **Network re-addressed** `10.60.1.0/24``192.168.8.0/24`.
5. **Wi-Fi password kept in git** (`firmware/data/config.json`) — maintainer decision
(private repo, low-sensitivity, closed net).
6. **RGB LED is common-anode** (`RGB_COMMON_ANODE 1`); OLED is **SH1106** @ `0x3C`.
7. **Camera registration = "Option B" self-assigned IDs:** node uses `rig-<MAC>` as a
stable `camera_id` from first boot; the hub registers under that id. No `cam-NNN`
assignment, no `registered`-reply handshake. (`docs/MQTT_CONTRACT.md` updated.)
8. **Hub tolerates clockless status timestamps** — nodes have no RTC; firmware omits
`timestamp`, the hub stamps server-side (it used to reject the status).
9. **ESP-01S updates:** settings change live via `set_config` (no reflash); full
firmware OTA is the future XIAO-as-flasher path (`docs/design/esp01s-uart-ota.md`).
10. **Pull-based deploy** via rolling `dev-latest` release + atomic binary replace +
network retries.
11. **Pi systemd service user = `overseer`** (this Pi has no `pi` user); `setup-pi.sh`
now defaults the service user to `$SUDO_USER`.
12. **Hub embeds the frontend** via `//go:embed all:src/dist`; Vite builds into
`cmd/server/src/dist` (a committed `index.html` placeholder keeps the embed valid).
13. **SQLite/modernc datetime:** `modernc.org/sqlite` returns a `COALESCE()`/expression
as a raw string (no type affinity) → can't scan into `time.Time`. Scan plain
`DATETIME` columns (returned as `time.Time`) via `sql.NullTime`; `ListCameras`
`COALESCE`s NULL int/bool status columns. Nodes send no usable timestamp on
status/heartbeat (numeric `millis()`) — the hub ignores it / stamps server-side.
14. **Legacy id migration:** `handleAnnounce` migrates a MAC registered under a
different `camera_id` (e.g. a pre-self-id `cam-NNN`) to the node's self-id.
15. **Kiosk API auth:** `api_key: ""` in `config.yaml` = no auth on `/api/v1/*`
(closed LAN, consistent with anonymous MQTT). A non-empty key requires the SPA
to send `X-API-Key` too, or the dashboard 401s and shows no cameras.
16. **Ops gotcha:** the pull updater swaps only the **binary**. `config.yaml` is NOT
auto-deployed — change it on the Pi (`/opt/remoterig/config.yaml` + restart).
17. **GoPro Hero 3 protocol** (validated on a Silver): API host `10.5.5.9`, status
read `GET /camera/se?t=<pwd>` (binary, starts with 0x00 — read the stream, not
Arduino String), recording = byte 29, battery = byte 19; record start/stop =
`/bacpac/SH?t=<pwd>&p=%01/%00`. ESP-01S flashing needs RST tied HIGH (RST→GND
holds it in reset) and a known-good UART adapter (verify with a TX↔RX loopback).
18. **Control path:** `/cameras/{id}/start|stop` publish `{"command":...}` to
`remoterig/cameras/<id>/command` via `Subscriber.PublishCommand`; the XIAO forwards
it over UART to the ESP-01S. (The handlers used to only write a DB row — no command
was ever sent.)
19. **SSE longevity:** no global `middleware.Timeout` and `WriteTimeout: 0` — a write
deadline terminates the long-lived `/events/stream` (it was dying at 10s). The SPA
also **seeds** the list via `GET /api/v1/cameras` on mount (SSE only pushes on change).
20. **Nullable status JSON:** `battery_pct`/`video_remaining_sec` serialized as `null`
(not `omitempty`) — omitting them became `undefined` in the SPA → "NaN%".
21. **UART is two independent wires:** status (ESP `TX/GPIO1` → XIAO `D7`) and commands
(XIAO `D6` → ESP `RX/GPIO3`) are separate paths — receiving status does NOT prove
the command direction works. Verify the command path with the `set_config`
poll-interval test (status rate should change).
## 10. Conventions
- Production hub/controller in **Go**; Python fine for diagnostics/experiments/migrations.
- **SQLite**, not Postgres. **Timezone: US Eastern.**
- Work on `dev`; commit messages end with a `Co-Authored-By` trailer.
- Canonical design docs live in **Notion** (Remote Rig parent page) and the repo
`docs/`; CAD in Seafile; code/build in Gitea.
## 11. Current status & open items (2026-06-05)
**Working / proven on hardware:**
- Hub up on the Pi (Mosquitto + `remoterig` + SQLite); **dashboard renders live**
(kiosk mode `api_key:""`, SSE kept alive, list seeded via REST on mount).
- Full CI/CD loop proven: commit → CI build → `dev-latest` → Pi self-update
(checksum, atomic replace, health-check) → service active.
- C6 (fw `0.4.0`) self-IDs as `rig-86d978`, registered + listed.
- **GoPro monitoring works (Hero 3 Silver):** ESP-01S joins `goprosilver-1`, reads
`/camera/se`, and `online:true` + `battery_raw` + `video_remaining_sec` flow
GoPro → ESP-01S → XIAO → MQTT → hub → SQLite → API/SSE → dashboard.
- Hub publishes start/stop commands to `…/<id>/command` (verified on the bus).
**In progress / unresolved:**
- **Camera CONTROL not working — XIAO→ESP command wire is faulty.** Status (ESP→XIAO,
`GPIO1→D7`) works, but the command direction (XIAO `D6` → ESP `RX/GPIO3`) does not,
so `start_recording`/`set_config` never reach the ESP. Confirmed via the `set_config`
poll-interval test (status rate didn't change). Fix/re-seat that one jumper; then
Record + live config will work. (See decision #21.)
- **Battery calibration:** `battery_raw` (~5659) flows; set `set_battery_cal`
(`raw_min/raw_max`, provisionally 0/100) for `battery_pct` — but this is a *command*,
so it's blocked by the same XIAO→ESP wire above. `video_remaining` offset (25-26)
provisional. SPA now shows "N/A" (not "NaN%") when `battery_pct` is null.
- **Pi SD-card health:** a transient `Input/output error` on core binaries cleared on
reboot — watch for recurrence (failing card); re-image via `setup-pi.sh` if it
returns (everything is reproducible from git).
- **Rotate the Gitea runner registration token** (was exposed in a setup paste).
- Gitea repo **default-branch HEAD** points at a nonexistent ref — set default branch.
- Optional: clear the stale **retained** MQTT message at
`remoterig/cameras/announce-rig-86d978` (from old firmware).
## 12. Handy commands
```bash
# Build/flash C6 firmware (Mac):
~/.platformio/penv/bin/pio run -d firmware -e seeed_xiao_esp32c6 -t upload
~/.platformio/penv/bin/pio run -d firmware -e seeed_xiao_esp32c6 -t uploadfs # provision /config.json
# ESP-01S: needs a 3.3V USB-UART adapter with GPIO0->GND for flash mode.
# Inspect Gitea CI (Mac, tea CLI logged in as 'overseer'):
tea actions runs list --repo CubeCraft-Creations/remote-rig
tea actions runs view <id> --repo … ; tea actions runs logs <id> --repo …
tea release list --repo CubeCraft-Creations/remote-rig
# On the Pi:
sudo systemctl start remoterig-update.service # force a pull/deploy
cat /opt/remoterig/VERSION ; systemctl is-active remoterig
journalctl -u remoterig -n 40 --no-pager
mosquitto_sub -h localhost -t 'remoterig/#' -v
curl -s -H "X-API-Key: changeme" http://localhost:8080/api/v1/cameras
```