generated from CubeCraft-Creations/Tracehound
Merge pull request 'feat: add v3 hardware case and update hub network' (#6) from agent/hermes/remoterig-hardware-v3-network into dev
Reviewed-on: #6
This commit was merged in pull request #6.
This commit is contained in:
+22
-22
@@ -36,7 +36,7 @@ RemoteRig is a **multi-camera remote monitoring system**. It provides a camera g
|
|||||||
```
|
```
|
||||||
┌──────────────────────────────────────────┐
|
┌──────────────────────────────────────────┐
|
||||||
│ Travel Router (self-contained LAN) │
|
│ Travel Router (self-contained LAN) │
|
||||||
│ Subnet: 192.168.4.0/24 │
|
│ Subnet: 10.60.1.0/24 │
|
||||||
│ DHCP pool: .100-.200 │
|
│ DHCP pool: .100-.200 │
|
||||||
└──────┬──────────┬──────────┬──────────────┘
|
└──────┬──────────┬──────────┬──────────────┘
|
||||||
│ │ │
|
│ │ │
|
||||||
@@ -44,32 +44,32 @@ RemoteRig is a **multi-camera remote monitoring system**. It provides a camera g
|
|||||||
▼ ▼ ▼
|
▼ ▼ ▼
|
||||||
┌──────────────┐ ┌──────────────┐ ┌──────────────────┐
|
┌──────────────┐ ┌──────────────┐ ┌──────────────────┐
|
||||||
│ ESP32 #1 │ │ ESP32 #N │ │ Pi Zero 2 W │
|
│ ESP32 #1 │ │ ESP32 #N │ │ Pi Zero 2 W │
|
||||||
│ DHCP addr │ │ DHCP addr │ │ 192.168.4.10 │
|
│ DHCP addr │ │ DHCP addr │ │ 10.60.1.56 │
|
||||||
│ │ │ │ │ (static IP) │
|
│ STA→Router │ │ STA→Router │ │ (static IP) │
|
||||||
│ STA→GoPro AP │ │ STA→GoPro AP │ │ │
|
│ MQTT→:1883 │ │ MQTT→:1883 │ │ Mosquitto :1883 │
|
||||||
│ STA→Router │ │ STA→Router │ │ Mosquitto :1883 │
|
│ UART relay │ │ UART relay │ │ Go API :8080 │
|
||||||
│ │ │ │ │ Go API :8080 │
|
│ │ │ │ │ React UI │
|
||||||
│ MQTT→:1883 │ │ MQTT→:1883 │ │ React UI │
|
|
||||||
└──────┬───────┘ └──────┬───────┘ │ SQLite DB │
|
└──────┬───────┘ └──────┬───────┘ │ SQLite DB │
|
||||||
│ │ └──────────────────┘
|
│ UART │ UART └──────────────────┘
|
||||||
▼ ▼ │
|
▼ ▼ │
|
||||||
┌──────────────┐ ┌──────────────┐ │
|
┌──────────────┐ ┌──────────────┐ │
|
||||||
│ GoPro Hero 3 │ │ GoPro Hero 3 │ SSE /api/v1/events/stream
|
│ ESP8266 #1 │ │ ESP8266 #N │ SSE /api/v1/events/stream
|
||||||
│ AP: 10.5.5.1 │ │ AP: 10.5.5.1 │ │
|
│ STA→GoPro AP │ │ STA→GoPro AP │ │
|
||||||
│ Wi-Fi only │ │ Wi-Fi only │ ▼
|
│ HTTP→10.5.5.1│ │ HTTP→10.5.5.1│ ▼
|
||||||
└──────────────┘ └──────────────┘ ┌──────────────────┐
|
└──────┬───────┘ └──────┬───────┘ ┌──────────────────┐
|
||||||
│ User Device │
|
▼ ▼ │ User Device │
|
||||||
│ (laptop/kiosk) │
|
┌──────────────┐ ┌──────────────┐ │ (laptop/kiosk) │
|
||||||
│ http://.4.10 │
|
│ GoPro Hero 3 │ │ GoPro Hero 3 │ │ 10.60.1.56:8080 │
|
||||||
└──────────────────┘
|
└──────────────┘ └──────────────┘ └──────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
**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.
|
**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). ESP8266 boards talk to the GoPro AP over HTTP, then relay camera status/commands over UART to ESP32 boards. ESP32 boards stay on the travel-router LAN and bridge UART messages to MQTT.
|
||||||
|
|
||||||
### Key Architecture Decisions (revised)
|
### 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`.
|
- **Closed travel router network** — No venue Wi-Fi dependency. User brings their own router. All devices on `10.60.1.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.
|
- **Two-board camera node** — ESP8266 handles GoPro AP/HTTP; ESP32 stays on the travel-router LAN for MQTT. This avoids ESP32 dual-STA/channel switching complexity.
|
||||||
- **ESP32 → GoPro over Wi-Fi** — Bacpac I²C route rejected (30-pin Herobus connector too complex). HTTP to GoPro AP is proven and reliable.
|
- **ESP8266 → GoPro over Wi-Fi** — Bacpac I²C route rejected (30-pin Herobus connector too complex). HTTP to GoPro AP is proven and reliable.
|
||||||
|
- **UART bridge between boards** — ESP8266 reports GoPro status and receives commands over UART; ESP32 relays those messages to/from MQTT.
|
||||||
- **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](./docs/MQTT_CONTRACT.md)
|
- **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](./docs/MQTT_CONTRACT.md)
|
||||||
- **SQLite over PostgreSQL** — Single-node Pi Zero 2 W deployment. WAL mode for concurrent read/write.
|
- **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.
|
- **SSE over WebSocket** — Unidirectional hub → browser updates. Simpler, sufficient for status dashboard.
|
||||||
@@ -215,8 +215,8 @@ platform:
|
|||||||
type: "pi-zero-2w"
|
type: "pi-zero-2w"
|
||||||
max_cameras: 16
|
max_cameras: 16
|
||||||
network:
|
network:
|
||||||
subnet: "192.168.4.0/24" # Travel router subnet
|
subnet: "10.60.1.0/24" # Travel router subnet
|
||||||
hub_ip: "192.168.4.10" # Pi Zero 2 W static IP
|
hub_ip: "10.60.1.56" # Pi Zero 2 W static IP
|
||||||
```
|
```
|
||||||
|
|
||||||
## Frontend Component Tree
|
## Frontend Component Tree
|
||||||
|
|||||||
+42
-35
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
```
|
```
|
||||||
┌──────────────────────────────────┐
|
┌──────────────────────────────────┐
|
||||||
│ Travel Router (192.168.4.1) │
|
│ Travel Router (10.60.1.1) │
|
||||||
│ DHCP: .100-.200 │
|
│ DHCP: .100-.200 │
|
||||||
└──────┬──────────┬──────────┬──────┘
|
└──────┬──────────┬──────────┬──────┘
|
||||||
│ │ │
|
│ │ │
|
||||||
@@ -15,27 +15,31 @@
|
|||||||
▼ ▼ ▼
|
▼ ▼ ▼
|
||||||
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||||
│ ESP32 #1 │ │ ESP32 #2 │ │ Pi Zero 2 W │
|
│ ESP32 #1 │ │ ESP32 #2 │ │ Pi Zero 2 W │
|
||||||
│ 192.168.4.101│ │ 192.168.4.102│ │ 192.168.4.10 │
|
│ 10.60.1.101 │ │ 10.60.1.102 │ │ 10.60.1.56 │
|
||||||
│ │ │ │ │ │
|
│ STA→Router │ │ STA→Router │ │ Mosquitto │
|
||||||
│ STA→GoPro AP │ │ STA→GoPro AP │ │ Mosquitto │
|
│ MQTT relay │ │ MQTT relay │ │ Go backend │
|
||||||
│ STA→Router │ │ STA→Router │ │ Go backend │
|
└──────┬───────┘ └──────┬───────┘ │ React UI │
|
||||||
└──────┬───────┘ └──────┬───────┘ │ React UI │
|
│ UART │ UART └──────────────┘
|
||||||
│ │ └──────────────┘
|
▼ ▼
|
||||||
▼ ▼
|
┌──────────────┐ ┌──────────────┐
|
||||||
┌──────────────┐ ┌──────────────┐
|
│ ESP8266 #1 │ │ ESP8266 #2 │
|
||||||
│ GoPro Hero 3 │ │ GoPro Hero 3 │
|
│ STA→GoPro AP │ │ STA→GoPro AP │
|
||||||
│ AP: 10.5.5.1 │ │ AP: 10.5.5.1 │
|
│ HTTP→10.5.5.1│ │ HTTP→10.5.5.1│
|
||||||
└──────────────┘ └──────────────┘
|
└──────┬───────┘ └──────┬───────┘
|
||||||
|
▼ ▼
|
||||||
|
┌──────────────┐ ┌──────────────┐
|
||||||
|
│ GoPro Hero 3 │ │ GoPro Hero 3 │
|
||||||
|
└──────────────┘ └──────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Travel router:** Self-contained, no internet. DHCP pool: `192.168.4.100-200`
|
- **Travel router:** Self-contained, no internet. Gateway `10.60.1.1`. DHCP pool: `10.60.1.100-200`
|
||||||
- **Pi Zero 2 W:** Static IP `192.168.4.10`. Runs Mosquitto (port 1883), Go backend (port 8080), serves React UI
|
- **Pi Zero 2 W:** Static IP `10.60.1.56`. Runs Mosquitto (port 1883), Go backend (port 8080), serves React UI
|
||||||
- **ESP32s:** DHCP from router. Each has dual STA: one to GoPro AP, one to router
|
- **ESP32s:** DHCP from router. Each stays on the travel-router LAN, relays MQTT to/from its paired ESP8266 over UART
|
||||||
- **User device:** Connects to router, opens `http://192.168.4.10:8080` for dashboard
|
- **User device:** Connects to router, opens `http://10.60.1.56:8080` for dashboard
|
||||||
|
|
||||||
## MQTT Broker
|
## MQTT Broker
|
||||||
|
|
||||||
- **Host:** `192.168.4.10` (Pi Zero 2 W)
|
- **Host:** `10.60.1.56` (Pi Zero 2 W)
|
||||||
- **Port:** `1883` (default MQTT, no TLS — closed network)
|
- **Port:** `1883` (default MQTT, no TLS — closed network)
|
||||||
- **Auth:** None (closed network, no external access)
|
- **Auth:** None (closed network, no external access)
|
||||||
- **Client ID format:** `remoterig-<esp32_mac_last6>` (e.g., `remoterig-a1b2c3`)
|
- **Client ID format:** `remoterig-<esp32_mac_last6>` (e.g., `remoterig-a1b2c3`)
|
||||||
@@ -61,7 +65,7 @@ remoterig/
|
|||||||
**Direction:** ESP32 → Hub
|
**Direction:** ESP32 → Hub
|
||||||
**QoS:** 1 | **Retain:** true | **Interval:** 30 seconds
|
**QoS:** 1 | **Retain:** true | **Interval:** 30 seconds
|
||||||
|
|
||||||
Published by the ESP32 every 30s with the latest GoPro status.
|
Published by the ESP32 every 30s using the latest GoPro status received from the paired ESP8266 over UART.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -143,8 +147,9 @@ Commands sent from the dashboard to individual cameras.
|
|||||||
| `stop_recording` | Stop GoPro recording | status (updated on next poll) |
|
| `stop_recording` | Stop GoPro recording | status (updated on next poll) |
|
||||||
| `reboot` | Reboot the ESP32 | — (ESP32 reconnects after boot) |
|
| `reboot` | Reboot the ESP32 | — (ESP32 reconnects after boot) |
|
||||||
|
|
||||||
**ESP32 behavior:**
|
**ESP32 / ESP8266 behavior:**
|
||||||
- On receipt, execute command against GoPro
|
- ESP32 receives the MQTT command and forwards it over UART to the paired ESP8266
|
||||||
|
- ESP8266 executes the corresponding HTTP command against the GoPro AP
|
||||||
- Next status publish will reflect the new state
|
- Next status publish will reflect the new state
|
||||||
- If command fails (GoPro unreachable), publish status with `online: false`
|
- If command fails (GoPro unreachable), publish status with `online: false`
|
||||||
|
|
||||||
@@ -217,20 +222,21 @@ Hub health status broadcast.
|
|||||||
ESP32 boots
|
ESP32 boots
|
||||||
│
|
│
|
||||||
├── Connects to travel router Wi-Fi
|
├── Connects to travel router Wi-Fi
|
||||||
├── Connects to MQTT broker (192.168.4.10:1883)
|
├── Connects to MQTT broker (10.60.1.56:1883)
|
||||||
├── Publishes announce (retained) on cameras/<id>/announce
|
├── Publishes announce (retained) on cameras/<id>/announce
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
┌─────────────────────────────────────────┐
|
┌───────────────────────────────────────────────┐
|
||||||
│ Main loop (every 30s): │
|
│ Main loop (every 30s): │
|
||||||
│ 1. HTTP GET GoPro status (10.5.5.1) │
|
│ 1. ESP32 requests/receives status via UART │
|
||||||
│ 2. Parse 60-byte status blob │
|
│ 2. ESP8266 polls GoPro HTTP (10.5.5.1) │
|
||||||
│ 3. Publish status (retained) │
|
│ 3. ESP8266 returns parsed status over UART │
|
||||||
│ 4. Every 60s: publish heartbeat │
|
│ 4. ESP32 publishes MQTT status (retained) │
|
||||||
└─────────────────────────────────────────┘
|
│ 5. Every 60s: ESP32 publishes heartbeat │
|
||||||
|
└───────────────────────────────────────────────┘
|
||||||
│
|
│
|
||||||
├── On MQTT disconnect → reconnect with 1s/2s/4s/8s/16s/30s backoff
|
├── On MQTT disconnect → reconnect with 1s/2s/4s/8s/16s/30s backoff
|
||||||
├── On GoPro unreachable → publish status with online: false
|
├── On ESP8266/GoPro unreachable → publish status with online: false
|
||||||
├── On Wi-Fi loss → buffer status locally, replay on reconnect (CUB-230)
|
├── On Wi-Fi loss → buffer status locally, replay on reconnect (CUB-230)
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
@@ -243,11 +249,12 @@ ESP32 shutdown / watchdog reboot
|
|||||||
1. User clicks "Start" on dashboard
|
1. User clicks "Start" on dashboard
|
||||||
2. Browser → HTTP POST /api/v1/cameras/cam-001/start → Go backend
|
2. Browser → HTTP POST /api/v1/cameras/cam-001/start → Go backend
|
||||||
3. Go backend → MQTT publish remoterig/cameras/cam-001/command {command: "start_recording"}
|
3. Go backend → MQTT publish remoterig/cameras/cam-001/command {command: "start_recording"}
|
||||||
4. ESP32 receives command, sends HTTP GET to 10.5.5.1/bacpac/SH?t=<password>&p=%01
|
4. ESP32 receives command and forwards it to ESP8266 over UART
|
||||||
5. GoPro starts recording
|
5. ESP8266 sends HTTP GET to 10.5.5.1/bacpac/SH?t=<password>&p=%01
|
||||||
6. Next 30s poll: ESP32 publishes status with recording: true
|
6. GoPro starts recording
|
||||||
7. Go backend receives status, updates SQLite, fans out via SSE
|
7. Next 30s poll: ESP8266 reports status over UART; ESP32 publishes status with recording: true
|
||||||
8. Dashboard updates with pulsing REC indicator
|
8. Go backend receives status, updates SQLite, fans out via SSE
|
||||||
|
9. Dashboard updates with pulsing REC indicator
|
||||||
```
|
```
|
||||||
|
|
||||||
## Offline Buffering (future — CUB-230)
|
## Offline Buffering (future — CUB-230)
|
||||||
@@ -268,6 +275,6 @@ When ESP32 loses connection to travel router:
|
|||||||
|
|
||||||
## Open Questions
|
## Open Questions
|
||||||
|
|
||||||
1. **NTP/time sync:** How do ESP32s get accurate time without internet? Options: (a) Pi runs NTP server, (b) ESP32 queries Pi's HTTP /api/v1/time endpoint, (c) GPS module. **Recommendation:** Pi runs NTPd, ESP32s use SNTP from `192.168.4.10`.
|
1. **NTP/time sync:** How do ESP32s get accurate time without internet? Options: (a) Pi runs NTP server, (b) ESP32 queries Pi's HTTP /api/v1/time endpoint, (c) GPS module. **Recommendation:** Pi runs NTPd, ESP32s use SNTP from `10.60.1.56`.
|
||||||
2. **Camera naming:** Should `friendly_name` be configurable from dashboard after auto-registration? **Recommendation:** Yes — allow rename via UI, stored in cameras table.
|
2. **Camera naming:** Should `friendly_name` be configurable from dashboard after auto-registration? **Recommendation:** Yes — allow rename via UI, stored in cameras table.
|
||||||
3. **Firmware OTA:** Should ESP32 firmware updates be possible over this network? **Recommendation:** Yes but out of scope for MVP.
|
3. **Firmware OTA:** Should ESP32 firmware updates be possible over this network? **Recommendation:** Yes but out of scope for MVP.
|
||||||
|
|||||||
+3
-3
@@ -14,7 +14,7 @@ Each camera node uses **two boards** connected via UART — zero network switchi
|
|||||||
│ (Camera Bridge) │ RX←──────TX │ (MQTT Bridge) │
|
│ (Camera Bridge) │ RX←──────TX │ (MQTT Bridge) │
|
||||||
│ │ 115200 │ │
|
│ │ 115200 │ │
|
||||||
│ STA → GoPro AP │ 8N1 │ STA → Travel Router │
|
│ STA → GoPro AP │ 8N1 │ STA → Travel Router │
|
||||||
│ HTTP → 10.5.5.1 │ │ MQTT → 192.168.4.10│
|
│ HTTP → 10.5.5.1 │ │ MQTT → 10.60.1.56│
|
||||||
│ Start/stop/status │ │ Hub registration │
|
│ Start/stop/status │ │ Hub registration │
|
||||||
└─────────────────────┘ └──────────────────────┘
|
└─────────────────────┘ └──────────────────────┘
|
||||||
```
|
```
|
||||||
@@ -22,7 +22,7 @@ Each camera node uses **two boards** connected via UART — zero network switchi
|
|||||||
| Board | Job | Network | Protocol |
|
| Board | Job | Network | Protocol |
|
||||||
|-------|-----|---------|----------|
|
|-------|-----|---------|----------|
|
||||||
| ESP8266 | Camera control | GoPro AP only (10.5.5.1) | HTTP → UART JSON |
|
| ESP8266 | Camera control | GoPro AP only (10.5.5.1) | HTTP → UART JSON |
|
||||||
| ESP32 | Hub relay | Travel router only (192.168.4.x) | UART JSON → MQTT |
|
| ESP32 | Hub relay | Travel router only (10.60.1.x) | UART JSON → MQTT |
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ JSON-per-line at 115200 8N1. GPIO16 on both boards.
|
|||||||
|-----|---------|-------------|
|
|-----|---------|-------------|
|
||||||
| `wifi_ssid` | `"RemoteRig"` | Travel router SSID |
|
| `wifi_ssid` | `"RemoteRig"` | Travel router SSID |
|
||||||
| `wifi_password` | `""` | Travel router password |
|
| `wifi_password` | `""` | Travel router password |
|
||||||
| `mqtt_broker` | `"192.168.4.10"` | Pi Zero 2 W IP |
|
| `mqtt_broker` | `"10.60.1.56"` | Pi Zero 2 W IP |
|
||||||
| `mqtt_port` | `1883` | Mosquitto port |
|
| `mqtt_port` | `1883` | Mosquitto port |
|
||||||
| `camera_id` | `""` | Assigned by hub on first announce (leave empty) |
|
| `camera_id` | `""` | Assigned by hub on first announce (leave empty) |
|
||||||
| `heartbeat_interval_sec` | `60` | MQTT heartbeat frequency |
|
| `heartbeat_interval_sec` | `60` | MQTT heartbeat frequency |
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"wifi_ssid": "RemoteRig",
|
"wifi_ssid": "RemoteRig",
|
||||||
"wifi_password": "",
|
"wifi_password": "",
|
||||||
"mqtt_broker": "192.168.4.10",
|
"mqtt_broker": "10.60.1.56",
|
||||||
"mqtt_port": 1883,
|
"mqtt_port": 1883,
|
||||||
"camera_id": "",
|
"camera_id": "",
|
||||||
"heartbeat_interval_sec": 60
|
"heartbeat_interval_sec": 60
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
struct Config {
|
struct Config {
|
||||||
String wifi_ssid = "RemoteRig";
|
String wifi_ssid = "RemoteRig";
|
||||||
String wifi_password = "";
|
String wifi_password = "";
|
||||||
String mqtt_broker = "192.168.4.10";
|
String mqtt_broker = "10.60.1.56";
|
||||||
int mqtt_port = 1883;
|
int mqtt_port = 1883;
|
||||||
String camera_id = ""; // assigned by hub
|
String camera_id = ""; // assigned by hub
|
||||||
int heartbeat_sec = 60;
|
int heartbeat_sec = 60;
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# RemoteRig Hardware Design Pipeline
|
||||||
|
|
||||||
|
> Living queue for 3D-printed / physical hardware design work.
|
||||||
|
|
||||||
|
## Active / Ready for prototype print
|
||||||
|
|
||||||
|
### Tripod electronics case v3
|
||||||
|
|
||||||
|
**Status:** STL generated and validated watertight.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- `hardware/case/tripod-case-v3.scad`
|
||||||
|
- `hardware/case/case-body-v3.stl`
|
||||||
|
- `hardware/case/case-lid-v3.stl`
|
||||||
|
- `hardware/case/tripod-clamp-v3.stl`
|
||||||
|
- `hardware/case/full-case-preview-v3.stl`
|
||||||
|
|
||||||
|
**Design notes:**
|
||||||
|
- Holds ESP32 + ESP8266 stack.
|
||||||
|
- Screw-on lid with vent slots.
|
||||||
|
- Rear dovetail-style rail/socket interface.
|
||||||
|
- Separate screw-tightened tripod clamp sized around a 35 mm stand/pole.
|
||||||
|
- Clamp uses M3 hardware: one M3 screw across the clamp mouth, with an M3 nut trap.
|
||||||
|
|
||||||
|
**Prototype questions:**
|
||||||
|
- Does the clamp close enough on smaller tripod legs, or do we need swappable inserts?
|
||||||
|
- Does the dovetail hold under vibration without a retention screw?
|
||||||
|
- Are USB/LED/UART cutouts in the correct orientation for the actual boards?
|
||||||
|
|
||||||
|
## Backlog
|
||||||
|
|
||||||
|
### 10.1-inch touchscreen + Raspberry Pi Zero case
|
||||||
|
|
||||||
|
**Status:** Specific display identified; mechanical measurements needed before CAD.
|
||||||
|
|
||||||
|
**Goal:** A printable enclosure for the RemoteRig hub/control panel using a 10.1-inch touchscreen and Raspberry Pi Zero / Zero 2 W.
|
||||||
|
|
||||||
|
**Display target:**
|
||||||
|
- Vendor/model: HZWDONE Raspberry Pi Screen 10.1" Touchscreen
|
||||||
|
- Resolution: 1024×600
|
||||||
|
- Interface: HDMI portable monitor
|
||||||
|
- Mounting: includes fixing holes
|
||||||
|
- Compatibility listing: Raspberry Pi 5/4/3B/B+ and Windows 11/10/8
|
||||||
|
|
||||||
|
**Initial assumptions to validate:**
|
||||||
|
- Compute: Raspberry Pi Zero / Zero 2 W mounted behind or below the display.
|
||||||
|
- Use case: RemoteRig local monitor/control panel at field recording setup.
|
||||||
|
- Likely needs: front bezel, rear electronics cavity, Pi mounting posts, HDMI/USB/power cable exits, strain relief, ventilation, and optional tripod/stand mounting.
|
||||||
|
- Because this is a 10.1" panel, design should prioritize rigidity: thicker bezel ribs, rear standoffs, and possibly a two-piece shell instead of a small snap case.
|
||||||
|
|
||||||
|
**Required measurements before CAD:**
|
||||||
|
- Product link or datasheet for the exact HZWDONE 10.1" variant.
|
||||||
|
- Screen/PCB outer dimensions: width, height, thickness.
|
||||||
|
- Active display opening dimensions.
|
||||||
|
- Fixing-hole locations, hole diameter, and screw size.
|
||||||
|
- Connector locations/orientation for HDMI, USB touch, and power.
|
||||||
|
- Whether the driver/controller board is integrated with the display PCB or separate.
|
||||||
|
- Pi Zero orientation, port access requirements, and whether GPIO/header must remain accessible.
|
||||||
|
- Power connector position and desired cable routing.
|
||||||
|
- Mounting preference: desktop kickstand, tripod clamp, VESA-style holes, handle, or combination.
|
||||||
|
|
||||||
|
**Proposed design approach:**
|
||||||
|
1. Create `hardware/display-case/`.
|
||||||
|
2. Build a parametric OpenSCAD model with measured display/Pi dimensions.
|
||||||
|
3. Split into printable parts: front bezel, rear shell, Pi/controller tray, optional stand/tripod mount.
|
||||||
|
4. Validate STLs with OpenSCAD + trimesh.
|
||||||
|
5. Upload generated STL/SCAD artifacts to Seafile.
|
||||||
+18
-15
@@ -41,18 +41,20 @@ Each camera node is two ESP boards in a small case that clips to the tripod/stan
|
|||||||
|
|
||||||
## 3D Printed Case
|
## 3D Printed Case
|
||||||
|
|
||||||
**File:** `hardware/case/tripod-case.scad`
|
**Current source:** `hardware/case/tripod-case-v3.scad`
|
||||||
|
**Pipeline:** `hardware/DESIGN_PIPELINE.md`
|
||||||
|
|
||||||
Three parts:
|
Four exported prototype files:
|
||||||
1. **Case body** — holds both boards stacked, cable ports, rail for clip
|
1. **Case body** — holds both boards stacked, cable ports, rear dovetail-style receiver
|
||||||
2. **Case lid** — screw-on cover with ventilation
|
2. **Case lid** — screw-on cover with ventilation
|
||||||
3. **Tripod clip** — C-clamp for 20-35mm poles, slides into case rail
|
3. **Tripod clamp** — separate screw-tightened C-clamp sized around a 35mm stand/pole
|
||||||
|
4. **Full preview** — combined visualization STL only, not intended as the print job
|
||||||
|
|
||||||
### Print Settings
|
### Print Settings
|
||||||
- **Material:** PETG (outdoor/heat) or PLA+
|
- **Material:** PETG preferred for heat/outdoor use and clamp flex
|
||||||
- **Layer:** 0.2mm | **Infill:** 20% gyroid
|
- **Layer:** 0.2mm | **Infill:** 20% gyroid minimum; 35%+ recommended for clamp
|
||||||
- **Supports:** Yes (for clip overhang)
|
- **Supports:** Likely yes for clamp ears / dovetail overhangs depending on slicer orientation
|
||||||
- **Post-processing:** M3x8mm screws for lid (4x)
|
- **Post-processing:** M3x8mm screws for lid (4x), one M3 screw + M3 nut for clamp tightening
|
||||||
|
|
||||||
## Wiring
|
## Wiring
|
||||||
|
|
||||||
@@ -82,11 +84,11 @@ GoPro Hero 3 ──(AP @ 10.5.5.1)──→ ESP8266 (camera bridge)
|
|||||||
UART │ (inside case)
|
UART │ (inside case)
|
||||||
│
|
│
|
||||||
Travel Router ──(AP)─────────────────→ ESP32 (MQTT bridge)
|
Travel Router ──(AP)─────────────────→ ESP32 (MQTT bridge)
|
||||||
(192.168.4.1) │
|
(10.60.1.1) │
|
||||||
│
|
│
|
||||||
MQTT │
|
MQTT │
|
||||||
▼
|
▼
|
||||||
Pi Hub (192.168.4.10)
|
Pi Hub (10.60.1.56)
|
||||||
```
|
```
|
||||||
|
|
||||||
The ESP8266 and GoPro talk over Wi-Fi — **no data cable between them**. The only cable to the GoPro is USB power from the battery pack.
|
The ESP8266 and GoPro talk over Wi-Fi — **no data cable between them**. The only cable to the GoPro is USB power from the battery pack.
|
||||||
@@ -97,13 +99,14 @@ The ESP8266 and GoPro talk over Wi-Fi — **no data cable between them**. The on
|
|||||||
2. **Clip case** to tripod leg
|
2. **Clip case** to tripod leg
|
||||||
3. **Connect power bank** via USB to case + GoPro
|
3. **Connect power bank** via USB to case + GoPro
|
||||||
4. **Power on** — ESP32 auto-connects to travel router, ESP8266 auto-connects to GoPro
|
4. **Power on** — ESP32 auto-connects to travel router, ESP8266 auto-connects to GoPro
|
||||||
5. **Monitor** from `http://192.168.4.10:8080`
|
5. **Monitor** from `http://10.60.1.56:8080`
|
||||||
|
|
||||||
## Case Dimensions
|
## Case Dimensions
|
||||||
|
|
||||||
| | W × D × H (mm) |
|
| | W × D × H (mm) |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Case external | ~64 × 38 × 27 |
|
| Case body external | ~56.8 × 38.2 × 19.0 |
|
||||||
| Case internal | ~57 × 31 × 18 |
|
| Lid external | ~56.8 × 32.8 × 4.0 |
|
||||||
| Fits poles | 20-35mm diameter |
|
| Tripod clamp | ~43.0 × 56.9 × 16.0 |
|
||||||
| Total weight | ~50g (case + boards, without power bank) |
|
| Clamp pole fit | Nominal 35mm; smaller poles TBD / may need inserts |
|
||||||
|
| Total weight | TBD after prototype print |
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,2 @@
|
|||||||
|
include <tripod-case-v3.scad>;
|
||||||
|
render(convexity=10) case_body();
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
include <tripod-case-v3.scad>;
|
||||||
|
render(convexity=10) case_lid();
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
include <tripod-case-v3.scad>;
|
||||||
|
render(convexity=10) full_case();
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
include <tripod-case-v3.scad>;
|
||||||
|
render(convexity=10) tripod_clamp();
|
||||||
Binary file not shown.
@@ -0,0 +1,217 @@
|
|||||||
|
// RemoteRig — Dual-ESP Tripod Case v3
|
||||||
|
// v3 changes: screw-tightened tripod clamp + dovetail slide interface.
|
||||||
|
// Coordinate system: all case/lid geometry uses bottom-origin Z.
|
||||||
|
|
||||||
|
$fn = 36;
|
||||||
|
|
||||||
|
// Board dimensions
|
||||||
|
esp8266_w = 34.2; esp8266_d = 25.6; esp8266_h = 5;
|
||||||
|
esp32_w = 52; esp32_d = 28; esp32_h = 5;
|
||||||
|
board_gap = 3;
|
||||||
|
stack_h = esp8266_h + esp32_h + board_gap;
|
||||||
|
inner_w = max(esp8266_w, esp32_w);
|
||||||
|
inner_d = max(esp8266_d, esp32_d);
|
||||||
|
inner_h = stack_h + 2;
|
||||||
|
|
||||||
|
// Case parameters
|
||||||
|
wall = 2.0;
|
||||||
|
tol = 0.4;
|
||||||
|
outer_w = inner_w + wall*2 + tol*2; // 56.8mm
|
||||||
|
outer_d = inner_d + wall*2 + tol*2; // 32.8mm
|
||||||
|
outer_h = inner_h + wall*2; // 19mm
|
||||||
|
corner_r = 2.5;
|
||||||
|
|
||||||
|
// Tripod clamp parameters
|
||||||
|
pole_dia = 35; // nominal stand/pole diameter
|
||||||
|
clamp_thick = 4.0; // ring wall thickness
|
||||||
|
clamp_width = 16.0; // extrusion width along Z
|
||||||
|
mouth_width = 13.0; // clamp opening
|
||||||
|
m3_clearance = 3.4; // M3 screw clearance
|
||||||
|
nut_flat = 6.4; // M3 nut trap flat-to-flat
|
||||||
|
|
||||||
|
// Dovetail slide interface
|
||||||
|
// Male rail is on the case; matching female socket is on the tripod clamp.
|
||||||
|
// This is easier to inspect and avoids the previous mismatched "two lips + tab" geometry.
|
||||||
|
rail_z = outer_h * 0.78;
|
||||||
|
rail_depth = 5.0;
|
||||||
|
rail_neck_w = 12.0; // narrow width at case wall / slot opening
|
||||||
|
rail_outer_w = 18.0; // wider retained edge
|
||||||
|
rail_clearance = 0.45; // FDM sliding clearance per side-ish
|
||||||
|
socket_wall = 2.2;
|
||||||
|
|
||||||
|
// Cable ports
|
||||||
|
usb_port_w = 12; usb_port_h = 6;
|
||||||
|
uart_port_w = 6; uart_port_h = 4;
|
||||||
|
|
||||||
|
// Uncomment one for manual OpenSCAD use
|
||||||
|
// full_case();
|
||||||
|
// case_body();
|
||||||
|
// case_lid();
|
||||||
|
// tripod_clamp();
|
||||||
|
|
||||||
|
module rounded_cube_centered(w, d, h, r) {
|
||||||
|
hull() {
|
||||||
|
for (x = [-1, 1], y = [-1, 1], z = [-1, 1]) {
|
||||||
|
translate([x*(w/2 - r), y*(d/2 - r), z*(h/2 - r)])
|
||||||
|
sphere(r=r, $fn=24);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module rounded_cube0(w, d, h, r) {
|
||||||
|
translate([0, 0, h/2]) rounded_cube_centered(w, d, h, r);
|
||||||
|
}
|
||||||
|
|
||||||
|
module hex_prism(d, h) {
|
||||||
|
cylinder(d=d, h=h, center=true, $fn=6);
|
||||||
|
}
|
||||||
|
|
||||||
|
module dovetail_prism(length_z, front_w, back_w, depth) {
|
||||||
|
// 2D profile is X/Y, extruded along Z.
|
||||||
|
rotate([0, 0, 0])
|
||||||
|
linear_extrude(height=length_z, center=true, convexity=10)
|
||||||
|
polygon(points=[
|
||||||
|
[-front_w/2, 0], [front_w/2, 0],
|
||||||
|
[back_w/2, depth], [-back_w/2, depth]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
module case_shell() {
|
||||||
|
difference() {
|
||||||
|
rounded_cube0(outer_w, outer_d, outer_h, corner_r);
|
||||||
|
|
||||||
|
// Open internal cavity: starts above bottom wall, extends past top.
|
||||||
|
translate([0, 0, wall])
|
||||||
|
rounded_cube0(inner_w + tol, inner_d + tol, outer_h + 2, 1.6);
|
||||||
|
|
||||||
|
// USB power IN / OUT ports through front/back walls.
|
||||||
|
translate([0, outer_d/2 + 0.1, wall + 4])
|
||||||
|
cube([usb_port_w, wall*3, usb_port_h], center=true);
|
||||||
|
translate([0, -outer_d/2 - 0.1, wall + 4])
|
||||||
|
cube([usb_port_w, wall*3, usb_port_h], center=true);
|
||||||
|
|
||||||
|
// UART side channel.
|
||||||
|
translate([outer_w/2 + 0.1, 0, wall + 6])
|
||||||
|
cube([wall*3, uart_port_w, uart_port_h], center=true);
|
||||||
|
|
||||||
|
// LED viewing window on front lower wall.
|
||||||
|
translate([-outer_w/4, -outer_d/2 - 0.1, wall + 2])
|
||||||
|
cube([6, wall*2, 3], center=true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module screw_post(x, y) {
|
||||||
|
difference() {
|
||||||
|
translate([x, y, wall]) cylinder(d=5.0, h=outer_h-wall-0.5, center=false, $fn=24);
|
||||||
|
translate([x, y, wall-0.5]) cylinder(d=2.1, h=outer_h+1, center=false, $fn=20);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module case_male_dovetail_rail() {
|
||||||
|
// Positive tapered rail on the case back. Cross-section is narrow at the
|
||||||
|
// wall and wider at the outside, so the clamp socket captures it.
|
||||||
|
translate([0, outer_d/2 - 0.15, outer_h/2])
|
||||||
|
dovetail_prism(rail_z, rail_neck_w, rail_outer_w, rail_depth);
|
||||||
|
|
||||||
|
// Bottom stop so the clamp socket cannot slide past the case.
|
||||||
|
translate([0, outer_d/2 + rail_depth/2, outer_h*0.12])
|
||||||
|
rounded_cube_centered(rail_outer_w + 3.0, rail_depth + 0.8, 2.4, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
module case_body() {
|
||||||
|
union() {
|
||||||
|
case_shell();
|
||||||
|
for (x = [-1, 1], y = [-1, 1])
|
||||||
|
screw_post(x*(outer_w/2 - 5), y*(outer_d/2 - 5));
|
||||||
|
case_male_dovetail_rail();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module case_lid() {
|
||||||
|
difference() {
|
||||||
|
rounded_cube0(outer_w, outer_d, wall*2, 1.8);
|
||||||
|
|
||||||
|
for (x = [-1, 1], y = [-1, 1]) {
|
||||||
|
translate([x*(outer_w/2 - 5), y*(outer_d/2 - 5), -0.5])
|
||||||
|
cylinder(d=2.4, h=wall*2 + 1, center=false, $fn=20);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (x = [-outer_w/4, 0, outer_w/4]) {
|
||||||
|
translate([x, 0, wall*2/2])
|
||||||
|
cube([8, outer_d*0.6, wall*3], center=true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module clamp_ring_with_mouth() {
|
||||||
|
outer_r = pole_dia/2 + clamp_thick;
|
||||||
|
difference() {
|
||||||
|
cylinder(r=outer_r, h=clamp_width, center=true, $fn=72);
|
||||||
|
cylinder(r=pole_dia/2 + rail_clearance, h=clamp_width + 1, center=true, $fn=72);
|
||||||
|
// Mouth opens toward +Y. Width is intentionally generous for snap-on placement before tightening.
|
||||||
|
translate([0, outer_r, 0])
|
||||||
|
cube([mouth_width, outer_r*2, clamp_width + 2], center=true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module clamp_ears() {
|
||||||
|
outer_r = pole_dia/2 + clamp_thick;
|
||||||
|
ear_y = outer_r + 2.2;
|
||||||
|
ear_z = 0;
|
||||||
|
difference() {
|
||||||
|
union() {
|
||||||
|
translate([-mouth_width/2 - 3.2, ear_y, ear_z])
|
||||||
|
rounded_cube_centered(7.0, 9.0, clamp_width, 1.4);
|
||||||
|
translate([ mouth_width/2 + 3.2, ear_y, ear_z])
|
||||||
|
rounded_cube_centered(7.0, 9.0, clamp_width, 1.4);
|
||||||
|
}
|
||||||
|
// M3 screw passes across the mouth along X.
|
||||||
|
translate([0, ear_y, ear_z])
|
||||||
|
rotate([0, 90, 0]) cylinder(d=m3_clearance, h=mouth_width + 24, center=true, $fn=24);
|
||||||
|
// Nut trap on the right ear.
|
||||||
|
translate([mouth_width/2 + 3.2, ear_y, ear_z])
|
||||||
|
rotate([0, 90, 0]) hex_prism(nut_flat, 4.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module clamp_dovetail_socket() {
|
||||||
|
outer_r = pole_dia/2 + clamp_thick;
|
||||||
|
socket_outer_w = rail_outer_w + socket_wall*2;
|
||||||
|
socket_depth = rail_depth + socket_wall*2;
|
||||||
|
|
||||||
|
// Solid boss on the rear of the clamp, opposite the tightening mouth.
|
||||||
|
// A matching dovetail void is cut through it along Z so the case rail
|
||||||
|
// slides in from the top/bottom with practical FDM clearance.
|
||||||
|
difference() {
|
||||||
|
translate([0, -outer_r - socket_depth/2 + socket_wall, 0])
|
||||||
|
rounded_cube_centered(socket_outer_w, socket_depth, clamp_width, 1.2);
|
||||||
|
|
||||||
|
translate([0, -outer_r - 0.15, 0])
|
||||||
|
dovetail_prism(
|
||||||
|
clamp_width + 1.0,
|
||||||
|
rail_neck_w + rail_clearance,
|
||||||
|
rail_outer_w + rail_clearance,
|
||||||
|
rail_depth + 0.6
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module tripod_clamp() {
|
||||||
|
union() {
|
||||||
|
clamp_ring_with_mouth();
|
||||||
|
clamp_ears();
|
||||||
|
clamp_dovetail_socket();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backward-compatible alias for earlier export scripts.
|
||||||
|
module tripod_clip() {
|
||||||
|
tripod_clamp();
|
||||||
|
}
|
||||||
|
|
||||||
|
module full_case() {
|
||||||
|
case_body();
|
||||||
|
translate([0, 0, outer_h + 2]) case_lid();
|
||||||
|
translate([0, outer_d/2 + pole_dia/2 + clamp_thick + 8, outer_h/2])
|
||||||
|
rotate([90, 0, 0]) tripod_clamp();
|
||||||
|
}
|
||||||
Binary file not shown.
+7
-7
@@ -9,8 +9,8 @@
|
|||||||
# Options:
|
# Options:
|
||||||
# --config PATH Path to config.yaml template to copy to /opt/remoterig/
|
# --config PATH Path to config.yaml template to copy to /opt/remoterig/
|
||||||
# --service-user USER Systemd service user (default: pi)
|
# --service-user USER Systemd service user (default: pi)
|
||||||
# --static-ip IP Static IP for wlan0 (default: 192.168.4.10/24)
|
# --static-ip IP Static IP for wlan0 (default: 10.60.1.56/24)
|
||||||
# --gateway IP Gateway for wlan0 (default: 192.168.4.1)
|
# --gateway IP Gateway for wlan0 (default: 10.60.1.1)
|
||||||
# --help Show this help
|
# --help Show this help
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
@@ -20,8 +20,8 @@ set -euo pipefail
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
CONFIG_TEMPLATE=""
|
CONFIG_TEMPLATE=""
|
||||||
SERVICE_USER="pi"
|
SERVICE_USER="pi"
|
||||||
STATIC_IP="192.168.4.10/24"
|
STATIC_IP="10.60.1.56/24"
|
||||||
GATEWAY="192.168.4.1"
|
GATEWAY="10.60.1.1"
|
||||||
MOSQUITTO_PKG="mosquitto mosquitto-clients"
|
MOSQUITTO_PKG="mosquitto mosquitto-clients"
|
||||||
DEPLOY_DIR="/opt/remoterig"
|
DEPLOY_DIR="/opt/remoterig"
|
||||||
SERVICE_NAME="remoterig"
|
SERVICE_NAME="remoterig"
|
||||||
@@ -324,13 +324,13 @@ echo " Next steps:"
|
|||||||
echo " 1. Build the remoterig binary for ARM64:"
|
echo " 1. Build the remoterig binary for ARM64:"
|
||||||
echo " GOOS=linux GOARCH=arm64 go build -o remoterig ./cmd/server"
|
echo " GOOS=linux GOARCH=arm64 go build -o remoterig ./cmd/server"
|
||||||
echo " 2. Copy binary to Pi:"
|
echo " 2. Copy binary to Pi:"
|
||||||
echo " scp remoterig pi@192.168.4.10:/opt/remoterig/"
|
echo " scp remoterig pi@10.60.1.56:/opt/remoterig/"
|
||||||
echo " 3. Copy config if needed:"
|
echo " 3. Copy config if needed:"
|
||||||
echo " scp config.yaml pi@192.168.4.10:/opt/remoterig/"
|
echo " scp config.yaml pi@10.60.1.56:/opt/remoterig/"
|
||||||
echo " 4. Start the service:"
|
echo " 4. Start the service:"
|
||||||
echo " sudo systemctl start remoterig"
|
echo " sudo systemctl start remoterig"
|
||||||
echo " 5. Check health:"
|
echo " 5. Check health:"
|
||||||
echo " curl http://192.168.4.10:8080/health"
|
echo " curl http://10.60.1.56:8080/health"
|
||||||
echo ""
|
echo ""
|
||||||
echo " To deploy updates, use: scripts/deploy.sh"
|
echo " To deploy updates, use: scripts/deploy.sh"
|
||||||
echo "=============================================="
|
echo "=============================================="
|
||||||
|
|||||||
Reference in New Issue
Block a user