firmware: add OLED status panel to camera node (C6)
Build (Dev) / build (push) Failing after 12s
CI/CD / lint-and-typecheck (push) Successful in 9m31s
CI/CD / test (push) Successful in 9m27s
CI/CD / build (push) Failing after 4m49s
CI/CD / deploy (push) Has been skipped

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>
This commit is contained in:
Joshua King
2026-06-04 18:22:23 -04:00
parent 2fb73ec8c4
commit 996ef87dfd
2 changed files with 111 additions and 2 deletions
+3 -1
View File
@@ -65,7 +65,9 @@ platform = https://github.com/pioarduino/platform-espressif32/releases/download/
board = seeed_xiao_esp32c6
framework = arduino
monitor_speed = 115200
lib_deps = ${common.lib_deps}
lib_deps =
${common.lib_deps}
olikraus/U8g2 @ ^2.35
build_flags = ${common.build_flags}
-D CONFIG_ARDUINO_LOOP_STACK_SIZE=8192
-D ARDUINO_USB_MODE=1
+108 -1
View File
@@ -31,6 +31,8 @@
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include <SPIFFS.h>
#include <Wire.h>
#include <U8g2lib.h>
// ────────────────────────────────────────────
// Configuration (SPIFFS)
@@ -91,9 +93,58 @@ bool saveConfig() {
#define UART_TX_PIN D6
// Camera-online indicator → green channel of the RGB STAT LED.
// (Full RGB/OLED status-panel bring-up is not implemented yet.)
#define STAT_LED_PIN D1
// ────────────────────────────────────────────
// Status OLED — 1.3" I2C panel on D4(SDA)/D5(SCL)
// ────────────────────────────────────────────
// 1.3" 128x64 modules are SH1106. If the image is shifted ~2px or
// wrapped, the panel is an SSD1306 — swap the constructor below to
// U8G2_SSD1306_128X64_NONAME_F_HW_I2C.
#define OLED_SDA_PIN D4
#define OLED_SCL_PIN D5
#define OLED_I2C_ADDR 0x3C
U8G2_SH1106_128X64_NONAME_F_HW_I2C oled(U8G2_R0, U8X8_PIN_NONE);
bool oledReady = false;
// Last-known camera status, mirrored for the display.
int dispBatteryRaw = 0;
bool dispRecording = false;
int dispVideoRemain = 0; // seconds
unsigned long recStartMs = 0; // 0 = not recording
// Walk the bus and log every responder — confirms the OLED address
// (and wiring) independent of the display driver.
void i2cScan() {
Serial.println("[I2C] Scanning...");
byte found = 0;
for (byte a = 1; a < 127; a++) {
Wire.beginTransmission(a);
if (Wire.endTransmission() == 0) {
Serial.printf("[I2C] device @ 0x%02X\n", a);
found++;
}
}
if (!found) Serial.println("[I2C] none found — check wiring/power");
}
void displayInit() {
Wire.begin(OLED_SDA_PIN, OLED_SCL_PIN);
i2cScan();
oled.setI2CAddress(OLED_I2C_ADDR << 1);
oledReady = oled.begin();
Serial.printf("[OLED] begin %s\n", oledReady ? "ok" : "FAILED");
if (!oledReady) return;
oled.clearBuffer();
oled.setFont(u8g2_font_7x14B_tr);
oled.drawStr(0, 14, "RemoteRig");
oled.setFont(u8g2_font_6x10_tr);
oled.drawStr(0, 32, "Camera node");
oled.drawStr(0, 46, "booting...");
oled.sendBuffer();
}
void sendCmdToESP8266(const String& command) {
JsonDocument doc;
doc["type"] = "cmd";
@@ -201,6 +252,48 @@ bool connectMQTT() {
return true;
}
// ────────────────────────────────────────────
// Status screen
// ────────────────────────────────────────────
void renderStatus() {
if (!oledReady) return;
oled.clearBuffer();
// Camera id (top, bold)
oled.setFont(u8g2_font_7x14B_tr);
String id = cfg.camera_id.length() ? cfg.camera_id : clientID();
oled.drawStr(0, 13, id.c_str());
oled.setFont(u8g2_font_6x10_tr);
char line[24];
// REC state + session timer
if (dispRecording) {
unsigned long s = recStartMs ? (millis() - recStartMs) / 1000 : 0;
oled.drawBox(0, 19, 6, 6); // filled square = REC
snprintf(line, sizeof(line), "REC %02lu:%02lu", s / 60, s % 60);
oled.drawStr(10, 26, line);
} else {
oled.drawStr(0, 26, "IDLE");
}
// Battery (raw until calibrated) + video remaining (minutes)
snprintf(line, sizeof(line), "BAT %d VID %dm", dispBatteryRaw, dispVideoRemain / 60);
oled.drawStr(0, 38, line);
// Uplink to the hub
const char* link = mqtt.connected() ? "LINK: MQTT ok"
: WiFi.status() == WL_CONNECTED ? "LINK: wifi only"
: "LINK: offline";
oled.drawStr(0, 50, link);
// Camera reachability
oled.drawStr(0, 62, cameraOnline ? "CAM: online" : "CAM: --");
oled.sendBuffer();
}
// ────────────────────────────────────────────
// Setup
// ────────────────────────────────────────────
@@ -214,6 +307,8 @@ void setup() {
pinMode(STAT_LED_PIN, OUTPUT); // RGB STAT LED — green = camera online
digitalWrite(STAT_LED_PIN, LOW);
displayInit(); // I2C scan + OLED splash
loadConfig();
// UART to ESP-01S
@@ -251,6 +346,10 @@ void loop() {
static unsigned long lastBeat = 0, lastRecon = 0;
static int reconDelay = 1;
// ── OLED refresh (always — keep the panel live even when offline) ──
static unsigned long lastDisp = 0;
if (now - lastDisp > 500) { lastDisp = now; renderStatus(); }
// ── Wi-Fi watchdog ──
if (WiFi.status() != WL_CONNECTED) {
if (now - lastRecon > 5000) { lastRecon = now; WiFi.reconnect(); }
@@ -288,6 +387,14 @@ void loop() {
digitalWrite(STAT_LED_PIN, online ? HIGH : LOW);
}
// Mirror status onto the OLED fields
dispBatteryRaw = doc["battery_raw"] | 0;
dispVideoRemain = doc["video_remaining_sec"] | 0;
bool rec = doc["recording"] | false;
if (rec && !dispRecording) recStartMs = millis();
if (!rec) recStartMs = 0;
dispRecording = rec;
if (cfg.camera_id.length() > 0) {
// Build the MQTT status payload per contract
JsonDocument mqttDoc;