firmware: scaffold optional cargo OLED display

This commit is contained in:
2026-06-08 08:45:24 -06:00
parent bc56514489
commit 46d52052e3
4 changed files with 315 additions and 0 deletions
+28
View File
@@ -113,3 +113,31 @@ Implementation:
- Sender to ADS1115 over I2C if no suitable exposed ADC exists
Use lookup-table interpolation.
## Cargo OLED service display
Planned optional hardware:
- 0.96 inch SSD1306 OLED
- 128x64 resolution
- I2C interface
- Typical address: 0x3C
- Suggested initial pins:
- SDA: GPIO 21
- SCL: GPIO 22
- VCC: 3.3V
- GND: common ground
Purpose:
- Show first-boot AP setup credentials without requiring Serial Monitor.
- Show AP SSID, password, and IP address during setup/recovery.
- Show quick service status:
- battery SOC
- voltage/current
- relay states
- WiFi/IP
- active alarms
Firmware support is optional and disabled by default with OLED_ENABLED set to 0. Enable only after the OLED libraries are installed and the display is wired.
+25
View File
@@ -103,3 +103,28 @@ UI direction:
- Add QR code display for joining Cargo AP.
- Add dashboard settings UI.
- Add LVGL on-screen keyboard only after core dashboard functionality is stable.
## Cargo OLED firmware foundation
Status: planned / scaffolded
Initial OLED firmware goals:
- Add optional SSD1306 128x64 support behind a compile-time flag.
- Keep OLED support disabled by default so the controller firmware builds without extra libraries.
- Display boot/setup status.
- Display AP SSID/password/IP for phone-first setup.
- Display rotating service pages:
- battery
- relays
- network
- alarms
Future OLED goals:
- Add setup/status button.
- Short press cycles pages.
- Long press shows AP credentials.
- Extended hold enters recovery/reset mode.
- Add first-boot setup screen once AP password generation is finalized.
@@ -3,6 +3,15 @@
#include <Preferences.h>
#include <esp_wifi.h>
#include <esp_system.h>
#define OLED_ENABLED 0
#if OLED_ENABLED
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#endif
#include <ArduinoJson.h>
#include "config.h"
@@ -1163,6 +1172,231 @@ void handleUpdateApConfig() {
server.send(200, "application/json", output);
}
#if OLED_ENABLED
const int OLED_SCREEN_WIDTH = 128;
const int OLED_SCREEN_HEIGHT = 64;
const int OLED_RESET_PIN = -1;
const uint8_t OLED_I2C_ADDR = 0x3C;
// Default ESP32 I2C pins. Change these if the cargo controller wiring uses a different I2C bus.
const int OLED_I2C_SDA = 21;
const int OLED_I2C_SCL = 22;
// Optional future setup/status button. Leave disabled until hardware is wired.
const int OLED_BUTTON_PIN = -1;
Adafruit_SSD1306 oled(OLED_SCREEN_WIDTH, OLED_SCREEN_HEIGHT, &Wire, OLED_RESET_PIN);
bool oledReady = false;
unsigned long lastOledUpdateMs = 0;
unsigned long lastOledPageChangeMs = 0;
uint8_t oledPage = 0;
void oledClear(const char* title) {
oled.clearDisplay();
oled.setTextSize(1);
oled.setTextColor(SSD1306_WHITE);
oled.setCursor(0, 0);
oled.println(title);
oled.drawLine(0, 10, OLED_SCREEN_WIDTH - 1, 10, SSD1306_WHITE);
}
void oledPrintTrimmed(const String& value, int maxChars = 21) {
if (value.length() <= maxChars) {
oled.println(value);
} else {
oled.println(value.substring(0, maxChars));
}
}
void oledShowBoot() {
if (!oledReady) return;
oledClear("Overland Controller");
oled.setCursor(0, 18);
oled.println("Booting...");
oled.println();
oled.println("Cargo Controller");
oled.display();
}
void oledShowSetupPage() {
if (!oledReady) return;
oledClear("Setup / AP");
oled.setCursor(0, 14);
oled.print("SSID: ");
oledPrintTrimmed(apSsid, 15);
oled.print("PASS: ");
oledPrintTrimmed(apPassword, 15);
oled.print("IP: ");
oled.println(WiFi.softAPIP());
oled.display();
}
void oledShowBatteryPage() {
if (!oledReady) return;
oledClear("Battery");
oled.setCursor(0, 16);
if (bmsData.valid) {
oled.print("SOC: ");
oled.print(bmsData.soc);
oled.println("%");
oled.print("V: ");
oled.print(bmsData.voltage, 2);
oled.println("V");
oled.print("A: ");
oled.print(bmsData.current, 1);
oled.println("A");
oled.print("Temp: ");
oled.print(bmsData.temperatureF, 1);
oled.println("F");
} else {
oled.println("BMS unavailable");
oled.println();
oled.println("Check BLE/config");
}
oled.display();
}
void oledShowRelayPage() {
if (!oledReady) return;
oledClear("Relays");
oled.setCursor(0, 16);
oled.print(appConfig.relays[0].name);
oled.print(": ");
oled.println(relays.relay1 ? "ON" : "OFF");
oled.print(appConfig.relays[1].name);
oled.print(": ");
oled.println(relays.relay2 ? "ON" : "OFF");
oled.display();
}
void oledShowNetworkPage() {
if (!oledReady) return;
oledClear("Network");
oled.setCursor(0, 14);
oled.print("AP: ");
oledPrintTrimmed(apSsid, 17);
oled.print("AP IP: ");
oled.println(WiFi.softAPIP());
oled.print("STA: ");
if (WiFi.status() == WL_CONNECTED) {
oled.println(WiFi.localIP());
} else {
oled.println("not connected");
}
oled.display();
}
void oledShowAlarmPage() {
if (!oledReady) return;
oledClear("Alarms");
oled.setCursor(0, 14);
bool any = false;
if (alarms.lowSoc) {
oled.println("LOW SOC");
any = true;
}
if (alarms.criticalSoc) {
oled.println("CRITICAL SOC");
any = true;
}
if (alarms.bmsDisconnected) {
oled.println("BMS DISCONNECTED");
any = true;
}
if (alarms.highBatteryTemp) {
oled.println("HIGH BATT TEMP");
any = true;
}
if (alarms.cellImbalance) {
oled.println("CELL IMBALANCE");
any = true;
}
if (!any) {
oled.println("No active alarms");
}
oled.display();
}
void oledRenderPage() {
switch (oledPage) {
case 0:
oledShowBatteryPage();
break;
case 1:
oledShowRelayPage();
break;
case 2:
oledShowNetworkPage();
break;
case 3:
oledShowAlarmPage();
break;
default:
oledPage = 0;
oledShowBatteryPage();
break;
}
}
void oledBegin() {
Wire.begin(OLED_I2C_SDA, OLED_I2C_SCL);
oledReady = oled.begin(SSD1306_SWITCHCAPVCC, OLED_I2C_ADDR);
if (!oledReady) {
Serial.println("OLED not detected");
return;
}
Serial.println("OLED initialized");
oledShowBoot();
}
void oledLoop() {
if (!oledReady) return;
unsigned long now = millis();
if (now - lastOledPageChangeMs > 5000) {
oledPage = (oledPage + 1) % 4;
lastOledPageChangeMs = now;
}
if (now - lastOledUpdateMs > 1000) {
oledRenderPage();
lastOledUpdateMs = now;
}
}
#else
void oledBegin() {}
void oledShowSetupPage() {}
void oledLoop() {}
#endif
int findNextWifiIndexByPriority(bool tried[]) {
int bestIndex = -1;
int bestPriority = 1000000;
@@ -3139,6 +3373,8 @@ void setup() {
Serial.println("Overland Controller Booting");
Serial.println("==================================");
oledBegin();
loadConfig();
printConfig();
@@ -3153,6 +3389,7 @@ void setup() {
WiFi.mode(WIFI_AP_STA);
startAccessPoint();
oledShowSetupPage();
connectStaWifi();
@@ -3235,6 +3472,7 @@ void loop() {
updateBms();
updateAlarms();
pollDashboardUart();
oledLoop();
static unsigned long lastSensorUpdate = 0;
+24
View File
@@ -401,3 +401,27 @@ def test_embedded_webui_config_subtabs():
assert "config-maintenance" in source
assert "WiFi / AP" in source
assert "subtabbtn active" in source
def test_cargo_oled_support_is_optional():
source = firmware_source()
assert "#define OLED_ENABLED 0" in source
assert "#if OLED_ENABLED" in source
assert "void oledBegin()" in source
assert "void oledLoop()" in source
assert "void oledShowSetupPage()" in source
assert "OLED_I2C_ADDR = 0x3C" in source
assert "Adafruit_SSD1306" in source
def test_cargo_oled_shows_setup_and_service_pages():
source = firmware_source()
assert 'oledClear("Setup / AP")' in source
assert 'oledClear("Battery")' in source
assert 'oledClear("Relays")' in source
assert 'oledClear("Network")' in source
assert 'oledClear("Alarms")' in source
assert "oledShowSetupPage();" in source
assert "oledLoop();" in source