firmware: scaffold optional cargo OLED display
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user