generated from CubeCraft-Creations/Tracehound
Dev #26
@@ -0,0 +1,100 @@
|
|||||||
|
# ESP-01S firmware updates without a USB-UART adapter
|
||||||
|
|
||||||
|
Status: **design / not yet implemented.** Interim mitigations (config-over-UART,
|
||||||
|
GPIO reservation) are shipped; see "Shipped now" below.
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
The ESP-01S camera bridge has no native USB. Today it is flashed with an external
|
||||||
|
3.3 V USB-UART adapter and a `GPIO0 → GND` jumper held during reset. On an assembled
|
||||||
|
field node that is impractical — we want to update it over the network.
|
||||||
|
|
||||||
|
## Why not Wi-Fi OTA on the ESP-01S itself
|
||||||
|
|
||||||
|
1. **Network topology.** The ESP-01S joins the *GoPro* AP (`10.5.5.1`), not the hub /
|
||||||
|
travel-router network. The hub cannot reach it to push an OTA.
|
||||||
|
2. **1 MB flash.** Standard ESP8266 OTA stages a second copy of the sketch alongside the
|
||||||
|
running one. Our sketch is ~333 KB; a 1 MB module has no room for two copies plus FS
|
||||||
|
and reserved areas.
|
||||||
|
|
||||||
|
So updates must arrive **through the XIAO**, which is already UART-connected to the
|
||||||
|
ESP-01S and sits on the hub network.
|
||||||
|
|
||||||
|
## Approach: XIAO ESP32-C6 as the flasher (UART OTA)
|
||||||
|
|
||||||
|
The XIAO plays the role the USB-UART adapter plays today, driving the ESP-01S's ROM
|
||||||
|
serial bootloader over the existing UART.
|
||||||
|
|
||||||
|
### Hardware — two added control lines
|
||||||
|
|
||||||
|
| XIAO pin | → ESP-01S | purpose |
|
||||||
|
|----------|-----------|---------|
|
||||||
|
| `D8` (`ESP01_RST_PIN`) | `RST` | pulse low to reset the ESP-01S |
|
||||||
|
| `D10` (`ESP01_PGM_PIN`) | `GPIO0` | hold low across reset → enter bootloader |
|
||||||
|
| `D6` (TX) / `D7` (RX) | `RX` / `TX` | existing `Serial1` link (crossed) |
|
||||||
|
| GND | GND | common ground |
|
||||||
|
|
||||||
|
> **Confirm before committing the PCB/wiring:** verify `D8`/`D10` on the actual XIAO
|
||||||
|
> ESP32-C6 variant do **not** map to ESP32-C6 strapping pins (`GPIO8`, `GPIO9`, `GPIO15`)
|
||||||
|
> or the USB-JTAG pins. Pins are reserved in firmware (`ESP01_RST_PIN`, `ESP01_PGM_PIN`)
|
||||||
|
> but not yet driven.
|
||||||
|
|
||||||
|
### Bootloader entry
|
||||||
|
|
||||||
|
`GPIO0 = LOW`, pulse `RST` low→high → ESP-01S enters the serial bootloader on the UART.
|
||||||
|
After writing: `GPIO0 = HIGH`, pulse `RST` → run the new firmware. Always restore
|
||||||
|
`GPIO0 = HIGH` on give-up so the ESP can boot normally.
|
||||||
|
|
||||||
|
### Flash protocol
|
||||||
|
|
||||||
|
Implement enough of the ESP8266 ROM bootloader / esptool SLIP protocol on the XIAO over
|
||||||
|
`Serial1`:
|
||||||
|
|
||||||
|
- `SYNC`, then `FLASH_BEGIN` / `FLASH_DATA` (≈1 KB blocks) / `FLASH_END` to write the app
|
||||||
|
at offset `0x0`.
|
||||||
|
- Start at 115200 baud; optionally raise after sync.
|
||||||
|
- Verify with the ROM `SPI_FLASH_MD5` against the expected MD5.
|
||||||
|
|
||||||
|
### Firmware delivery (hub → XIAO)
|
||||||
|
|
||||||
|
Greenfield on the Go hub (only a `firmware_version` field exists today). Recommended:
|
||||||
|
|
||||||
|
- **HTTP pull.** Hub exposes `GET /firmware/esp01s/<version>.bin` (+ MD5). XIAO is
|
||||||
|
triggered by an MQTT command, e.g.
|
||||||
|
`{"command":"update_esp01s","url":"http://<hub>/firmware/esp01s/0.4.0.bin","md5":"…"}`,
|
||||||
|
fetches the `.bin` in chunks, and streams each chunk straight into a `FLASH_DATA` block.
|
||||||
|
- Avoid buffering the whole image in RAM — stream HTTP chunk → flash block → repeat.
|
||||||
|
- MQTT chunked transfer is possible but heavier on the broker; prefer HTTP.
|
||||||
|
|
||||||
|
### Sequencing / safety
|
||||||
|
|
||||||
|
- Pause the UART JSON status/command protocol while flashing (the link is busy with the
|
||||||
|
bootloader protocol).
|
||||||
|
- On failure leave the ESP recoverable and retry; report progress/result to the hub over
|
||||||
|
MQTT.
|
||||||
|
|
||||||
|
### XIAO self-update (separate, easy)
|
||||||
|
|
||||||
|
The XIAO (4 MB flash, on the hub network) can use standard ESP32 OTA (`ArduinoOTA` or
|
||||||
|
`httpUpdate`). No gymnastics required. Split: **XIAO = native OTA; ESP-01S = flashed by
|
||||||
|
the XIAO over UART.**
|
||||||
|
|
||||||
|
## Scope estimate
|
||||||
|
|
||||||
|
- **XIAO firmware:** ESP8266 ROM-loader client over `Serial1` + `GPIO0`/`RST` control +
|
||||||
|
HTTP fetch + MQTT trigger. Medium–large.
|
||||||
|
- **Hub (Go):** firmware store + HTTP endpoint + MQTT trigger command. Small–medium.
|
||||||
|
- **Hardware:** two control wires + confirm non-strapping pins. Small.
|
||||||
|
|
||||||
|
## Shipped now (interim)
|
||||||
|
|
||||||
|
- **Config-over-UART (`set_config`)** — change GoPro SSID/password/IP and poll interval
|
||||||
|
with no reflash. Hub → `set_camera_config` (MQTT) → XIAO → `set_config` (UART) →
|
||||||
|
ESP-01S persists to LittleFS.
|
||||||
|
- **GPIO reservation** — `ESP01_RST_PIN = D8`, `ESP01_PGM_PIN = D10` reserved in the XIAO
|
||||||
|
firmware for the flasher.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- ESP8266 ROM serial bootloader protocol (esptool).
|
||||||
|
- Wiring: Notion "XIAO ESP32-C6 Pin-to-Pin Wiring Diagram".
|
||||||
@@ -113,6 +113,13 @@ int batteryPct(int raw) {
|
|||||||
#define UART_RX_PIN D7
|
#define UART_RX_PIN D7
|
||||||
#define UART_TX_PIN D6
|
#define UART_TX_PIN D6
|
||||||
|
|
||||||
|
// Reserved for future ESP-01S UART OTA ("XIAO as flasher"): two control
|
||||||
|
// lines let the XIAO drive the ESP-01S into its serial bootloader and
|
||||||
|
// reflash it over Serial1 — no USB-UART adapter or GPIO0 jumper needed.
|
||||||
|
// Not driven yet; see docs/design/esp01s-uart-ota.md.
|
||||||
|
#define ESP01_RST_PIN D8 // → ESP-01S RST (pulse low to reset)
|
||||||
|
#define ESP01_PGM_PIN D10 // → ESP-01S GPIO0 (low at reset = bootloader)
|
||||||
|
|
||||||
// ────────────────────────────────────────────
|
// ────────────────────────────────────────────
|
||||||
// RGB STAT LED — D0/D1/D2 (red/green/blue) via 220Ω each
|
// RGB STAT LED — D0/D1/D2 (red/green/blue) via 220Ω each
|
||||||
// ────────────────────────────────────────────
|
// ────────────────────────────────────────────
|
||||||
@@ -263,6 +270,20 @@ void mqttCallback(char* topic, byte* payload, unsigned int len) {
|
|||||||
saveConfig();
|
saveConfig();
|
||||||
Serial.printf("[BAT] Calibration set: raw_min=%d raw_max=%d\n",
|
Serial.printf("[BAT] Calibration set: raw_min=%d raw_max=%d\n",
|
||||||
cfg.bat_raw_min, cfg.bat_raw_max);
|
cfg.bat_raw_min, cfg.bat_raw_max);
|
||||||
|
} else if (cmd == "set_camera_config") {
|
||||||
|
// Forward camera-bridge config to the ESP-01S over UART so the
|
||||||
|
// GoPro creds / poll rate can change without reflashing it.
|
||||||
|
JsonDocument out;
|
||||||
|
out["type"] = "cmd";
|
||||||
|
out["command"] = "set_config";
|
||||||
|
if (!doc["camera_ssid"].isNull()) out["camera_ssid"] = doc["camera_ssid"];
|
||||||
|
if (!doc["camera_password"].isNull()) out["camera_password"] = doc["camera_password"];
|
||||||
|
if (!doc["camera_ip"].isNull()) out["camera_ip"] = doc["camera_ip"];
|
||||||
|
if (!doc["poll_interval_sec"].isNull()) out["poll_interval_sec"] = doc["poll_interval_sec"];
|
||||||
|
String line; serializeJson(out, line);
|
||||||
|
UART_ESP8266.println(line);
|
||||||
|
UART_ESP8266.flush();
|
||||||
|
Serial.println("[MQTT] Forwarded set_config → ESP-01S");
|
||||||
} else if (cmd == "registered") {
|
} else if (cmd == "registered") {
|
||||||
String id = doc["camera_id"] | "";
|
String id = doc["camera_id"] | "";
|
||||||
if (id.length() > 0 && id != cfg.camera_id) {
|
if (id.length() > 0 && id != cfg.camera_id) {
|
||||||
|
|||||||
@@ -16,17 +16,23 @@
|
|||||||
* ESP32 → ESP8266: {"type":"cmd","command":"start_recording"}\n
|
* ESP32 → ESP8266: {"type":"cmd","command":"start_recording"}\n
|
||||||
*
|
*
|
||||||
* Hardware:
|
* Hardware:
|
||||||
* - ESP8266 D1 Mini (or NodeMCU)
|
* - ESP-01S (ESP8266, 1MB flash) on its own 3.3V buck
|
||||||
* - UART TX → ESP32 RX (GPIO 16)
|
* - UART is the hardware Serial (GPIO1 TX / GPIO3 RX), crossed:
|
||||||
* - UART RX → ESP32 TX (GPIO 16)
|
* ESP-01S TX (GPIO1) → XIAO D7 (RX)
|
||||||
|
* ESP-01S RX (GPIO3) ← XIAO D6 (TX)
|
||||||
* - Shared GND between boards
|
* - Shared GND between boards
|
||||||
* - LiPo → 3.3V buck → VIN on both boards
|
* - Flash with a 3.3V USB-UART adapter, GPIO0 → GND on power-up
|
||||||
|
*
|
||||||
|
* Note: the JSON protocol shares the same UART as the boot-ROM/debug
|
||||||
|
* output, so the ESP32 also sees boot chatter and ignores it as
|
||||||
|
* non-JSON. There is no spare pin for a status LED on the ESP-01S
|
||||||
|
* (GPIO1 is the UART TX) — status is shown on the XIAO panel instead.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include <ESP8266WiFi.h>
|
#include <ESP8266WiFi.h>
|
||||||
#include <WiFiClient.h>
|
#include <WiFiClient.h>
|
||||||
#include <HTTPClient.h>
|
#include <ESP8266HTTPClient.h>
|
||||||
#include <ArduinoJson.h>
|
#include <ArduinoJson.h>
|
||||||
#include <LittleFS.h>
|
#include <LittleFS.h>
|
||||||
|
|
||||||
@@ -58,6 +64,22 @@ bool loadConfig() {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Persist current config to LittleFS. Lets the hub update camera
|
||||||
|
// credentials/poll rate over UART without reflashing the ESP-01S.
|
||||||
|
bool saveConfig() {
|
||||||
|
if (!LittleFS.begin()) { Serial.println("[CFG] LittleFS mount failed"); return false; }
|
||||||
|
File f = LittleFS.open("/config.json", "w");
|
||||||
|
if (!f) { Serial.println("[CFG] open for write failed"); return false; }
|
||||||
|
JsonDocument doc;
|
||||||
|
doc["camera_ssid"] = cfg.camera_ssid;
|
||||||
|
doc["camera_password"] = cfg.camera_password;
|
||||||
|
doc["camera_ip"] = cfg.camera_ip;
|
||||||
|
doc["poll_interval_sec"] = cfg.poll_interval_sec;
|
||||||
|
serializeJson(doc, f);
|
||||||
|
f.close();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// ────────────────────────────────────────────
|
// ────────────────────────────────────────────
|
||||||
// Camera HTTP Client (GoPro Hero 3)
|
// Camera HTTP Client (GoPro Hero 3)
|
||||||
// ────────────────────────────────────────────
|
// ────────────────────────────────────────────
|
||||||
@@ -173,6 +195,21 @@ void handleCommand(const JsonDocument& doc) {
|
|||||||
pong["type"] = "pong";
|
pong["type"] = "pong";
|
||||||
pong["uptime_ms"] = millis();
|
pong["uptime_ms"] = millis();
|
||||||
sendToESP32(pong);
|
sendToESP32(pong);
|
||||||
|
} else if (cmd == "set_config") {
|
||||||
|
// No-reflash config update from the hub (via the XIAO over UART).
|
||||||
|
// Only provided fields change; the rest keep their current value.
|
||||||
|
String oldSsid = cfg.camera_ssid, oldPw = cfg.camera_password;
|
||||||
|
cfg.camera_ssid = doc["camera_ssid"] | cfg.camera_ssid;
|
||||||
|
cfg.camera_password = doc["camera_password"] | cfg.camera_password;
|
||||||
|
cfg.camera_ip = doc["camera_ip"] | cfg.camera_ip;
|
||||||
|
cfg.poll_interval_sec = doc["poll_interval_sec"] | cfg.poll_interval_sec;
|
||||||
|
saveConfig();
|
||||||
|
sendAck("set_config");
|
||||||
|
// Re-associate if the camera Wi-Fi credentials changed.
|
||||||
|
if (cfg.camera_ssid != oldSsid || cfg.camera_password != oldPw) {
|
||||||
|
WiFi.disconnect();
|
||||||
|
WiFi.begin(cfg.camera_ssid.c_str(), cfg.camera_password.c_str());
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
sendError("Unknown command: " + cmd);
|
sendError("Unknown command: " + cmd);
|
||||||
}
|
}
|
||||||
@@ -197,26 +234,16 @@ bool readLine(String& line) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ────────────────────────────────────────────
|
|
||||||
// LED
|
|
||||||
// ────────────────────────────────────────────
|
|
||||||
|
|
||||||
const int LED = LED_BUILTIN; // active-low on ESP8266 D1 Mini
|
|
||||||
|
|
||||||
void ledOn() { digitalWrite(LED, LOW); }
|
|
||||||
void ledOff() { digitalWrite(LED, HIGH); }
|
|
||||||
|
|
||||||
// ────────────────────────────────────────────
|
// ────────────────────────────────────────────
|
||||||
// Setup
|
// Setup
|
||||||
// ────────────────────────────────────────────
|
// ────────────────────────────────────────────
|
||||||
|
// No status LED: GPIO1 is the UART TX to the XIAO and GPIO3 is RX,
|
||||||
|
// leaving no free pin on the ESP-01S. Status lives on the XIAO panel.
|
||||||
|
|
||||||
void setup() {
|
void setup() {
|
||||||
Serial.begin(115200);
|
Serial.begin(115200);
|
||||||
delay(500);
|
delay(500);
|
||||||
Serial.println("\n[BRIDGE] ESP8266 Camera Bridge v1.0");
|
Serial.println("\n[BRIDGE] ESP-01S Camera Bridge v1.0");
|
||||||
|
|
||||||
pinMode(LED, OUTPUT);
|
|
||||||
ledOff();
|
|
||||||
|
|
||||||
loadConfig();
|
loadConfig();
|
||||||
|
|
||||||
@@ -232,7 +259,6 @@ void setup() {
|
|||||||
|
|
||||||
if (WiFi.status() == WL_CONNECTED) {
|
if (WiFi.status() == WL_CONNECTED) {
|
||||||
Serial.printf("\n[WIFI] Connected. IP: %s\n", WiFi.localIP().toString().c_str());
|
Serial.printf("\n[WIFI] Connected. IP: %s\n", WiFi.localIP().toString().c_str());
|
||||||
ledOn(); // Solid = connected
|
|
||||||
} else {
|
} else {
|
||||||
Serial.println("\n[WIFI] FAILED — will retry in loop");
|
Serial.println("\n[WIFI] FAILED — will retry in loop");
|
||||||
}
|
}
|
||||||
@@ -246,7 +272,6 @@ void loop() {
|
|||||||
unsigned long now = millis();
|
unsigned long now = millis();
|
||||||
static unsigned long lastPoll = 0;
|
static unsigned long lastPoll = 0;
|
||||||
static unsigned long lastWiFiRetry = 0;
|
static unsigned long lastWiFiRetry = 0;
|
||||||
static bool cameraOnline = false;
|
|
||||||
|
|
||||||
// ── Wi-Fi reconnection ──
|
// ── Wi-Fi reconnection ──
|
||||||
if (WiFi.status() != WL_CONNECTED && now - lastWiFiRetry > 10000) {
|
if (WiFi.status() != WL_CONNECTED && now - lastWiFiRetry > 10000) {
|
||||||
@@ -261,15 +286,6 @@ void loop() {
|
|||||||
|
|
||||||
if (WiFi.status() == WL_CONNECTED) {
|
if (WiFi.status() == WL_CONNECTED) {
|
||||||
CamStatus s = fetchStatus();
|
CamStatus s = fetchStatus();
|
||||||
|
|
||||||
if (s.valid && !cameraOnline) {
|
|
||||||
cameraOnline = true;
|
|
||||||
ledOn();
|
|
||||||
} else if (!s.valid && cameraOnline) {
|
|
||||||
cameraOnline = false;
|
|
||||||
ledOff();
|
|
||||||
}
|
|
||||||
|
|
||||||
sendStatus(s);
|
sendStatus(s);
|
||||||
} else {
|
} else {
|
||||||
// Offline — send empty status so ESP32 knows we're alive but camera is down
|
// Offline — send empty status so ESP32 knows we're alive but camera is down
|
||||||
@@ -291,13 +307,4 @@ void loop() {
|
|||||||
// Ignore other message types — they're for the ESP32
|
// Ignore other message types — they're for the ESP32
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── LED blink when offline ──
|
|
||||||
if (!cameraOnline) {
|
|
||||||
static unsigned long lastBlink = 0;
|
|
||||||
if (now - lastBlink > 500) {
|
|
||||||
lastBlink = now;
|
|
||||||
digitalWrite(LED, !digitalRead(LED));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-4
@@ -129,7 +129,7 @@ USB Power Bank
|
|||||||
└── USB-C cable → bottom USB-C female input on Camera Node Case
|
└── USB-C cable → bottom USB-C female input on Camera Node Case
|
||||||
├── rocker switch → node power rail
|
├── rocker switch → node power rail
|
||||||
├── PWR LED indicator
|
├── PWR LED indicator
|
||||||
├── ESP32-C3 Super Mini
|
├── XIAO ESP32-C6
|
||||||
├── ESP-01S / ESP8266
|
├── ESP-01S / ESP8266
|
||||||
├── 1.3-inch OLED display
|
├── 1.3-inch OLED display
|
||||||
├── RGB status LED
|
├── RGB status LED
|
||||||
@@ -138,9 +138,14 @@ USB Power Bank
|
|||||||
(power only — no data)
|
(power only — no data)
|
||||||
|
|
||||||
UART / control inside case:
|
UART / control inside case:
|
||||||
ESP-01S TX ──→ ESP32-C3 RX
|
ESP-01S TX (GPIO1) ──→ XIAO D7 (RX)
|
||||||
ESP-01S RX ←── ESP32-C3 TX
|
ESP-01S RX (GPIO3) ←── XIAO D6 (TX)
|
||||||
ESP-01S GND ─── ESP32-C3 GND
|
ESP-01S GND ─── XIAO GND
|
||||||
|
|
||||||
|
# Reserved for UART OTA (XIAO reflashes the ESP-01S — no adapter):
|
||||||
|
ESP-01S RST ←── XIAO D8 (pulse low to reset)
|
||||||
|
ESP-01S GPIO0 ←── XIAO D10 (low at reset = bootloader)
|
||||||
|
# See docs/design/esp01s-uart-ota.md
|
||||||
```
|
```
|
||||||
|
|
||||||
**Power note:** exact wiring depends on the regulator/power board used. Confirm OLED voltage, LED current limiting, and whether the rocker switches USB 5 V input or a regulated node rail.
|
**Power note:** exact wiring depends on the regulator/power board used. Confirm OLED voltage, LED current limiting, and whether the rocker switches USB 5 V input or a regulated node rail.
|
||||||
|
|||||||
Reference in New Issue
Block a user