On the go-react runner image, npm/node/go already work (npm ci succeeds),
but the setup-go/setup-node actions install tool-cache binaries that can't
execute on this runner (node/22.22.3/x64: "cannot execute"), failing the
jobs in their post steps. Drop those actions and use the image's built-in
toolchains; also reduces flaky github.com action clones.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The workflows used runs-on: ubuntu-latest, which mapped to
docker.gitea.com/runner-images:ubuntu-latest — an image whose Node from
setup-node won't execute (exit 127) and which lacks curl/jq. The runner
already advertises a purpose-built "go-react" CI image; point the
workflows at it instead.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The build-dev publish step failed with exit 127 — the act runner image is
minimal (no curl, jq, or sudo; runs as root). Node is always present
(setup-node), so do the release publish in Node using built-in fetch/crypto
and FormData/Blob for the asset upload. No external tools needed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
cmd/server/main.go has //go:embed all:src/dist (relative to cmd/server/),
but Vite built to repo-root dist/, so cmd/server/src/dist never existed and
every `go build` failed with "pattern all:src/dist: no matching files found".
The hub binary has never built in CI as a result.
- vite.config.ts: outDir -> cmd/server/src/dist (emptyOutDir)
- commit cmd/server/src/dist/index.html placeholder so the embed always has
a file (real build overwrites it)
- .gitignore: scope dist ignore to /dist; ignore cmd/server/src/dist/* but
keep the index.html placeholder (the prior !src/dist/index.html rule
pointed at the wrong path)
- ci.yaml: upload artifact from the new output path
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Pi is on a closed travel-router LAN, so push-based deploy from a
runner can't reach it. Switch to pull: the runner builds + publishes,
the Pi fetches.
- build-dev.yaml: after the arm64 build, publish the binary + sha256 +
version.txt to a rolling "dev" Gitea release (replaces the
upload-artifact + repository_dispatch -> deploy-dev hop)
- remove deploy-dev.yaml (push/scp-based deploy no longer used)
- scripts/pi-update.sh: poll the dev release, verify sha256, install via
deploy.sh (backup/restart/rollback); only updates when version changes
- scripts/remoterig-update.{service,timer}: run the updater every 5 min
- setup-pi.sh: install deploy.sh + pi-update.sh + update.env template +
the updater timer; summary now reflects the pull flow
- README: document the pull-based CI/CD; fix stale GOARM=6 (Zero 2 W is
arm64 on 64-bit OS / arm GOARM=7 on 32-bit)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per maintainer decision: this is a private repo and the closed
travel-router Wi-Fi password is low-sensitivity, so keep the real
value in the tracked config for reproducible uploadfs provisioning.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The project was designed around a 10.60.1.0/24 travel-router network,
but the actual RemoteRig router uses 192.168.8.0/24 (the C6 associates
and gets 192.168.8.x; hub confirmed at 192.168.8.56). Replace the
network prefix everywhere (last octet preserved; GoPro 10.5.5.1 left
alone).
- scripts/setup-pi.sh: static IP 192.168.8.56/24, gateway 192.168.8.1,
deploy/health command examples updated
- esp32-mqtt-bridge.cpp: default mqtt_broker -> 192.168.8.56
- firmware/data/config.json: broker -> 192.168.8.56 (wifi_password kept
blank in git; real value flashed to the device only)
- docs (CONTEXT, MQTT_CONTRACT, READMEs, wireframes): gateway/hub/DHCP
and example IPs re-addressed for consistency
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The C6 never loaded its /config.json, so it fell back to defaults
(SSID RemoteRig, empty password) and couldn't join Wi-Fi. Two bugs:
- Data file was named esp32-config.json but the firmware reads
/config.json → renamed to config.json.
- Firmware used SPIFFS while pioarduino's uploadfs builds a LittleFS
image; the SPIFFS mount then reformatted it empty. Switch the C6 to
LittleFS (matches the toolchain default and the ESP-01S).
Also:
- log loaded ssid/broker/camera_id on config load (not the password)
- platformio.ini: land the ESP-01S env retarget (board d1_mini ->
esp01_1m, dout, upload_speed 115200) that was missed in 403e1d9
- committed config.json keeps wifi_password blank; the real value is
flashed to the device, not stored in git
Verified: C6 loads config and associates (got a DHCP lease). MQTT to
the broker is a separate network issue (hub IP / subnet).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Updating the buried ESP-01S currently means a USB-UART adapter and a
GPIO0 jumper. Add a path to change its settings without reflashing, and
lay the groundwork for full firmware updates over the existing UART.
set_config (no reflash for settings):
- ESP-01S: add saveConfig() + a set_config command — updates GoPro
SSID/password/IP and poll interval, persists to LittleFS, acks, and
re-associates Wi-Fi if creds changed
- XIAO: forward an MQTT set_camera_config down to the ESP-01S over UART
(hub -> MQTT -> XIAO -> UART -> ESP-01S/LittleFS)
UART-OTA groundwork ("XIAO as flasher"):
- reserve XIAO GPIOs ESP01_RST_PIN=D8, ESP01_PGM_PIN=D10 for driving the
ESP-01S serial bootloader (not driven yet)
- docs/design/esp01s-uart-ota.md: full design (why Wi-Fi OTA doesn't fit
the 1MB ESP-01S on the GoPro AP, bootloader entry, ROM flash protocol,
HTTP-pull delivery, scope)
- hardware/README.md: fix stale ESP32-C3 -> XIAO ESP32-C6 wiring, add the
two control lines (Notion wiring diagram updated to match)
Both firmwares build clean and are flashed; set_config round-trip needs
the broker to exercise end-to-end.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Battery calibration:
- two-point linear cal (bat_raw_min->0%, bat_raw_max->100%) of the
GoPro offset-57 raw byte, persisted in SPIFFS config
- publish battery_pct in MQTT status only when calibrated (omit
otherwise, per MQTT_CONTRACT); OLED shows % when calibrated, raw
until then
- set_battery_cal MQTT command: explicit {raw_min,raw_max} or
capture-current {point:"full"|"empty"} for field calibration
RGB STAT LED:
- drive D0/D1/D2 (R/G/B) with health colors instead of the single
green channel: red=offline, magenta=wifi-but-no-hub,
yellow=hub-but-no-camera, green=healthy; blue during boot
- RGB_COMMON_ANODE polarity flag; this module is common-anode
Verified on hardware: boots, OLED ok, RGB shows correct colors
(blue->red on the bench).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bring up the 1.3" SH1106 128x64 I2C OLED on the XIAO ESP32-C6
(D4/SDA, D5/SCL @ 0x3C) per the Notion wiring diagram.
- add U8g2 dependency to the seeed_xiao_esp32c6 env
- I2C bus scan at boot (logs responders to serial)
- boot splash + live status screen: camera id, IDLE/REC + session
timer, battery (raw until calibrated) + video-remaining, hub link
state (MQTT/wifi/offline), and camera reachability
- refresh runs at the top of loop() so the panel stays live even
when WiFi/MQTT are down
Verified on hardware: I2C scan finds 0x3C, U8g2 begin ok, panel
shows clean readable text.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The MCU changed from ESP32 Dev Board to a Seeed Studio XIAO ESP32-C6,
but the firmware still targeted esp32dev. Retarget it and fix the
build so it compiles and flashes for the C6.
platformio.ini:
- env esp32-mqtt -> seeed_xiao_esp32c6 on the pioarduino platform fork
(mainline espressif32 lags the Arduino-core 3.x the C6 needs)
- add ARDUINO_USB_MODE=1 / ARDUINO_USB_CDC_ON_BOOT=1 for Serial over
the C6 native USB
- fix build_src_filter ordering in BOTH envs: -<*.cpp> ran last and
re-excluded the target, leaving setup()/loop() undefined at link
esp32-mqtt-bridge.cpp:
- UART Serial2 RX16/TX17 -> Serial1 RX=D7/TX=D6 (XIAO C6 mapping)
- status LED GPIO2 -> D1 (green channel of the RGB STAT LED)
- fix pre-existing ArduinoJson v7 / PubSubClient build errors
(.c_str() on a const char*, String topic where const char* required)
Verified: builds clean and boots on hardware (native-USB serial banner
confirmed).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add migration 002: UNIQUE index on status_logs(camera_id, recorded_at)
- Upgrade migration system to version-tracked (schema_version table)
- Prevents race-condition double-inserts that application-level COUNT(*) check cannot guard against
- Complements existing application-level dedup from CUB-230
- Add request validation: Content-Type check, body size limit (64KB)
- Add field length validation (camera_id: 64, friendly_name: 128, mode: 32, resolution: 32)
- Add FPS range validation (0-240)
- Add battery_pct range validation (0-100)
- Replace ad-hoc map[string]string errors with structured APIError {error, code, details}
- Fix isUniqueConstraintErr to catch both camera_id and mac_address constraint violations
- Fix MacAddress model field from string to *string for NULL handling
- Fix splitSQL to strip -- line comments before splitting (was causing migration failures with modernc.org/sqlite)
- Add 30 integration tests covering all endpoints
- All tests pass: ok github.com/cubecraft/remoterig/internal/api
- Add dedup check in handleStatus using (camera_id, recorded_at) uniqueness
- Skip insert if duplicate detected - logs replayed entries
- Go mod: updated version to 1.19
Rotatable 3D render of the tripod-mounted dual-ESP case:
- Case body with rounded corners and lid
- Stacked ESP32 + ESP8266 boards inside
- LED indicator, USB port, ventilation slots
- Tripod pole with C-clamp mount
- USB cables, screws, chip details
- Drag to rotate, scroll to zoom
- Open in any browser
Replaced GoPro-sleeve case design with simpler stand-mounted box:
- Case clips to tripod leg/stand pole (20-35mm diameter)
- No camera sleeve needed — case sits on the stand
- Powered by standard USB power bank (off-the-shelf)
- Holds ESP8266 + ESP32 stacked with UART wiring
- Cable ports for USB in/out, LED window, ventilation
Simplified BOM: ~1/node (down from 4), no buck converters needed
Complete rewrite of firmware into two dedicated boards per camera node:
ESP8266 (Camera Bridge):
- Connects ONLY to GoPro AP — polls status, sends over UART
- Zero network switching, zero MQTT
- HTTP GET /bacpac/SH for status, start/stop
- JSON-per-line UART protocol to ESP32
ESP32 (MQTT Bridge):
- Connects ONLY to travel router — MQTT to Pi hub
- Reads status from ESP8266 over UART2 (RX16/TX17)
- Auto-registration, heartbeat, command forwarding
- Zero camera communication
UART Protocol: JSON-per-line at 115200 8N1
ESP8266→ESP32: status/ack/pong/error
ESP32→ESP8266: cmd (start_recording/stop_recording/ping)
Hardware updates:
- BOM now includes both boards (~4/node)
- 3D case has stacked dual-board compartment
- UART wire channel between board recesses
- Shared 3.3V power rail for both boards
- Unified firmware for ESP32 (dual-STA) and ESP8266 (time-shared STA)
- ESP8266: alternates between GoPro AP and travel router per poll cycle
- PlatformIO dual-target: esp32dev + esp8266dev (d1_mini)
- camera_ip config field for Akaso/non-GoPro cameras
- LittleFS support for ESP8266 (replaces SPIFFS)
- Camera compatibility table (GoPro H3/H4, Akaso)
- LED polarity handled per-platform (ESP8266 active-low)
ESP8266 time-sharing adds ~4s latency per 30s cycle — invisible at poll rate.
Conflicts in cmd/server/main.go, go.mod, internal/db/db.go
— main had older/simpler versions from PR #4 (README)
— dev has full codebase with MQTT, SSE, embed
- GoPro Hero 3 sleeve with lens/screen/USB cutouts
- ESP32 D1 Mini electronics compartment (vented)
- LiPo battery compartment with velcro strap slots
- Bill of materials (~0 per camera node)
- Wiring diagram (LiPo → dual buck converters → ESP32 + GoPro)
- Field deployment workflow
OpenSCAD model in hardware/case/remoterig-case.scad
Assembly guide in hardware/README.md