Files
Hermes ce188086cb
Build (Dev) / build (push) Failing after 1s
CI/CD / lint-and-typecheck (push) Failing after 0s
CI/CD / test (push) Has been skipped
CI/CD / build (push) Has been skipped
CI/CD / deploy (push) Has been skipped
docs: update CONTEXT.md with closed-network architecture and current state
- Replaced architecture diagram with travel router LAN design
- Added MQTT contract reference
- Updated issue status table with agent assignments
- Revised known limitations to reflect actual gaps
- Added network config section (subnet, static IP)
- Removed CUB-182 (canceled duplicate)

Post-sync: 13 done, 15 backlog, 1 in review, 1 canceled.
2026-05-21 21:11:33 +00:00

18 KiB
Raw Permalink Blame History

RemoteRig — Project Context

Last updated: 2026-05-21 (evening — post-planning sync) Repo: CubeCraft-Creations/remote-rig | Host: code.cubecraftcreations.com Local clone: /mnt/ai-storage/projects/remote-rig | Default branch: dev Discord: DISCORD_DEV_REMOTERIG_CHANNEL_ID Linear Epic: CUB-198 MQTT Contract: docs/MQTT_CONTRACT.md


Overview

RemoteRig is a multi-camera remote monitoring system. It provides a camera grid dashboard for monitoring multiple GoPro cameras remotely. Cameras push status via MQTT/HTTP, the UI shows a live grid with SSE updates, and the system supports start/stop recording control.

Target hardware: Raspberry Pi Zero 2 W as the central hub, with ESP32 nodes attached to each GoPro camera for status collection and MQTT communication.

Tech Stack

Layer Technology Notes
Backend Go 1.25+ Chi v5 router, SQLite (modernc.org/sqlite), go-yaml v3
Frontend React 19 + TypeScript 5.7 Vite 6, Tailwind CSS 3.4
State Zustand 5 Client-side camera state store
Icons lucide-react 0.469
Real-time SSE (Server-Sent Events) /api/v1/events/stream
Messaging MQTT Mosquitto broker for ESP32 → hub communication
Database SQLite WAL mode, foreign keys enabled
Auth API key (Bearer token) Middleware, configurable in config.yaml
Testing Vitest 4.1 + testing-library/react Unit tests for React components
Linting ESLint 9 npm run lint
CI/CD Gitea Actions .gitea/workflows/ci.yaml, build-dev.yaml, deploy-dev.yaml

Architecture

                   ┌──────────────────────────────────────────┐
                   │     Travel Router (self-contained LAN)    │
                   │         Subnet: 192.168.4.0/24            │
                   │         DHCP pool: .100-.200              │
                   └──────┬──────────┬──────────┬──────────────┘
                          │          │          │
          ┌───────────────┘          │          └───────────────┐
          ▼                          ▼                          ▼
   ┌──────────────┐         ┌──────────────┐         ┌──────────────────┐
   │   ESP32 #1   │         │   ESP32 #N   │         │  Pi Zero 2 W     │
   │  DHCP addr   │         │  DHCP addr   │         │  192.168.4.10    │
   │              │         │              │         │  (static IP)     │
   │ STA→GoPro AP │         │ STA→GoPro AP │         │                  │
   │ STA→Router   │         │ STA→Router   │         │  Mosquitto :1883 │
   │              │         │              │         │  Go API :8080    │
   │ MQTT→:1883   │         │ MQTT→:1883   │         │  React UI        │
   └──────┬───────┘         └──────┬───────┘         │  SQLite DB       │
          │                        │                  └──────────────────┘
          ▼                        ▼                           │
   ┌──────────────┐         ┌──────────────┐                  │
   │ GoPro Hero 3 │         │ GoPro Hero 3 │     SSE /api/v1/events/stream
   │ AP: 10.5.5.1 │         │ AP: 10.5.5.1 │                  │
   │ Wi-Fi only   │         │ Wi-Fi only   │                  ▼
   └──────────────┘         └──────────────┘         ┌──────────────────┐
                                                     │  User Device     │
                                                     │  (laptop/kiosk)  │
                                                     │  http://.4.10    │
                                                     └──────────────────┘

Network is fully self-contained — no internet dependency. The travel router creates the LAN. All devices connect to it. The Pi runs all services (Mosquitto, Go API, React UI, SQLite). ESP32s bridge the GoPro's AP to the LAN via MQTT.

