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
307 changes: 307 additions & 0 deletions usermods/OLED_72x40/OLED_72x40.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,307 @@
#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:
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 = 60000;
int xOff = 28;
int yOff = 24;
byte vValues[72] = {};
bool flipDisplay = false;
int8_t buttonIndex = 0;
int8_t ledPin = 8;
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 LED 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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai what's the meaning of U8G2_R0 here?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@softhack007U8G2_R0 is the display rotation argument passed to the U8g2 constructor. Specifically, it means no rotation, landscape orientation. The full set of valid values is:

Value Meaning
U8G2_R0 No rotation, landscape
U8G2_R1 90° clockwise rotation
U8G2_R2 180° clockwise rotation
U8G2_R3 270° clockwise rotation
U8G2_MIRROR No rotation, landscape, display content mirrored (v2.6.x+)
U8G2_MIRROR_VERTICAL Display content vertically mirrored (v2.29.x+)

So in this usermod, U8G2_R0 starts the display in its default unrotated landscape mode. The 180° flip (for when the board is mounted upside down) is then handled separately at runtime via the flipDisplay config flag and U8g2's setFlipMode() call — which keeps the hardware rotation fixed while allowing a software-level flip without recreating the driver object.


🧠 Learnings used
Learnt from: softhack007
Repo: wled/WLED PR: 5355
File: wled00/util.cpp:635-638
Timestamp: 2026-02-07T16:06:08.677Z
Learning: PSRAM-related compilation guards should enable PSRAM code only for ESP32 variants that actually include PSRAM: ESP32-C61, ESP32-C5, and ESP32-P4. Exclude ESP32-C3, ESP32-C6, and ESP8266 from these guards. Apply this rule across the codebase (not just wled00/util.cpp) by reviewing and updating PSRAM guards/macros in all relevant files (C/C++ headers and sources).

Learnt from: softhack007
Repo: wled/WLED PR: 4838
File: lib/NeoESP32RmtHI/src/NeoEsp32RmtHIMethod.cpp:30-35
Timestamp: 2026-03-27T12:33:48.499Z
Learning: In C/C++ preprocessor conditionals (`#if`, `#elif`) GCC/Clang treat `&&` as short-circuit evaluated during preprocessing. This means guards like `#if defined(ARDUINO_ARCH_ESP32) && ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0)` are safe even if the macro/function-like macro on the RHS (e.g., `ESP_IDF_VERSION_VAL`) is not defined on some targets, because the RHS will not be expanded when the LHS is false (e.g., `defined(...)` evaluates to 0). During code review, avoid flagging such cases as “undefined function-like macro invocation” if they are protected by short-circuiting `defined(...) && ...`/`||` logic; some tools like cppcheck may not model this and can produce false positives. Also, don’t suggest refactoring that moves ESP32-specific includes/headers (e.g., `esp_idf_version.h`) outside of these guarded preprocessor blocks, since that will break targets (e.g., ESP8266) where the headers don’t exist.

Learnt from: softhack007
Repo: wled/WLED PR: 4843
File: usermods/elastic_collisions/Elastic_Collisions.cpp:1-1
Timestamp: 2026-05-06T22:10:27.517Z
Learning: In C/C++, do not flag precedence issues for expressions that mix additive operators (`+`, `-`) with shift operators (`<<`, `>>`) based on the assumption that shifts bind tighter. Per C/C++ operator precedence, `+`/`-` have higher precedence than `<<`/`>>` (e.g., `x - edge0 << 8` parses as `(x - edge0) << 8`). When reviewing WLED (and other) fixed-point/bit-manipulation code, confirm the intended parse using cppreference before reporting a precedence bug for mixed `-`/`+` and `<<`/`>>` expressions.

Learnt from: softhack007
Repo: wled/WLED PR: 5599
File: usermods/audioreactive/audio_reactive.cpp:1303-1308
Timestamp: 2026-05-11T16:00:13.574Z
Learning: When targeting arduino-esp32 v3.x (ESP-IDF 5.x), treat `WiFiUDP::flush()` as deprecated and ineffective for RX draining; it should not be used to clear/drain the UDP receive buffer. Instead, use `WiFiUDP::clear()`, which was introduced in arduino-esp32 v3.0 and is marked as the deprecated replacement via `NetworkUdp.h`.

If the code must compile and behave correctly across both arduino-esp32 v2.x (ESP-IDF < 5) and v3.x (ESP-IDF >= 5), use conditional compilation:
- for `ESP_IDF_VERSION_MAJOR < 5`: use `udp.flush()`
- for `ESP_IDF_VERSION_MAJOR >= 5`: use `udp.clear()`

Do not flag `WiFiUDP::clear()` usages for ESP-IDF >= 5 as non-existent—this is the documented correct replacement.

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.
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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

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

int textX = flipDisplay ? (128 - 72 - xOff) : xOff;
u8g2->setFont(u8g2_font_4x6_tf);
u8g2->setCursor(textX + 12, splashY + 38);
if (Network.isConnected()) u8g2->print(Network.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 (!Network.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.
}
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 ? (128 - 72 - xOff) : xOff;
int currentY = flipDisplay ? (64 - 40 - yOff) : yOff;

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

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

// Stats line: Speed, Intensity, Brightness
u8g2->setFont(u8g2_font_4x6_tf);
u8g2->setCursor(currentX, currentY + 38);
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.
}

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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be return true; ?

}

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, 28);
configComplete &= getJsonValue(top[F("y-offset")], yOff, 24);
unsigned long timeoutSec = screenTimeoutMS / 1000;
configComplete &= getJsonValue(top[F("sleepTimeout")], timeoutSec, 60UL);
screenTimeoutMS = timeoutSec * 1000;
configComplete &= getJsonValue(top[F("buttonIndex")], buttonIndex, (int8_t)0);

int8_t oldLedPin = ledPin;
configComplete &= getJsonValue(top[F("ledPin")], ledPin, (int8_t)8);

// 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.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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
Loading