Repository navigation
Usermod for ESP32-C3 Super Mini OLED 72x40, rebased to v0.16.x #5475
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
c6ff365
0a54ce5
d206276
28b1e3e
0ed2598
8e16f8e
2c867a0
d9c7868
590f29d
064fa50
faccc94
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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); | ||
|
|
||
| if (!u8g2->begin()) { | ||
| if (activeLedPin >= 0) { | ||
| pinMode(activeLedPin, INPUT); | ||
| PinManager::deallocatePin(activeLedPin, PinOwner::UM_Unspecified); | ||
| activeLedPin = -1; | ||
| } | ||
| delete u8g2; | ||
| u8g2 = nullptr; | ||
| setupFailed = true; | ||
| return; | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
|
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 | ||
|
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; | ||
| } | ||
|
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 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should this be
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, |
||
| } | ||
|
|
||
| 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); | ||
| } | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
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); | ||
There was a problem hiding this comment.
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_R0here?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@softhack007—U8G2_R0is the display rotation argument passed to the U8g2 constructor. Specifically, it means no rotation, landscape orientation. The full set of valid values is:U8G2_R0U8G2_R1U8G2_R2U8G2_R3U8G2_MIRRORU8G2_MIRROR_VERTICALSo in this usermod,
U8G2_R0starts 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 theflipDisplayconfig flag and U8g2'ssetFlipMode()call — which keeps the hardware rotation fixed while allowing a software-level flip without recreating the driver object.🧠 Learnings used
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
U8G2_R0is U8g2's rotation argument (no rotation, landscape). The 180° case is handled at runtime throughflipDisplay→setFlipMode(), so the constructor rotation stays fixed. Note the driver is the generic 128x64 SSD1306 one; the 72x40 panel is positioned inside that framebuffer via the x/y offsets. I can add a short comment at the constructor call if that would help readers.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.