generated from CubeCraft-Creations/Tracehound
firmware: no-reflash config updates for ESP-01S + UART-OTA groundwork
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>
This commit is contained in:
@@ -113,6 +113,13 @@ int batteryPct(int raw) {
|
||||
#define UART_RX_PIN D7
|
||||
#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
|
||||
// ────────────────────────────────────────────
|
||||
@@ -263,6 +270,20 @@ void mqttCallback(char* topic, byte* payload, unsigned int len) {
|
||||
saveConfig();
|
||||
Serial.printf("[BAT] Calibration set: raw_min=%d raw_max=%d\n",
|
||||
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") {
|
||||
String id = doc["camera_id"] | "";
|
||||
if (id.length() > 0 && id != cfg.camera_id) {
|
||||
|
||||
@@ -16,17 +16,23 @@
|
||||
* ESP32 → ESP8266: {"type":"cmd","command":"start_recording"}\n
|
||||
*
|
||||
* Hardware:
|
||||
* - ESP8266 D1 Mini (or NodeMCU)
|
||||
* - UART TX → ESP32 RX (GPIO 16)
|
||||
* - UART RX → ESP32 TX (GPIO 16)
|
||||
* - ESP-01S (ESP8266, 1MB flash) on its own 3.3V buck
|
||||
* - UART is the hardware Serial (GPIO1 TX / GPIO3 RX), crossed:
|
||||
* ESP-01S TX (GPIO1) → XIAO D7 (RX)
|
||||
* ESP-01S RX (GPIO3) ← XIAO D6 (TX)
|
||||
* - 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 <ESP8266WiFi.h>
|
||||
#include <WiFiClient.h>
|
||||
#include <HTTPClient.h>
|
||||
#include <ESP8266HTTPClient.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <LittleFS.h>
|
||||
|
||||
@@ -58,6 +64,22 @@ bool loadConfig() {
|
||||
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)
|
||||
// ────────────────────────────────────────────
|
||||
@@ -173,6 +195,21 @@ void handleCommand(const JsonDocument& doc) {
|
||||
pong["type"] = "pong";
|
||||
pong["uptime_ms"] = millis();
|
||||
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 {
|
||||
sendError("Unknown command: " + cmd);
|
||||
}
|
||||
@@ -197,26 +234,16 @@ bool readLine(String& line) {
|
||||
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
|
||||
// ────────────────────────────────────────────
|
||||
// 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() {
|
||||
Serial.begin(115200);
|
||||
delay(500);
|
||||
Serial.println("\n[BRIDGE] ESP8266 Camera Bridge v1.0");
|
||||
|
||||
pinMode(LED, OUTPUT);
|
||||
ledOff();
|
||||
Serial.println("\n[BRIDGE] ESP-01S Camera Bridge v1.0");
|
||||
|
||||
loadConfig();
|
||||
|
||||
@@ -232,7 +259,6 @@ void setup() {
|
||||
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
Serial.printf("\n[WIFI] Connected. IP: %s\n", WiFi.localIP().toString().c_str());
|
||||
ledOn(); // Solid = connected
|
||||
} else {
|
||||
Serial.println("\n[WIFI] FAILED — will retry in loop");
|
||||
}
|
||||
@@ -246,7 +272,6 @@ void loop() {
|
||||
unsigned long now = millis();
|
||||
static unsigned long lastPoll = 0;
|
||||
static unsigned long lastWiFiRetry = 0;
|
||||
static bool cameraOnline = false;
|
||||
|
||||
// ── Wi-Fi reconnection ──
|
||||
if (WiFi.status() != WL_CONNECTED && now - lastWiFiRetry > 10000) {
|
||||
@@ -261,15 +286,6 @@ void loop() {
|
||||
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
CamStatus s = fetchStatus();
|
||||
|
||||
if (s.valid && !cameraOnline) {
|
||||
cameraOnline = true;
|
||||
ledOn();
|
||||
} else if (!s.valid && cameraOnline) {
|
||||
cameraOnline = false;
|
||||
ledOff();
|
||||
}
|
||||
|
||||
sendStatus(s);
|
||||
} else {
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// ── LED blink when offline ──
|
||||
if (!cameraOnline) {
|
||||
static unsigned long lastBlink = 0;
|
||||
if (now - lastBlink > 500) {
|
||||
lastBlink = now;
|
||||
digitalWrite(LED, !digitalRead(LED));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user