Key Architecture Decisions (revised)

  • Closed travel router network — No venue Wi-Fi dependency. User brings their own router. All devices on 192.168.4.0/24.
  • ESP32 dual-STA — One STA to GoPro AP (10.5.5.1), one STA to travel router. No channel-hopping concerns on closed network.
  • ESP32 → GoPro over Wi-Fi — Bacpac I²C route rejected (30-pin Herobus connector too complex). HTTP to GoPro AP is proven and reliable.
  • MQTT for ESP32 → Hub — Lightweight, designed for IoT. Mosquitto on Pi. QoS 1 for status, QoS 2 for commands. Full contract: docs/MQTT_CONTRACT.md
  • SQLite over PostgreSQL — Single-node Pi Zero 2 W deployment. WAL mode for concurrent read/write.
  • SSE over WebSocket — Unidirectional hub → browser updates. Simpler, sufficient for status dashboard.
  • Chi router — Lightweight Go HTTP router with middleware support.
  • Zustand over Redux — Minimal boilerplate for camera status store.
  • API key auth — Simple bearer token; closed LAN, not internet-facing.
  • Camera auto-discovery — ESP32 publishes announce message on first MQTT connect. Hub auto-registers by MAC, assigns sequential cam-NNN ID.

Directory Layout

remote-rig/
├── cmd/server/main.go          # Entry point — config load, router setup, graceful shutdown
├── config.yaml                 # Runtime configuration
├── go.mod / go.sum             # Go dependencies
├── internal/
│   ├── api/
│   │   ├── api.go              # Package doc
│   │   ├── cameras.go          # GET /cameras, POST /cameras, GET /cameras/:id
│   │   ├── recording.go        # POST /cameras/:id/start, POST /cameras/:id/stop
│   │   └── status.go           # POST /cameras/:id/status (push from ESP32)
│   ├── auth/
│   │   └── middleware.go        # API key auth middleware
│   ├── db/
│   │   ├── db.go               # Open, migrations, WAL mode
│   │   └── migrations/
│   │       └── 001_create_tables.sql
│   └── events/
│       └── sse.go              # SSE hub (subscribe, broadcast)
├── pkg/models/
│   └── camera.go               # Camera, StatusLog, RecordingEvent, CameraStatus, Settings
├── src/                        # React frontend
│   ├── App.tsx                  # Main app — header, camera grid, footer
│   ├── components/
│   │   ├── CameraCard.tsx       # Single camera status card
│   │   ├── CameraCard.test.tsx  # Unit tests
│   │   └── index.ts
│   ├── hooks/
│   │   ├── useSSE.ts            # SSE connection hook
│   │   ├── useCameraStatus.ts   # Camera status hook
│   │   ├── useSystemHealth.ts   # System health hook
│   │   └── index.ts
│   ├── services/
│   │   └── api.ts               # API client
│   ├── store/
│   │   ├── useCameraStore.ts    # Zustand store
│   │   └── index.ts
│   ├── types/
│   │   └── index.ts             # TypeScript interfaces
│   ├── utils/
│   │   └── index.ts
│   └── main.tsx
├── docs/
│   ├── CONTEXT.md               # ← this file
│   └── plans/
│       └── 2026-05-21-cub-196-cameracard.md  # CameraCard implementation plan
├── .gitea/workflows/
│   ├── ci.yaml                 # PR CI: lint → typecheck → test → build
│   ├── build-dev.yaml          # Go binary build on dev push
│   └── deploy-dev.yaml         # SCP + SSH deploy with rollback
├── .env.example                # VITE_API_URL=http://localhost:8080/api
├── package.json
├── vite.config.ts
├── tailwind.config.js
└── tsconfig.json

Database Schema (SQLite)

cameras

Column Type Notes
camera_id TEXT PK Unique camera identifier
friendly_name TEXT NOT NULL Human-readable name
mac_address TEXT UNIQUE MAC address (optional)
created_at DATETIME Default now
updated_at DATETIME Default now

status_logs

Column Type Notes
id INTEGER PK AUTO
camera_id TEXT FK → cameras
recorded_at DATETIME Default now
battery_pct INTEGER Nullable
video_remaining_sec INTEGER Nullable
recording_state INTEGER 0=idle, 1=recording
mode TEXT e.g. "video"
resolution TEXT e.g. "1080p"
fps INTEGER
online INTEGER 0=offline, 1=online
raw_battery_pct REAL Float precision

