diff --git a/firmware/platformio.ini b/firmware/platformio.ini index dca0d84..5c73b2e 100644 --- a/firmware/platformio.ini +++ b/firmware/platformio.ini @@ -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 diff --git a/firmware/src/esp32-mqtt-bridge.cpp b/firmware/src/esp32-mqtt-bridge.cpp index fa78fff..5ae9072 100644 --- a/firmware/src/esp32-mqtt-bridge.cpp +++ b/firmware/src/esp32-mqtt-bridge.cpp @@ -31,6 +31,8 @@ #include #include #include +#include +#include // ──────────────────────────────────────────── // 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;