Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
349 changes: 349 additions & 0 deletions usermods/OLED_72x40/OLED_72x40.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,349 @@
#include "wled.h"
#include <U8g2lib.h>

// 128x40 bitmap of Akemi logo, generated by LCD Matrix Studio
const unsigned char akemi_logo [] PROGMEM = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x01, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xFF, 0xC0, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x07, 0xFF, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x06, 0x7E, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x3C, 0x60, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x1C, 0x18, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x1C, 0x18, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x18, 0x20, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x1E, 0x3C, 0x60, 0x00, 0x38, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xE0, 0x00, 0x0C, 0x00, 0x80, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xE0, 0x00,
0x02, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x1F, 0xC3, 0xE0, 0x00, 0x01, 0x00, 0x43, 0xC3, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x1F, 0xE7, 0xE0, 0x00, 0x01, 0x02, 0x46, 0x43,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xE0, 0x00,
0x01, 0x02, 0x44, 0x4F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80,
0x1F, 0xFF, 0xE0, 0x01, 0x01, 0x02, 0x45, 0xC9, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x38, 0x1F, 0xFF, 0xE0, 0x1C, 0x01, 0x32, 0x47, 0x09,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x20, 0x00, 0x00, 0x10,
0x01, 0x12, 0x42, 0x0D, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C,
0x30, 0x00, 0x00, 0x30, 0x01, 0xD2, 0x4B, 0x07, 0xE0, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x0C, 0x2E, 0xE7, 0x70, 0x30, 0x00, 0xDE, 0x79, 0xE0,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x06, 0xE7, 0x60, 0x60,
0x00, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06,
0x06, 0xC6, 0x60, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x03, 0x08, 0xC6, 0x30, 0xC0, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x10, 0xC6, 0x08, 0xC0,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
0xE0, 0xC6, 0x0F, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xE0, 0xC6, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC6, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0xC6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0xC6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC6, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0xC6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00
};