Index: (camera_id, recorded_at DESC)

recording_events

Column Type Notes
id INTEGER PK AUTO
camera_id TEXT FK → cameras
started_at DATETIME NOT NULL
stopped_at DATETIME Null while recording
reason TEXT e.g. "manual"
duration INTEGER Seconds

Index: (camera_id, started_at DESC)

settings

Column Type Notes
key TEXT PK
value TEXT NOT NULL
updated_at DATETIME Default now

Default seeds: poll_interval_sec=30, low_battery_threshold=15, low_storage_alert_sec=300

API Endpoints

Method Path Auth Description
GET /health No Health check → {"status":"ok"}
GET /api/v1/cameras Yes List all cameras with latest status
POST /api/v1/cameras Yes Register a new camera
GET /api/v1/cameras/:id Yes Camera detail + latest status + 24h history
POST /api/v1/cameras/:id/start Yes Start recording + MQTT publish
POST /api/v1/cameras/:id/stop Yes Stop recording
POST /api/v1/cameras/:id/status Yes Push status from ESP32 node
GET /api/v1/events/stream No (SSE) Real-time camera status stream

Configuration (config.yaml)

db_path: "remoterig.db"         # SQLite database path
api_key: "changeme"             # Bearer token for API auth
port: 8080                      # HTTP listen port
read_timeout: 5s
write_timeout: 10s
idle_timeout: 120s
mqtt:
  broker: "localhost:1883"      # Mosquitto on Pi Zero 2 W
  client_id: "remoterig-hub"
platform:
  type: "pi-zero-2w"
  max_cameras: 16
network:
  subnet: "192.168.4.0/24"     # Travel router subnet
  hub_ip: "192.168.4.10"       # Pi Zero 2 W static IP

Frontend Component Tree

App
├── Header
│   ├── Logo + Title ("RemoteRig Dashboard")
│   └── Stats bar (online count, recording count)
├── CameraGrid
│   └── CameraCard[] (responsive: 1→2→3→4 columns)
│       ├── Camera name + online/offline badge
│       ├── Resolution + FPS display
│       ├── Recording indicator (pulsing dot + REC/IDLE badge)
│       ├── Battery bar (color-coded: green/yellow/red)
│       └── Footer (Live/Last seen + video remaining)
└── Footer
    └── "RemoteRig v0.1.0 — Multi-Camera Remote Monitoring System"

Empty state: "Waiting for cameras..." with pulsing radio icon when no cameras connected. Offline state: Camera card dimmed with dashed border, shows "Last seen Xm ago".

Color Palette (Tailwind — dark dashboard theme)

Custom theme in tailwind.config.js:

  • rig-dark-900 (background), rig-dark-800, rig-dark-700 (cards), etc.
  • rig-accent (accent color)
  • rig-success (green — battery ≥50%, online)
  • rig-warning (yellow — battery 15-49%)
  • rig-danger (red — battery <15%, offline, recording)

Linear Issue Map

Last synced: 2026-05-21 (evening)

CUB Title Status Agent
198 Epic: Multi-camera remote monitoring system Backlog
238 Define MQTT message format contract Done Dex
228 Add battery_calibration_offset to cameras table Backlog Hex
230 ESP32 offline status buffering and replay Backlog Pip
232 Implement MQTT subscriber in Go hub Backlog Dex
229 Design camera auto-discovery and registration flow Backlog Dex
231 Mosquitto MQTT broker setup on Pi Zero 2 W Backlog Dex
233 Verify and harden SSE endpoint Backlog Dex
234 Verify and harden all camera API endpoints Backlog Dex
235 Implement GET /api/v1/cameras/:id with 24h history Backlog Dex
236 Implement POST /api/v1/cameras registration Backlog Dex
237 Update CONTEXT.md to actual state Backlog Otto
173 Confirm GoPro Hero 3 WiFi control API Done Otto
174 ESP32 firmware baseline Done Pip
175 Central hub backend (Go service) Done Dex
177 Database schema (cameras, events, status_logs) Done Hex
180 Risk mitigation checklist Done
181 Scaffold Go module, directory layout, config Done
187 POST start recording + MQTT publish Done Dex
194 Scaffold Vite + React + TypeScript + Tailwind Done
195 React SSE hook (useSSE.ts) + Zustand store Done Rex
196 CameraCard component + 16 unit tests Done Rex
179 Logging & persistence strategy Done
208 Add README with project overview Done Hermes
182 Duplicate of CUB-181 Canceled
183 SQLite schema migration + DB init Backlog Hex
184 API key auth middleware Backlog Dex
185 Camera/StatusLog/RecordingEvent Go models Backlog Hex
186 GET /api/v1/cameras (list with live status) Backlog Dex
188 POST stop recording Backlog Dex
189 POST register new camera Backlog Dex
190 GET camera detail + history Backlog Dex
191 POST push-status (HTTP ingestion) Backlog Dex
192 MQTT subscriber Backlog Dex
193 SSE /api/v1/events/stream endpoint Backlog Dex
178 UX/UI design mockups Backlog Sketch
197 Dashboard camera grid wireframe In Review Sketch
176 Frontend umbrella (React + Tailwind) Backlog Rex