class UsermodOLED72x40 : public Usermod {
private:
// Config defaults: shared by the member initializers and the readFromConfig() fallbacks
static constexpr unsigned long DEFAULT_SCREEN_TIMEOUT_S = 60;
static constexpr unsigned long MAX_SCREEN_TIMEOUT_S = 4294966; // largest value whose *1000 fits in 32 bits
static constexpr int DEFAULT_X_OFFSET = 28;
static constexpr int DEFAULT_Y_OFFSET = 24;
static constexpr int8_t DEFAULT_BUTTON_INDEX = 0;
static constexpr int8_t DEFAULT_HEARTBEAT_LED_PIN = 8;

// The panel is 72x40 pixels, driven as a 128x64 SSD1306 framebuffer
static constexpr int FB_WIDTH = 128;
static constexpr int FB_HEIGHT = 64;
static constexpr int PANEL_WIDTH = 72;
static constexpr int PANEL_HEIGHT = 40;
static constexpr int LOGO_WIDTH_TILES = 16; // drawBitmap() width is in 8-pixel tiles

// Dashboard layout: y baselines relative to the panel's top edge
static constexpr int NAME_BASELINE_Y = 7;
static constexpr int NAME_RULE_Y = 9;
static constexpr int GRAPH_BASE_Y = 26;
static constexpr int GRAPH_MAX_HEIGHT = 15;
static constexpr int STATS_BASELINE_Y = 38;

U8G2_SSD1306_128X64_NONAME_F_HW_I2C *u8g2 = nullptr;
bool enabled = true;
bool setupFailed = false;
bool initDone = false;
unsigned long lastUpdate = 0;
unsigned long lastInteraction = 0;
unsigned long screenTimeoutMS = DEFAULT_SCREEN_TIMEOUT_S * 1000;
int xOff = DEFAULT_X_OFFSET;
int yOff = DEFAULT_Y_OFFSET;
byte vValues[PANEL_WIDTH] = {};
bool flipDisplay = false;
int8_t buttonIndex = DEFAULT_BUTTON_INDEX;
int8_t ledPin = DEFAULT_HEARTBEAT_LED_PIN;
int8_t activeLedPin = -1;

enum DisplayState : uint8_t { SPLASH, DASHBOARD, OFF };
DisplayState displayState = SPLASH;
unsigned long splashStartTime = 0;
static constexpr unsigned long SPLASH_DURATION = 5000;

static const char _name[];
static const char _enabled[];

public:
void setup() override {
if (!enabled) return;

// Use WLED's global I2C pins; user must configure them in Usermod Preferences
if (i2c_sda < 0 || i2c_scl < 0) {
setupFailed = true;
return;
}

// Allocate LED pin for heartbeat indicator
if (ledPin >= 0 && PinManager::allocatePin(ledPin, true, PinOwner::UM_Unspecified)) {
activeLedPin = ledPin;
pinMode(activeLedPin, OUTPUT);
}

u8g2 = new U8G2_SSD1306_128X64_NONAME_F_HW_I2C(U8G2_R0, U8X8_PIN_NONE);

Comment thread
softhack007 marked this conversation as resolved.
Resolved
if (!u8g2->begin()) {
if (activeLedPin >= 0) {
pinMode(activeLedPin, INPUT);
PinManager::deallocatePin(activeLedPin, PinOwner::UM_Unspecified);
activeLedPin = -1;
}
delete u8g2;
u8g2 = nullptr;
setupFailed = true;
return;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}

u8g2->setContrast(255);
u8g2->setFlipMode(flipDisplay ? 1 : 0);
u8g2->clearBuffer();

// Draw splash screen (non-blocking: no delay, handled in loop())
int splashY = flipDisplay ? (FB_HEIGHT - PANEL_HEIGHT - yOff) : yOff;
u8g2->drawBitmap(0, splashY, LOGO_WIDTH_TILES, PANEL_HEIGHT, akemi_logo);

int textX = flipDisplay ? (FB_WIDTH - PANEL_WIDTH - xOff) : xOff;
u8g2->setFont(u8g2_font_4x6_tf);
u8g2->setCursor(textX + 12, splashY + STATS_BASELINE_Y);
if (WLEDNetwork.isConnected()) u8g2->print(WLEDNetwork.localIP());
else u8g2->print(cmDNS);

u8g2->sendBuffer();
splashStartTime = millis();
displayState = SPLASH;
lastInteraction = millis();
initDone = true;
}

void loop() override {
if (!enabled || setupFailed || !initDone || !u8g2) return;

// LED heartbeat
if (activeLedPin >= 0) {
static unsigned long ledTimer = 0;
if (!WLEDNetwork.isConnected()) {
if (millis() - ledTimer > 200) {
ledTimer = millis();
digitalWrite(activeLedPin, !digitalRead(activeLedPin));
}
} else {
float pulse = (sin_approx(millis() / 2000.0f * PI) + 1.0f) * 127.5f;
analogWrite(activeLedPin, (int)pulse);
}
}

// Non-blocking splash: wait for duration, then switch to dashboard
if (displayState == SPLASH) {
if (millis() - splashStartTime >= SPLASH_DURATION) {
displayState = DASHBOARD;
lastInteraction = millis();
lastUpdate = 0; // force immediate dashboard redraw
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}
return;
}

// Timeout logic: blank screen after inactivity
if (screenTimeoutMS > 0 && (millis() - lastInteraction > screenTimeoutMS)) {
if (displayState != OFF) {
u8g2->setPowerSave(1);
displayState = OFF;
}
return;
}

// Allow display updates even with long LED strips, just rate-limited
if (strip.isUpdating() && (millis() - lastUpdate < 100)) return;

if (millis() - lastUpdate > 100) {
lastUpdate = millis();
if (displayState == OFF) {
u8g2->setPowerSave(0);
displayState = DASHBOARD;
}

u8g2->clearBuffer();

int currentX = flipDisplay ? (FB_WIDTH - PANEL_WIDTH - xOff) : xOff;
int currentY = flipDisplay ? (FB_HEIGHT - PANEL_HEIGHT - yOff) : yOff;

// Effect name
u8g2->setFont(u8g2_font_5x7_tf);
char lineBuffer[17] = "";
extractModeName(effectCurrent, JSON_mode_names, lineBuffer, 16);
u8g2->drawStr(currentX, currentY + NAME_BASELINE_Y, lineBuffer);
u8g2->drawHLine(currentX, currentY + NAME_RULE_Y, PANEL_WIDTH);

// Brightness history graph (scrolling)
for (int i = 0; i < PANEL_WIDTH - 1; i++) vValues[i] = vValues[i + 1];
vValues[PANEL_WIDTH - 1] = map(bri, 0, 255, 0, GRAPH_MAX_HEIGHT);
for (int i = 0; i < PANEL_WIDTH; i++) {
u8g2->drawLine(currentX + i, currentY + GRAPH_BASE_Y, currentX + i, currentY + GRAPH_BASE_Y - vValues[i]);
}

// Stats line: Speed, Intensity, Brightness
u8g2->setFont(u8g2_font_4x6_tf);
u8g2->setCursor(currentX, currentY + STATS_BASELINE_Y);
u8g2->print("S:"); u8g2->print(map(effectSpeed, 0, 255, 0, 99)); u8g2->print("% ");
u8g2->print("I:"); u8g2->print(map(effectIntensity, 0, 255, 0, 99)); u8g2->print("% ");
u8g2->print("B:"); u8g2->print(map(bri, 0, 255, 0, 100)); u8g2->print("%");

u8g2->sendBuffer();
}
}

void onStateChange(uint8_t mode) override {
if (!enabled || setupFailed || !initDone || !u8g2) return;
lastInteraction = millis();
lastUpdate = 0;
if (displayState == OFF) {
u8g2->setPowerSave(0);
displayState = DASHBOARD;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}

bool handleButton(uint8_t b) override {
if (!enabled || setupFailed || !initDone || !u8g2) return false;
if (b != buttonIndex) return false;
lastInteraction = millis();
if (displayState == OFF) {
u8g2->setPowerSave(0);
displayState = DASHBOARD;
}
return false; // let WLED core handle the actual button action
Comment thread
softhack007 marked this conversation as resolved.
Resolved
}

void addToConfig(JsonObject &root) override {
JsonObject top = root.createNestedObject(FPSTR(_name));
top[FPSTR(_enabled)] = enabled;
top[F("flipDisplay")] = flipDisplay;
top[F("x-offset")] = xOff;
top[F("y-offset")] = yOff;
top[F("sleepTimeout")] = screenTimeoutMS / 1000;
top[F("buttonIndex")] = buttonIndex;
top[F("ledPin")] = ledPin;
}

bool readFromConfig(JsonObject &root) override {
JsonObject top = root[FPSTR(_name)];
bool configComplete = !top.isNull();

bool oldEnabled = enabled;
configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled, true);
configComplete &= getJsonValue(top[F("flipDisplay")], flipDisplay, false);
configComplete &= getJsonValue(top[F("x-offset")], xOff, DEFAULT_X_OFFSET);
configComplete &= getJsonValue(top[F("y-offset")], yOff, DEFAULT_Y_OFFSET);
unsigned long timeoutSec = screenTimeoutMS / 1000;
configComplete &= getJsonValue(top[F("sleepTimeout")], timeoutSec, DEFAULT_SCREEN_TIMEOUT_S);
// clamp so the seconds -> ms conversion cannot wrap on 32-bit unsigned long
if (timeoutSec > MAX_SCREEN_TIMEOUT_S) timeoutSec = MAX_SCREEN_TIMEOUT_S;
screenTimeoutMS = timeoutSec * 1000;
configComplete &= getJsonValue(top[F("buttonIndex")], buttonIndex, DEFAULT_BUTTON_INDEX);

int8_t oldLedPin = ledPin;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
// read into a wider type so out-of-range values are not silently narrowed to int8_t
int32_t newLedPin = DEFAULT_HEARTBEAT_LED_PIN;
configComplete &= getJsonValue(top[F("ledPin")], newLedPin, DEFAULT_HEARTBEAT_LED_PIN);
if (newLedPin == -1 || (newLedPin >= 0 && newLedPin < WLED_NUM_PINS && PinManager::isPinOk(newLedPin, true))) {
ledPin = newLedPin;
} else {
configComplete = false; // invalid pin: keep the previous value
}

// Handle enabled -> disabled: tear down hardware
if (initDone && oldEnabled && !enabled) {
if (activeLedPin >= 0) {
pinMode(activeLedPin, INPUT);
PinManager::deallocatePin(activeLedPin, PinOwner::UM_Unspecified);
activeLedPin = -1;
}
if (u8g2) u8g2->setPowerSave(1);
displayState = OFF;
}

// Reallocate LED pin on config change (only when enabled)
if (enabled && initDone && (oldLedPin != ledPin || (ledPin >= 0 && activeLedPin < 0))) {
if (activeLedPin >= 0) {
pinMode(activeLedPin, INPUT);
PinManager::deallocatePin(activeLedPin, PinOwner::UM_Unspecified);
}
activeLedPin = -1;
if (ledPin >= 0 && PinManager::allocatePin(ledPin, true, PinOwner::UM_Unspecified)) {
activeLedPin = ledPin;
pinMode(activeLedPin, OUTPUT);
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

// Wake the panel so a settings change is visible, and so re-enabling a
// usermod that was blanked on disable does not wait for the timeout guard in loop().
if (enabled && !setupFailed && initDone && u8g2) {
lastInteraction = millis();
lastUpdate = 0;
if (displayState == OFF) {
u8g2->setPowerSave(0);
displayState = DASHBOARD;
}
}

if (initDone && u8g2) u8g2->setFlipMode(flipDisplay ? 1 : 0);
return configComplete;
}

void appendConfigData() override {
oappend(SET_F("addInfo('OLED_72x40:x-offset',1,'pixels');"));
oappend(SET_F("addInfo('OLED_72x40:y-offset',1,'pixels');"));
oappend(SET_F("addInfo('OLED_72x40:sleepTimeout',1,'seconds (0=never)');"));
oappend(SET_F("addInfo('OLED_72x40:buttonIndex',1,'WLED button index');"));
oappend(SET_F("addInfo('OLED_72x40:ledPin',1,'GPIO (-1=disable)');"));
}

uint16_t getId() override { return USERMOD_ID_OLED_72x40; }
};

const char UsermodOLED72x40::_name[] PROGMEM = "OLED_72x40";
const char UsermodOLED72x40::_enabled[] PROGMEM = "enabled";

static UsermodOLED72x40 oled_72x40;
REGISTER_USERMOD(oled_72x40);
Loading