CI/CD Pipeline

ci.yaml (PR gate)

Runs on push/PR to dev and main:

  1. lint-and-typecheck: npm ci → eslint → tsc --noEmit
  2. test (needs lint): npm test (vitest)
  3. build (needs test): npm run build → upload dist artifact

build-dev.yaml

Triggered by repository_dispatch: dev-build-success:

  • Checks out, downloads artifact, builds Go binary

deploy-dev.yaml

Triggered by workflow_dispatch:

  • SCP binary + deploy script to dev host
  • Deploy with backup/rollback, systemctl restart
  • Failure notification

Key Design Decisions

  1. SQLite chosen over PostgreSQL — Single-node Pi Zero 2 W deployment; no need for a separate DB server. WAL mode for concurrent read/write.
  2. SSE chosen over WebSocket — Unidirectional updates (hub → browser) are sufficient for status dashboard; SSE is simpler to implement and maintain.
  3. Chi router — Lightweight, idiomatic Go HTTP router with middleware support.
  4. Zustand over Redux — Minimal boilerplate for the camera status store.
  5. API key auth — Simple bearer token; hub is on a local network, not internet-facing.
  6. MQTT — Standard IoT protocol for ESP32 communication; Mosquitto broker runs locally on the Pi.
  7. Recording state tracking — Status push handler detects recording state changes and automatically opens/closes recording_events rows.

Known Limitations

  • MQTT subscriber not yet implemented (CUB-232) — ESP32→hub communication backbone is designed (see MQTT_CONTRACT.md) but not built. Currently only HTTP status push is wired up.
  • SSE endpoint needs verification (CUB-233) — Frontend SSE hook exists (merged PR #2), backend SSE hub code exists (140 lines) but needs heartbeat, reconnection, and integration testing.
  • No camera auto-discovery (CUB-229) — Cameras must be manually registered. ESP32 announce protocol designed in MQTT contract but not implemented.
  • No battery calibration (CUB-228) — GoPro Hero 3 reports raw byte; per-camera calibration offset column not yet added to schema.
  • No offline buffering on ESP32 (CUB-230) — If travel router Wi-Fi drops, status data is lost. SPIFFS buffer designed but not implemented.
  • CameraCard wireframe pending (CUB-197) — Dashboard UI has live code but needs UX review/wireframe sign-off.
  • remoterig.db committed to repo — Should be in .gitignore for production. Low priority (convenient for dev).
  • Time sync TBD — ESP32s need accurate timestamps without internet. Options: Pi as NTP server, GPS module, or HTTP time endpoint. See MQTT contract open questions.

Getting Started

# Clone
cd /mnt/ai-storage/projects/remote-rig
git checkout dev
git pull origin dev

# Backend
go run cmd/server/main.go
# → runs on :8080

# Frontend
cp .env.example .env    # edit if needed
npm install
npm run dev             # → Vite dev server with API proxy

Default Agent Assignments

Area Agent Notes
Backend (Go API, MQTT, SSE) Dex gitea-dex MCP
Database (SQLite schema/migrations) Hex gitea-hex MCP
Frontend (React, Tailwind) Rex gitea-rex MCP
Hardware (ESP32 firmware, GPIO) Pip gitea-pip MCP
Design (wireframes, UX) Sketch