diff --git a/usermods/usermod_v2_gc9a01_display/library.json b/usermods/usermod_v2_gc9a01_display/library.json new file mode 100644 index 0000000000..f4e6908ba7 --- /dev/null +++ b/usermods/usermod_v2_gc9a01_display/library.json @@ -0,0 +1,8 @@ +{ + "name": "gc9a01_display", + "build": { "libArchive": false }, + "dependencies": { + "bodmer/TFT_eSPI": "~2.5.43", + "SPI": "" + } +} diff --git a/usermods/usermod_v2_gc9a01_display/platformio_override.sample.ini b/usermods/usermod_v2_gc9a01_display/platformio_override.sample.ini new file mode 100644 index 0000000000..9ef9aea717 --- /dev/null +++ b/usermods/usermod_v2_gc9a01_display/platformio_override.sample.ini @@ -0,0 +1,46 @@ +; GC9A01 Round Display usermod — sample build configuration +; Copy/rename to platformio_override.ini and adjust pin numbers for your board. +; +; All SPI pins MUST be set at compile time (TFT_eSPI requirement). +; CS / DC / RST / BL can also be changed here freely. +; +; Default pin assignments below are for a generic ESP32 DevKit: +; MOSI = GPIO 23 SCLK = GPIO 18 +; CS = GPIO 15 DC = GPIO 2 +; RST = GPIO 4 BL = GPIO 32 (set -1 if backlight is always on) + +[env:esp32dev_gc9a01] +extends = env:esp32dev +lib_deps = ${esp32dev.lib_deps} + bodmer/TFT_eSPI @ ~2.5.43 +build_flags = ${esp32dev.build_flags} + ; ---- Enable the usermod ---- + -D USERMOD_GC9A01_DISPLAY + + ; ---- TFT_eSPI configuration ---- + -D USER_SETUP_LOADED=1 + -D USER_SETUP_ID=1 + -D GC9A01_DRIVER=1 + -D TFT_WIDTH=240 + -D TFT_HEIGHT=240 + + ; ---- SPI data / clock (shared bus) ---- + -D TFT_MOSI=23 + -D TFT_SCLK=18 + -D TFT_MISO=-1 + + ; ---- Display-specific pins ---- + -D TFT_CS=15 + -D TFT_DC=2 + -D TFT_RST=4 + -D TFT_BL=32 + -D TFT_BACKLIGHT_ON=HIGH + + ; ---- Font / feature selection ---- + -D LOAD_GLCD=1 + -D LOAD_FONT2=1 + -D LOAD_FONT4=1 + -D SMOOTH_FONT=1 + + ; ---- SPI clock speed ---- + -D SPI_FREQUENCY=40000000 diff --git a/usermods/usermod_v2_gc9a01_display/readme.md b/usermods/usermod_v2_gc9a01_display/readme.md new file mode 100644 index 0000000000..f4a677f332 --- /dev/null +++ b/usermods/usermod_v2_gc9a01_display/readme.md @@ -0,0 +1,73 @@ +# GC9A01 Round Display Usermod + +Displays WLED state on a **1.28" round IPS LCD** (240×240, GC9A01 driver, 4-wire SPI). +Tested with the Waveshare 1.28" Round LCD Module. + +## Watch-face layout + +``` + ╭──────────────────╮ + ╱ │ ░░░░░░░░░░░░░░ │ ╲ ← brightness arc (yellow, 0–360°) + ╱ │ [Effect Name] │ ╲ + │ │ ╭────────────╮ │ │ + │ │ │ │ │ │ ← colour disc (primary segment colour) + │ │ │ 75 % │ │ │ + │ │ ╰────────────╯ │ │ + ╲ │ 192.168.1.10 │ ╱ + ╲ │ │ ╱ + ╰──────────────────╯ +``` + +| Element | Content | +|---|---| +| Outer arc | Brightness 0-100 % (yellow on dark grey ring) | +| Centre disc | Primary colour of the main segment | +| Disc text | Brightness percentage (contrast-adaptive colour) | +| Upper text | Current effect name | +| Lower text | IP address / AP mode address / "No WiFi" | + +## Wiring + +| Display | ESP32 (default) | +|---|---| +| VCC | 3.3 V | +| GND | GND | +| DIN (MOSI) | GPIO 23 | +| CLK | GPIO 18 | +| CS | GPIO 15 | +| DC | GPIO 2 | +| RST | GPIO 4 | +| BL | GPIO 32 | + +## Building + +Copy `platformio_override.sample.ini` to `platformio_override.ini` and adjust pins for your board. Then build the `esp32dev_gc9a01` environment: + +```bash +pio run -e esp32dev_gc9a01 +``` + +All eight pin values **must** be set as build flags — TFT_eSPI does not support runtime pin selection. + +## Configuration (WLED Settings → Usermods) + +| Option | Default | Description | +|---|---|---| +| `enabled` | true | Enable/disable the display | +| `screenTimeoutSec` | 300 | Seconds of inactivity before display blanks (0 = never) | +| `flip` | false | Rotate display 180° | + +The four pin fields shown in the UI are read-only — they reflect the compile-time values. + +## Public API (for pairing with other usermods) + +```cpp +GC9A01DisplayUsermod *disp = + (GC9A01DisplayUsermod *)UsermodManager::lookup(USERMOD_ID_GC9A01_DISPLAY); + +if (disp) { + disp->wakeDisplay(); // wake from sleep; returns true if was off + disp->showOverlay("Brightness", "75%", 1500); // 2-line overlay for 1.5 s + disp->forceRedraw(); // immediate full refresh +} +``` diff --git a/usermods/usermod_v2_gc9a01_display/usermod_v2_gc9a01_display.cpp b/usermods/usermod_v2_gc9a01_display/usermod_v2_gc9a01_display.cpp new file mode 100644 index 0000000000..ecae1a82b4 --- /dev/null +++ b/usermods/usermod_v2_gc9a01_display/usermod_v2_gc9a01_display.cpp @@ -0,0 +1,459 @@ +#include "wled.h" + +// +// GC9A01 Round Display Usermod +// +// Adds support for the Waveshare 1.28" Round LCD Module (and compatible +// GC9A01-based 240×240 IPS displays) as a WLED status screen. +// +// Uses TFT_eSPI (Bodmer) which must be configured via build flags — +// see platformio_override.sample.ini for a ready-to-use example. +// +// Displays: +// • Outer ring : brightness progress arc +// • Centre fill : primary segment colour +// • Top text : current effect name +// • Centre text : brightness percentage +// • Bottom text : IP address (or "No WiFi") +// +// Public API (for pairing with a rotary encoder or other usermods): +// wakeDisplay() — wake from timeout, returns true if was sleeping +// showOverlay(...) — display temporary text over the watch face +// forceRedraw() — request immediate full refresh +// + +#ifdef USERMOD_GC9A01_DISPLAY + +#include +#include + +// Verify required build flags are present +#ifndef USER_SETUP_LOADED + #ifndef GC9A01_DRIVER + #error "GC9A01_DISPLAY: define GC9A01_DRIVER=1 in build flags" + #endif + #ifndef TFT_WIDTH + #error "GC9A01_DISPLAY: define TFT_WIDTH=240 in build flags" + #endif + #ifndef TFT_HEIGHT + #error "GC9A01_DISPLAY: define TFT_HEIGHT=240 in build flags" + #endif + #ifndef TFT_CS + #error "GC9A01_DISPLAY: define TFT_CS in build flags" + #endif + #ifndef TFT_DC + #error "GC9A01_DISPLAY: define TFT_DC in build flags" + #endif + #ifndef TFT_RST + #error "GC9A01_DISPLAY: define TFT_RST in build flags" + #endif +#endif + +#ifndef TFT_BL + #define TFT_BL -1 +#endif + +// How often the loop checks for state changes (ms) +#define GC9A01_REFRESH_MS 500 +// Screen off after this many ms without a state change +#define GC9A01_TIMEOUT_MS (5UL * 60UL * 1000UL) // 5 minutes +// Overlay display duration default (ms) +#define GC9A01_OVERLAY_MS 2000 +// Max chars for effect/palette name display +#define GC9A01_NAME_BUF 24 + +// Display geometry +#define GC9A01_CX 120 // centre X +#define GC9A01_CY 120 // centre Y +#define GC9A01_R 120 // display radius +// Brightness arc: outer radius, inner radius +#define GC9A01_ARC_R_OUT 115 +#define GC9A01_ARC_R_IN 104 + +// ------------------------------------------------------------------------- +// File-scope TFT object (must be file-scope so build flags apply) +// ------------------------------------------------------------------------- +TFT_eSPI tft = TFT_eSPI(TFT_WIDTH, TFT_HEIGHT); + +// ------------------------------------------------------------------------- + +class GC9A01DisplayUsermod : public Usermod { + + private: + + bool enabled = true; + bool initDone = false; + bool displayOff = false; + bool needRedraw = true; + + // For state-change detection + uint8_t knownBri = 0; + uint8_t knownMode = 255; + uint32_t knownColor = 0; + bool knownWifi = false; + IPAddress knownIp; + + unsigned long lastCheck = 0; + unsigned long lastChange = 0; // millis of last detected state change + + // Overlay support + bool overlayActive = false; + unsigned long overlayUntil = 0; + char overlayLine1[GC9A01_NAME_BUF] = {0}; + char overlayLine2[GC9A01_NAME_BUF] = {0}; + + // Config + uint16_t screenTimeoutSec = 300; // 5 minutes + bool flipDisplay = false; + + static const char _name[]; + static const char _enabled[]; + static const char _timeout[]; + static const char _flip[]; + + // ---------------------------------------------------------------- helpers + + // Copy TFT_MOSI / TFT_SCLK compile-time pins into WLED's global SPI vars + // so PinManager and the web UI see them (same pattern as pixels_dice_tray). + static void setSPIPinsFromMacros() { +#ifdef TFT_MOSI + spi_mosi = TFT_MOSI; +#endif +#ifdef TFT_MISO + #if defined(TFT_MOSI) && (TFT_MISO == TFT_MOSI) + spi_miso = -1; // shared data line — not a real MISO + #else + spi_miso = TFT_MISO; + #endif +#endif +#ifdef TFT_SCLK + spi_sclk = TFT_SCLK; +#endif + } + + // Convert WLED 0xRRGGBB(WW) to TFT RGB565 + uint16_t wledToRGB565(uint32_t color) { + uint8_t r = (color >> 16) & 0xFF; + uint8_t g = (color >> 8) & 0xFF; + uint8_t b = color & 0xFF; + return tft.color565(r, g, b); + } + + void setBacklight(bool on) { + if (TFT_BL < 0) return; +#ifndef TFT_BACKLIGHT_ON + #define TFT_BACKLIGHT_ON HIGH +#endif + digitalWrite(TFT_BL, on ? TFT_BACKLIGHT_ON : (TFT_BACKLIGHT_ON == HIGH ? LOW : HIGH)); + } + + // ---------------------------------------------------------------- drawing + + // Fill the circular display background with a solid colour. + void fillDisplay(uint32_t color) { + tft.fillCircle(GC9A01_CX, GC9A01_CY, GC9A01_R, wledToRGB565(color)); + } + + // Draw the brightness arc (0° = 12 o'clock, clockwise). + // TFT_eSPI drawArc: angle 0 = top, clockwise. + void drawBrightnessArc(uint8_t brightness) { + // Background ring + tft.drawArc(GC9A01_CX, GC9A01_CY, + GC9A01_ARC_R_OUT, GC9A01_ARC_R_IN, + 0, 360, + 0x2104 /*dark grey*/, TFT_BLACK); + if (brightness > 0) { + uint16_t endAngle = (uint16_t)((uint32_t)brightness * 360 / 255); + if (endAngle == 0) endAngle = 1; + tft.drawArc(GC9A01_CX, GC9A01_CY, + GC9A01_ARC_R_OUT, GC9A01_ARC_R_IN, + 0, endAngle, + TFT_YELLOW, TFT_BLACK); + } + } + + // Draw the primary-colour filled centre circle. + void drawColorDisk(uint32_t color) { + uint16_t c565 = wledToRGB565(color); + // 80 px radius gives a nice centre disc with room for text + tft.fillCircle(GC9A01_CX, GC9A01_CY, 80, c565); + // Thin border between disc and text area + tft.drawCircle(GC9A01_CX, GC9A01_CY, 80, TFT_BLACK); + } + + // Centered text helper — writes text centered at (cx, y). + void drawCenteredText(const char *text, int16_t y, + uint8_t textSize, uint32_t color) { + tft.setTextSize(textSize); + tft.setTextDatum(TC_DATUM); + tft.setTextColor(color, TFT_BLACK); + tft.drawString(text, GC9A01_CX, y); + } + + // Full watch-face redraw. + void drawWatchFace() { + uint32_t primaryColor = RGBW32(colPri[0], colPri[1], colPri[2], colPri[3]); + + // Black background + tft.fillScreen(TFT_BLACK); + + // Outer brightness ring + drawBrightnessArc(bri); + + // Coloured centre disc + drawColorDisk(primaryColor); + + // Effect name — white text above centre + char modeName[GC9A01_NAME_BUF]; + extractModeName(effectCurrent, JSON_mode_names, modeName, GC9A01_NAME_BUF - 1); + tft.setTextColor(TFT_WHITE, TFT_BLACK); + tft.setTextDatum(TC_DATUM); + tft.setTextSize(2); + tft.drawString(modeName, GC9A01_CX, 54); + + // Brightness % — large, centred on the disc + char briBuf[8]; + snprintf_P(briBuf, sizeof(briBuf), PSTR("%d%%"), (bri * 100 + 127) / 255); + + // Pick a contrasting text colour based on primary colour brightness + uint8_t luma = (77 * colPri[0] + 150 * colPri[1] + 29 * colPri[2]) >> 8; + uint16_t textCol = (luma > 128) ? TFT_BLACK : TFT_WHITE; + tft.setTextDatum(MC_DATUM); + tft.setTextColor(textCol); + tft.setTextSize(3); + tft.drawString(briBuf, GC9A01_CX, GC9A01_CY); + + // IP address — small, bottom of display + char ipBuf[20]; + if (WLED_CONNECTED) { + IPAddress ip = Network.localIP(); + snprintf_P(ipBuf, sizeof(ipBuf), PSTR("%d.%d.%d.%d"), + ip[0], ip[1], ip[2], ip[3]); + } else if (apActive) { + strlcpy_P(ipBuf, PSTR("AP 4.3.2.1"), sizeof(ipBuf)); + } else { + strlcpy_P(ipBuf, PSTR("No WiFi"), sizeof(ipBuf)); + } + tft.setTextDatum(BC_DATUM); + tft.setTextColor(TFT_DARKGREY); + tft.setTextSize(1); + tft.drawString(ipBuf, GC9A01_CX, 195); + } + + // Overlay: two lines of text in a rounded rect, auto-cleared after duration. + void drawOverlay() { + tft.fillRoundRect(30, 85, 180, 70, 12, TFT_NAVY); + tft.drawRoundRect(30, 85, 180, 70, 12, TFT_WHITE); + tft.setTextDatum(MC_DATUM); + tft.setTextColor(TFT_WHITE); + tft.setTextSize(2); + tft.drawString(overlayLine1, GC9A01_CX, 108); + if (overlayLine2[0]) { + tft.setTextSize(1); + tft.drawString(overlayLine2, GC9A01_CX, 135); + } + } + + // WLED splash screen shown once at boot. + void drawSplash() { + tft.fillScreen(TFT_BLACK); + tft.setTextDatum(MC_DATUM); + tft.setTextColor(TFT_ORANGE); + tft.setTextSize(4); + tft.drawString("WLED", GC9A01_CX, GC9A01_CY - 20); + tft.setTextColor(TFT_DARKGREY); + tft.setTextSize(1); + tft.drawString(versionString, GC9A01_CX, GC9A01_CY + 25); + } + + public: + + uint16_t getId() override { return USERMOD_ID_GC9A01_DISPLAY; } + + // ------------------------------------------------------ public API + + // Wake the display from timeout sleep. + // Returns true if the display was sleeping (so callers can discard input). + bool wakeDisplay() { + if (!enabled || !initDone) return false; + if (displayOff) { + setBacklight(true); + displayOff = false; + needRedraw = true; + lastChange = millis(); + return true; + } + return false; + } + + // Show a temporary two-line overlay over the watch face. + void showOverlay(const char *line1, const char *line2 = nullptr, + uint32_t durationMs = GC9A01_OVERLAY_MS) { + if (!enabled || !initDone) return; + wakeDisplay(); + strlcpy(overlayLine1, line1 ? line1 : "", sizeof(overlayLine1)); + strlcpy(overlayLine2, line2 ? line2 : "", sizeof(overlayLine2)); + overlayUntil = millis() + durationMs; + overlayActive = true; + drawOverlay(); + } + + // Request an immediate full redraw on the next loop tick. + void forceRedraw() { + needRedraw = true; + lastChange = millis(); + } + + // ------------------------------------------------------ lifecycle + + void setup() override { + DEBUG_PRINTLN(F("GC9A01 Display: init.")); + + setSPIPinsFromMacros(); + + // Register shared SPI bus pins + PinManagerPinType spiPins[] = { + { spi_mosi, true }, { spi_miso, false }, { spi_sclk, true } + }; + if (spi_sclk >= 0 && !PinManager::allocateMultiplePins(spiPins, 3, PinOwner::HW_SPI)) { + DEBUG_PRINTLN(F("GC9A01 Display: SPI pin allocation failed, disabling.")); + enabled = false; + return; + } + + // Register display-specific pins (CS, DC, RST, BL) + PinManagerPinType dispPins[] = { + { TFT_CS, true }, { TFT_DC, true }, { TFT_RST, true }, { TFT_BL, true } + }; + if (!PinManager::allocateMultiplePins(dispPins, 4, PinOwner::UM_GC9A01Display)) { + PinManager::deallocateMultiplePins(spiPins, 3, PinOwner::HW_SPI); + DEBUG_PRINTLN(F("GC9A01 Display: display pin allocation failed, disabling.")); + enabled = false; + return; + } + + tft.init(); + tft.setRotation(flipDisplay ? 2 : 0); + tft.fillScreen(TFT_BLACK); + + if (TFT_BL >= 0) { + pinMode(TFT_BL, OUTPUT); + setBacklight(true); + } + + drawSplash(); + delay(1200); + + lastChange = millis(); + lastCheck = millis(); + initDone = true; + } + + void loop() override { + if (!enabled || !initDone) return; + + unsigned long now = millis(); + if (now - lastCheck < GC9A01_REFRESH_MS) return; + lastCheck = now; + + // Clear overlay once it expires + if (overlayActive && now >= overlayUntil) { + overlayActive = false; + needRedraw = true; + } + + // Detect state changes + uint32_t curColor = RGBW32(colPri[0], colPri[1], colPri[2], colPri[3]); + bool curWifi = WLED_CONNECTED || apActive; + if (bri != knownBri || + effectCurrent != knownMode || + curColor != knownColor || + curWifi != knownWifi || + (curWifi && Network.localIP() != knownIp)) { + needRedraw = true; + lastChange = now; + knownBri = bri; + knownMode = effectCurrent; + knownColor = curColor; + knownWifi = curWifi; + knownIp = curWifi ? Network.localIP() : IPAddress(0,0,0,0); + } + + // Screen timeout + unsigned long timeoutMs = (unsigned long)screenTimeoutSec * 1000UL; + if (!displayOff && (now - lastChange > timeoutMs)) { + setBacklight(false); + tft.fillScreen(TFT_BLACK); + displayOff = true; + needRedraw = false; + return; + } + + if (displayOff || !needRedraw || overlayActive) return; + + needRedraw = false; + drawWatchFace(); + } + + // ------------------------------------------------------ config + + void addToConfig(JsonObject &root) override { + JsonObject top = root.createNestedObject(FPSTR(_name)); + top[FPSTR(_enabled)] = enabled; + top[FPSTR(_timeout)] = screenTimeoutSec; + top[FPSTR(_flip)] = flipDisplay; + // Expose compile-time pin values so the UI can show them (read-only) + JsonArray pins = top.createNestedArray("pin"); + pins.add(TFT_CS); + pins.add(TFT_DC); + pins.add(TFT_RST); + pins.add(TFT_BL); + } + + void appendConfigData() override { + // Show pin labels (read-only — pins are compile-time constants) + oappend(F("addInfo('GC9A01-Display:pin[]',0,'','CS (compile-time)');")); + oappend(F("addInfo('GC9A01-Display:pin[]',1,'','DC (compile-time)');")); + oappend(F("addInfo('GC9A01-Display:pin[]',2,'','RST (compile-time)');")); + oappend(F("addInfo('GC9A01-Display:pin[]',3,'','BL (compile-time)');")); + } + + bool readFromConfig(JsonObject &root) override { + JsonObject top = root[FPSTR(_name)]; + if (top.isNull()) return false; + bool en = top[FPSTR(_enabled)] | enabled; + if (en != enabled) { + enabled = en; + if (!enabled && TFT_BL >= 0) setBacklight(false); + } + screenTimeoutSec = top[FPSTR(_timeout)] | screenTimeoutSec; + bool newFlip = top[FPSTR(_flip)] | flipDisplay; + if (newFlip != flipDisplay && initDone) { + flipDisplay = newFlip; + tft.setRotation(flipDisplay ? 2 : 0); + needRedraw = true; + } else { + flipDisplay = newFlip; + } + return !top[FPSTR(_timeout)].isNull(); + } + + void addToJsonInfo(JsonObject &root) override { + JsonObject user = root["u"]; + if (user.isNull()) user = root.createNestedObject("u"); + JsonArray arr = user.createNestedArray(F("GC9A01")); + arr.add(enabled ? F("active") : F("disabled")); + } +}; + + +const char GC9A01DisplayUsermod::_name[] PROGMEM = "GC9A01-Display"; +const char GC9A01DisplayUsermod::_enabled[] PROGMEM = "enabled"; +const char GC9A01DisplayUsermod::_timeout[] PROGMEM = "screenTimeoutSec"; +const char GC9A01DisplayUsermod::_flip[] PROGMEM = "flip"; + + +static GC9A01DisplayUsermod gc9a01_display; +REGISTER_USERMOD(gc9a01_display); + +#endif // USERMOD_GC9A01_DISPLAY diff --git a/usermods/usermod_v2_i2c_encoder_ui/readme.md b/usermods/usermod_v2_i2c_encoder_ui/readme.md new file mode 100644 index 0000000000..4d66aa6837 --- /dev/null +++ b/usermods/usermod_v2_i2c_encoder_ui/readme.md @@ -0,0 +1,63 @@ +# I2C Rotary Encoder UI + +A WLED usermod that provides the same rotary-encoder-driven UI as the **Rotary Encoder UI** usermod, but for the **M5Stack Encoder Unit** (UNIT-ENCODER, SKU: U135) — an I2C device that handles quadrature decoding internally. + +No encoder GPIO pins are needed. The encoder communicates over the I2C bus configured globally in **WLED Settings → Hardware**. + +## Compatible Hardware + +| Device | I2C Address | Notes | +|---|---|---| +| M5Stack Encoder Unit (U135) | 0x40 (default) | Verified against V1 firmware protocol | + +## Controls + +| Action | Effect | +|---|---| +| Rotate | Adjust the current parameter | +| Short press | Cycle to the next parameter | +| Double press | Toggle LEDs on/off | +| Hold 3 s | Show network info (requires FourLineDisplay usermod) | + +## Parameters cycled by button press + +Without FourLineDisplay: Brightness → Effect Speed → Effect Intensity → Color Palette → Effect +With FourLineDisplay: adds Main Color (hue) → Saturation → CCT → Preset → Custom 1/2/3 + +CCT only appears when the active segment supports it. Preset only appears when preset-low and preset-high are both set. + +## Setup + +1. In **WLED Settings → Hardware**, set the I2C SDA and SCL pins for your board. +2. Connect the M5Stack Encoder Unit to those pins (and 3.3 V / GND). +3. Enable this usermod in **WLED Settings → Usermods**. +4. The default I2C address is **64 (0x40)**. Change it if you have re-soldered the address pads. + +## Configuration options + +| Option | Default | Description | +|---|---|---| +| enabled | true | Enable or disable the usermod | +| i2c-address | 64 (0x40) | I2C address of the encoder unit (decimal) | +| preset-low | 0 | Lower bound of preset cycling range | +| preset-high | 0 | Upper bound of preset cycling range (0 disables preset mode) | +| apply-2-all-seg | true | Apply changes to all active segments, not just the main one | + +## Building + +Add to your `platformio_override.ini` build environment: + +```ini +build_flags = + -D USERMOD_I2C_ENCODER_UI +``` + +Or enable it from the WLED usermod settings page if your build includes it by default. + +Optionally pair with FourLineDisplay for the full set of controls: + +```ini +build_flags = + -D USERMOD_I2C_ENCODER_UI + -D USERMOD_FOUR_LINE_DISPLAY +``` diff --git a/usermods/usermod_v2_i2c_encoder_ui/usermod_v2_i2c_encoder_ui.cpp b/usermods/usermod_v2_i2c_encoder_ui/usermod_v2_i2c_encoder_ui.cpp new file mode 100644 index 0000000000..8d4b329a32 --- /dev/null +++ b/usermods/usermod_v2_i2c_encoder_ui/usermod_v2_i2c_encoder_ui.cpp @@ -0,0 +1,726 @@ +#include "wled.h" + +// +// I2C Rotary Encoder UI Usermod +// +// Functionally equivalent to the GPIO Rotary Encoder UI usermod +// (usermod_v2_rotary_encoder_ui_ALT), but uses an I2C rotary encoder +// instead of a 5-pin GPIO encoder. +// +// Designed for the M5Stack Encoder Unit (UNIT-ENCODER, SKU: U135). +// I2C address: 0x40 (default). No encoder GPIO pins are needed — +// configure I2C SDA/SCL globally in the WLED Hardware settings page. +// +// M5Stack Encoder Unit register map: +// 0x00 : Mode (0=Pulse, 1=AB) — write 0x00 to initialise +// 0x10 : Encoder count low byte +// 0x11 : Encoder count high byte (int16_t, little-endian) +// 0x20 : Button (0=Released, 1=Pressed) +// +// Controls (same as GPIO variant): +// Rotate : adjust the currently selected parameter +// Press : cycle through parameter modes +// 2×Press : toggle on/off +// Hold 3s : show network info overlay (requires FourLineDisplay) +// + +#ifdef USERMOD_FOUR_LINE_DISPLAY +#include "usermod_v2_four_line_display.h" +#endif + +#ifdef USERMOD_MODE_SORT + #error "Usermod Mode Sort is no longer required. Remove -D USERMOD_MODE_SORT from platformio.ini" +#endif + +#ifndef I2C_ENCODER_POLL_MS +#define I2C_ENCODER_POLL_MS 10 // poll every 10 ms (100 Hz) +#endif + +// With FourLineDisplay: Brightness/Speed/Intensity/Palette/Effect/Hue/Sat/CCT/Preset/Cx3 +// Without : Brightness/Speed/Intensity/Palette/Effect only +#ifdef USERMOD_FOUR_LINE_DISPLAY + #define I2CENC_LAST_UI_STATE 11 +#else + #define I2CENC_LAST_UI_STATE 4 +#endif + +#define I2CENC_MODE_SORT_SKIP_COUNT 1 + +// File-static helpers for qsort comparator (internal linkage, no conflict with other mods) +static const char **i2cenc_listBeingSorted; + +static int i2cenc_qstringCmp(const void *ap, const void *bp) { + const char *a = i2cenc_listBeingSorted[*((byte *)ap)]; + const char *b = i2cenc_listBeingSorted[*((byte *)bp)]; + int i = 0; + do { + char aVal = pgm_read_byte_near(a + i); + if (aVal >= 97 && aVal <= 122) aVal -= 32; + char bVal = pgm_read_byte_near(b + i); + if (bVal >= 97 && bVal <= 122) bVal -= 32; + if (aVal == '"' || bVal == '"' || aVal == '\0' || bVal == '\0') { + if (aVal == bVal) return 0; + else if (aVal == '"' || aVal == '\0') return -1; + else return 1; + } + if (aVal == bVal) { i++; continue; } + return (aVal < bVal) ? -1 : 1; + } while (true); + return 0; +} + + +class I2CEncoderUIUsermod : public Usermod { + + private: + + const int8_t fadeAmount; // step size for most parameters + unsigned long loopTime; + + unsigned long buttonPressedTime; + unsigned long buttonWaitTime; + bool buttonPressedBefore; + bool buttonLongPressed; + + uint8_t i2cAddress; // I2C address of the encoder unit + uint16_t lastRaw; // last raw count read from device + bool deviceFound; // set true when encoder responds at setup + + unsigned char select_state; // which parameter we are currently editing + + uint16_t currentHue1; + byte currentSat1; + uint8_t currentCCT; + + #ifdef USERMOD_FOUR_LINE_DISPLAY + FourLineDisplayUsermod *display; + #else + void *display; + #endif + + const char **modes_qstrings; + byte *modes_alpha_indexes; + const char **palettes_qstrings; + byte *palettes_alpha_indexes; + + bool currentEffectAndPaletteInitialized; + uint8_t effectCurrentIndex; + uint8_t effectPaletteIndex; + + byte presetHigh; + byte presetLow; + bool applyToAll; + bool initDone; + bool enabled; + + static const char _name[]; + static const char _enabled[]; + static const char _i2cAddress[]; + static const char _presetHigh[]; + static const char _presetLow[]; + static const char _applyToAll[]; + + // ------------------------------------------------------------------ I2C + + // Write a single byte to a register. Returns true on success. + bool writeReg(uint8_t reg, uint8_t value) { + Wire.beginTransmission(i2cAddress); + Wire.write(reg); + Wire.write(value); + return Wire.endTransmission() == 0; + } + + // Read the encoder count as a signed 16-bit delta from the last reading. + // Returns 0 on I2C error. + int16_t readEncoderDelta() { + Wire.beginTransmission(i2cAddress); + Wire.write(0x10); + if (Wire.endTransmission(false) != 0) return 0; + if (Wire.requestFrom((uint8_t)i2cAddress, (uint8_t)2) < 2) return 0; + uint16_t raw = (uint16_t)Wire.read() | ((uint16_t)Wire.read() << 8); + int16_t delta = (int16_t)(raw - lastRaw); + lastRaw = raw; + return delta; + } + + // Read the button state. Returns true when the button is pressed. + bool readButtonState() { + Wire.beginTransmission(i2cAddress); + Wire.write(0x20); + if (Wire.endTransmission(false) != 0) return false; + if (Wire.requestFrom((uint8_t)i2cAddress, (uint8_t)1) < 1) return false; + return Wire.read() == 1; // 0=Released, 1=Pressed + } + + // --------------------------------------------------------- sorting helpers + + void sortModesAndPalettes() { + modes_qstrings = strip.getModeDataSrc(); + modes_alpha_indexes = re_initIndexArray(strip.getModeCount()); + re_sortModes(modes_qstrings, modes_alpha_indexes, strip.getModeCount(), I2CENC_MODE_SORT_SKIP_COUNT); + + palettes_qstrings = re_findModeStrings(JSON_palette_names, getPaletteCount()); + palettes_alpha_indexes = re_initIndexArray(getPaletteCount()); + if (customPalettes.size()) { + for (int i = 0; i < (int)customPalettes.size(); i++) { + palettes_alpha_indexes[FIXED_PALETTE_COUNT + i] = 255 - i; + palettes_qstrings[FIXED_PALETTE_COUNT + i] = PSTR("~Custom~"); + } + } + int skipPaletteCount = 1; + while (pgm_read_byte_near(palettes_qstrings[skipPaletteCount]) == '*') skipPaletteCount++; + re_sortModes(palettes_qstrings, palettes_alpha_indexes, FIXED_PALETTE_COUNT, skipPaletteCount); + } + + byte *re_initIndexArray(int numModes) { + byte *indexes = (byte *)malloc(sizeof(byte) * numModes); + for (unsigned i = 0; i < (unsigned)numModes; i++) indexes[i] = i; + return indexes; + } + + const char **re_findModeStrings(const char json[], int numModes) { + const char **modeStrings = (const char **)malloc(sizeof(const char *) * numModes); + uint8_t modeIndex = 0; + bool insideQuotes = false; + bool complete = false; + for (size_t i = 0; i < strlen_P(json); i++) { + char c = pgm_read_byte_near(json + i); + if (c == '\0') break; + switch (c) { + case '"': + insideQuotes = !insideQuotes; + if (insideQuotes) modeStrings[modeIndex] = (char *)(json + i + 1); + break; + case ']': + if (!insideQuotes) complete = true; + break; + case ',': + if (!insideQuotes) modeIndex++; + break; + default: break; + } + if (complete) break; + } + return modeStrings; + } + + void re_sortModes(const char **modeNames, byte *indexes, int count, int numSkip) { + if (!modeNames) return; + i2cenc_listBeingSorted = modeNames; + qsort(indexes + numSkip, count - numSkip, sizeof(byte), i2cenc_qstringCmp); + i2cenc_listBeingSorted = nullptr; + } + + public: + + I2CEncoderUIUsermod() + : fadeAmount(5) + , loopTime(0) + , buttonPressedTime(0) + , buttonWaitTime(0) + , buttonPressedBefore(false) + , buttonLongPressed(false) + , i2cAddress(0x40) + , lastRaw(0) + , deviceFound(false) + , select_state(0) + , currentHue1(16) + , currentSat1(255) + , currentCCT(128) + , display(nullptr) + , modes_qstrings(nullptr) + , modes_alpha_indexes(nullptr) + , palettes_qstrings(nullptr) + , palettes_alpha_indexes(nullptr) + , currentEffectAndPaletteInitialized(false) + , effectCurrentIndex(0) + , effectPaletteIndex(0) + , presetHigh(0) + , presetLow(0) + , applyToAll(true) + , initDone(false) + , enabled(true) + {} + + uint16_t getId() override { return USERMOD_ID_I2C_ENCODER_UI; } + + void setup() override { + DEBUG_PRINTLN(F("I2C Encoder UI: init.")); + + if (i2c_sda < 0 || i2c_scl < 0) { + DEBUG_PRINTLN(F("I2C Encoder UI: I2C pins not configured, disabling.")); + enabled = false; + return; + } + + // Set Pulse mode and verify device is present. + // A failed write means wrong address or not connected — keep enabled so the + // user can correct the address in settings without rebooting. + if (!writeReg(0x00, 0x00)) { + DEBUG_PRINTLN(F("I2C Encoder UI: device not found at address.")); + deviceFound = false; + return; + } + deviceFound = true; + + // Read baseline count so first delta is 0 + Wire.beginTransmission(i2cAddress); + Wire.write(0x10); + Wire.endTransmission(false); + if (Wire.requestFrom((uint8_t)i2cAddress, (uint8_t)2) >= 2) { + lastRaw = (uint16_t)Wire.read() | ((uint16_t)Wire.read() << 8); + } + + currentCCT = (approximateKelvinFromRGB(RGBW32(colPri[0], colPri[1], colPri[2], colPri[3])) - 1900) >> 5; + + if (!initDone) sortModesAndPalettes(); + + #ifdef USERMOD_FOUR_LINE_DISPLAY + display = (FourLineDisplayUsermod *)UsermodManager::lookup(USERMOD_ID_FOUR_LINE_DISP); + if (display != nullptr) display->setMarkLine(1, 0); + #endif + + loopTime = millis(); + initDone = true; + } + + void loop() override { + if (!enabled || !deviceFound) return; + + unsigned long currentTime = millis(); + if (strip.isUpdating() && (currentTime - loopTime) < I2C_ENCODER_POLL_MS) return; + if (currentTime - loopTime < I2C_ENCODER_POLL_MS) return; + + if (!currentEffectAndPaletteInitialized) findCurrentEffectAndPalette(); + + if (modes_alpha_indexes[effectCurrentIndex] != effectCurrent || + palettes_alpha_indexes[effectPaletteIndex] != effectPalette) { + currentEffectAndPaletteInitialized = false; + } + + // ---- button ---- + bool buttonPressed = readButtonState(); + if (buttonPressed) { + if (!buttonPressedBefore) buttonPressedTime = currentTime; + buttonPressedBefore = true; + if (currentTime - buttonPressedTime > 3000) { + if (!buttonLongPressed) displayNetworkInfo(); + buttonLongPressed = true; + } + } else if (!buttonPressed && buttonPressedBefore) { + bool doublePress = buttonWaitTime; + buttonWaitTime = 0; + if (!buttonLongPressed) { + if (doublePress) { + toggleOnOff(); + lampUdated(); + } else { + buttonWaitTime = currentTime; + } + } + buttonLongPressed = false; + buttonPressedBefore = false; + } + + // Single-press: cycle modes after 350 ms with no second press + if (buttonWaitTime && currentTime - buttonWaitTime > 350 && !buttonPressedBefore) { + buttonWaitTime = 0; + char newState = select_state + 1; + bool changedState = false; + char lineBuffer[64]; + do { + switch (newState) { + case 0: strcpy_P(lineBuffer, PSTR("Brightness")); changedState = true; break; + case 1: if (!extractModeSlider(effectCurrent, 0, lineBuffer, 63)) newState++; else changedState = true; break; + case 2: if (!extractModeSlider(effectCurrent, 1, lineBuffer, 63)) newState++; else changedState = true; break; + case 3: strcpy_P(lineBuffer, PSTR("Color Palette")); changedState = true; break; + case 4: strcpy_P(lineBuffer, PSTR("Effect")); changedState = true; break; + case 5: strcpy_P(lineBuffer, PSTR("Main Color")); changedState = true; break; + case 6: strcpy_P(lineBuffer, PSTR("Saturation")); changedState = true; break; + case 7: + if (!(strip.getSegment(applyToAll ? strip.getFirstSelectedSegId() : strip.getMainSegmentId()).getLightCapabilities() & 0x04)) newState++; + else { strcpy_P(lineBuffer, PSTR("CCT")); changedState = true; } + break; + case 8: if (presetHigh == 0 || presetLow == 0) newState++; else { strcpy_P(lineBuffer, PSTR("Preset")); changedState = true; } break; + case 9: + case 10: + case 11: if (!extractModeSlider(effectCurrent, newState - 7, lineBuffer, 63)) newState++; else changedState = true; break; + } + if (newState > I2CENC_LAST_UI_STATE) newState = 0; + } while (!changedState); + + if (display != nullptr) { + switch (newState) { + case 0: changedState = changeState(lineBuffer, 1, 0, 1); break; + case 1: changedState = changeState(lineBuffer, 1, 4, 2); break; + case 2: changedState = changeState(lineBuffer, 1, 8, 3); break; + case 3: changedState = changeState(lineBuffer, 2, 0, 4); break; + case 4: changedState = changeState(lineBuffer, 3, 0, 5); break; + case 5: changedState = changeState(lineBuffer, 255, 255, 7); break; + case 6: changedState = changeState(lineBuffer, 255, 255, 8); break; + case 7: changedState = changeState(lineBuffer, 255, 255, 10); break; + case 8: changedState = changeState(lineBuffer, 255, 255, 11); break; + case 9: changedState = changeState(lineBuffer, 255, 255, 10); break; + case 10: changedState = changeState(lineBuffer, 255, 255, 10); break; + case 11: changedState = changeState(lineBuffer, 255, 255, 10); break; + } + } + if (changedState) select_state = newState; + } + + // ---- encoder ---- + int16_t delta = readEncoderDelta(); + if (delta != 0) { + bool increase = delta > 0; + switch (select_state) { + case 0: changeBrightness(increase); break; + case 1: changeEffectSpeed(increase); break; + case 2: changeEffectIntensity(increase); break; + case 3: changePalette(increase); break; + case 4: changeEffect(increase); break; + case 5: changeHue(increase); break; + case 6: changeSat(increase); break; + case 7: changeCCT(increase); break; + case 8: changePreset(increase); break; + case 9: changeCustom(1, increase); break; + case 10: changeCustom(2, increase); break; + case 11: changeCustom(3, increase); break; + } + } + + loopTime = currentTime; + } + + // ------------------------------------------------------------------ display + + void displayNetworkInfo() { + #ifdef USERMOD_FOUR_LINE_DISPLAY + if (display) display->networkOverlay(PSTR("NETWORK INFO"), 10000); + #endif + } + + void findCurrentEffectAndPalette() { + currentEffectAndPaletteInitialized = true; + effectCurrentIndex = 0; + for (int i = 0; i < strip.getModeCount(); i++) { + if (modes_alpha_indexes[i] == effectCurrent) { effectCurrentIndex = i; break; } + } + effectPaletteIndex = 0; + for (unsigned i = 0; i < getPaletteCount() + customPalettes.size(); i++) { + if (palettes_alpha_indexes[i] == effectPalette) { effectPaletteIndex = i; break; } + } + } + + bool changeState(const char *stateName, byte markedLine, byte markedCol, byte glyph) { + #ifdef USERMOD_FOUR_LINE_DISPLAY + if (display != nullptr) { + if (display->wakeDisplay()) { display->redraw(true); return false; } + display->overlay(stateName, 750, glyph); + display->setMarkLine(markedLine, markedCol); + } + #endif + return true; + } + + void lampUdated() { + stateUpdated(CALL_MODE_BUTTON); + updateInterfaces(CALL_MODE_BUTTON); + } + + // ------------------------------------------------------------ change methods + + void changeBrightness(bool increase) { + #ifdef USERMOD_FOUR_LINE_DISPLAY + if (display && display->wakeDisplay()) { display->redraw(true); return; } + if (display) display->updateRedrawTime(); + #endif + if (bri < 40) bri = max(min((increase ? bri + fadeAmount/2 : bri - fadeAmount/2), 255), 0); + else bri = max(min((increase ? bri + fadeAmount : bri - fadeAmount), 255), 0); + lampUdated(); + #ifdef USERMOD_FOUR_LINE_DISPLAY + if (display) display->updateBrightness(); + #endif + } + + void changeEffect(bool increase) { + #ifdef USERMOD_FOUR_LINE_DISPLAY + if (display && display->wakeDisplay()) { display->redraw(true); return; } + if (display) display->updateRedrawTime(); + #endif + effectCurrentIndex = max(min((increase ? effectCurrentIndex + 1 : effectCurrentIndex - 1), strip.getModeCount() - 1), 0); + effectCurrent = modes_alpha_indexes[effectCurrentIndex]; + stateChanged = true; + if (applyToAll) { + for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { + Segment &seg = strip.getSegment(i); + if (!seg.isActive()) continue; + seg.setMode(effectCurrent); + } + } else { + strip.getSegment(strip.getMainSegmentId()).setMode(effectCurrent); + } + lampUdated(); + #ifdef USERMOD_FOUR_LINE_DISPLAY + if (display) display->showCurrentEffectOrPalette(effectCurrent, JSON_mode_names, 3); + #endif + } + + void changeEffectSpeed(bool increase) { + #ifdef USERMOD_FOUR_LINE_DISPLAY + if (display && display->wakeDisplay()) { display->redraw(true); return; } + if (display) display->updateRedrawTime(); + #endif + effectSpeed = max(min((increase ? effectSpeed + fadeAmount : effectSpeed - fadeAmount), 255), 0); + stateChanged = true; + if (applyToAll) { + for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { + Segment &seg = strip.getSegment(i); + if (!seg.isActive()) continue; + seg.speed = effectSpeed; + } + } else { + strip.getSegment(strip.getMainSegmentId()).speed = effectSpeed; + } + lampUdated(); + #ifdef USERMOD_FOUR_LINE_DISPLAY + if (display) display->updateSpeed(); + #endif + } + + void changeEffectIntensity(bool increase) { + #ifdef USERMOD_FOUR_LINE_DISPLAY + if (display && display->wakeDisplay()) { display->redraw(true); return; } + if (display) display->updateRedrawTime(); + #endif + effectIntensity = max(min((increase ? effectIntensity + fadeAmount : effectIntensity - fadeAmount), 255), 0); + stateChanged = true; + if (applyToAll) { + for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { + Segment &seg = strip.getSegment(i); + if (!seg.isActive()) continue; + seg.intensity = effectIntensity; + } + } else { + strip.getSegment(strip.getMainSegmentId()).intensity = effectIntensity; + } + lampUdated(); + #ifdef USERMOD_FOUR_LINE_DISPLAY + if (display) display->updateIntensity(); + #endif + } + + void changeCustom(uint8_t par, bool increase) { + uint8_t val = 0; + #ifdef USERMOD_FOUR_LINE_DISPLAY + if (display && display->wakeDisplay()) { display->redraw(true); return; } + if (display) display->updateRedrawTime(); + #endif + stateChanged = true; + if (applyToAll) { + uint8_t id = strip.getFirstSelectedSegId(); + Segment &sid = strip.getSegment(id); + switch (par) { + case 3: val = sid.custom3 = max(min((increase ? sid.custom3 + fadeAmount : sid.custom3 - fadeAmount), 255), 0); break; + case 2: val = sid.custom2 = max(min((increase ? sid.custom2 + fadeAmount : sid.custom2 - fadeAmount), 255), 0); break; + default: val = sid.custom1 = max(min((increase ? sid.custom1 + fadeAmount : sid.custom1 - fadeAmount), 255), 0); break; + } + for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { + Segment &seg = strip.getSegment(i); + if (!seg.isActive() || i == id) continue; + switch (par) { + case 3: seg.custom3 = sid.custom3; break; + case 2: seg.custom2 = sid.custom2; break; + default: seg.custom1 = sid.custom1; break; + } + } + } else { + Segment &seg = strip.getMainSegment(); + switch (par) { + case 3: val = seg.custom3 = max(min((increase ? seg.custom3 + fadeAmount : seg.custom3 - fadeAmount), 255), 0); break; + case 2: val = seg.custom2 = max(min((increase ? seg.custom2 + fadeAmount : seg.custom2 - fadeAmount), 255), 0); break; + default: val = seg.custom1 = max(min((increase ? seg.custom1 + fadeAmount : seg.custom1 - fadeAmount), 255), 0); break; + } + } + lampUdated(); + #ifdef USERMOD_FOUR_LINE_DISPLAY + if (display) { + char lineBuffer[64]; + sprintf(lineBuffer, "%d", val); + display->overlay(lineBuffer, 500, 10); + } + #endif + } + + void changePalette(bool increase) { + #ifdef USERMOD_FOUR_LINE_DISPLAY + if (display && display->wakeDisplay()) { display->redraw(true); return; } + if (display) display->updateRedrawTime(); + #endif + effectPaletteIndex = max(min((unsigned)(increase ? effectPaletteIndex + 1 : effectPaletteIndex - 1), getPaletteCount() + customPalettes.size() - 1), 0U); + effectPalette = palettes_alpha_indexes[effectPaletteIndex]; + stateChanged = true; + if (applyToAll) { + for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { + Segment &seg = strip.getSegment(i); + if (!seg.isActive()) continue; + seg.setPalette(effectPalette); + } + } else { + strip.getSegment(strip.getMainSegmentId()).setPalette(effectPalette); + } + lampUdated(); + #ifdef USERMOD_FOUR_LINE_DISPLAY + if (display) display->showCurrentEffectOrPalette(effectPalette, JSON_palette_names, 2); + #endif + } + + void changeHue(bool increase) { + #ifdef USERMOD_FOUR_LINE_DISPLAY + if (display && display->wakeDisplay()) { display->redraw(true); return; } + if (display) display->updateRedrawTime(); + #endif + currentHue1 = max(min((increase ? currentHue1 + fadeAmount : currentHue1 - fadeAmount), 255), 0); + colorHStoRGB(currentHue1 * 256, currentSat1, colPri); + stateChanged = true; + if (applyToAll) { + for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { + Segment &seg = strip.getSegment(i); + if (!seg.isActive()) continue; + seg.colors[0] = RGBW32(colPri[0], colPri[1], colPri[2], colPri[3]); + } + } else { + strip.getSegment(strip.getMainSegmentId()).colors[0] = RGBW32(colPri[0], colPri[1], colPri[2], colPri[3]); + } + lampUdated(); + #ifdef USERMOD_FOUR_LINE_DISPLAY + if (display) { + char lineBuffer[64]; + sprintf(lineBuffer, "%d", currentHue1); + display->overlay(lineBuffer, 500, 7); + } + #endif + } + + void changeSat(bool increase) { + #ifdef USERMOD_FOUR_LINE_DISPLAY + if (display && display->wakeDisplay()) { display->redraw(true); return; } + if (display) display->updateRedrawTime(); + #endif + currentSat1 = max(min((increase ? currentSat1 + fadeAmount : currentSat1 - fadeAmount), 255), 0); + colorHStoRGB(currentHue1 * 256, currentSat1, colPri); + if (applyToAll) { + for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { + Segment &seg = strip.getSegment(i); + if (!seg.isActive()) continue; + seg.colors[0] = RGBW32(colPri[0], colPri[1], colPri[2], colPri[3]); + } + } else { + strip.getSegment(strip.getMainSegmentId()).colors[0] = RGBW32(colPri[0], colPri[1], colPri[2], colPri[3]); + } + lampUdated(); + #ifdef USERMOD_FOUR_LINE_DISPLAY + if (display) { + char lineBuffer[64]; + sprintf(lineBuffer, "%d", currentSat1); + display->overlay(lineBuffer, 500, 8); + } + #endif + } + + void changePreset(bool increase) { + #ifdef USERMOD_FOUR_LINE_DISPLAY + if (display && display->wakeDisplay()) { display->redraw(true); return; } + if (display) display->updateRedrawTime(); + #endif + if (presetHigh && presetLow && presetHigh > presetLow) { + StaticJsonDocument<64> root; + char str[64]; + sprintf_P(str, PSTR("%d~%d~%s"), presetLow, presetHigh, increase ? "" : "-"); + root["ps"] = str; + deserializeState(root.as(), CALL_MODE_BUTTON_PRESET); + lampUdated(); + #ifdef USERMOD_FOUR_LINE_DISPLAY + if (display) { + sprintf(str, "%d", currentPreset); + display->overlay(str, 500, 11); + } + #endif + } + } + + void changeCCT(bool increase) { + #ifdef USERMOD_FOUR_LINE_DISPLAY + if (display && display->wakeDisplay()) { display->redraw(true); return; } + if (display) display->updateRedrawTime(); + #endif + currentCCT = max(min((increase ? currentCCT + fadeAmount : currentCCT - fadeAmount), 255), 0); + for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { + Segment &seg = strip.getSegment(i); + if (!seg.isActive()) continue; + seg.setCCT(currentCCT); + } + lampUdated(); + #ifdef USERMOD_FOUR_LINE_DISPLAY + if (display) { + char lineBuffer[64]; + sprintf(lineBuffer, "%d", currentCCT); + display->overlay(lineBuffer, 500, 10); + } + #endif + } + + // ------------------------------------------------------------------ config + + void addToConfig(JsonObject &root) override { + JsonObject top = root.createNestedObject(FPSTR(_name)); + top[FPSTR(_enabled)] = enabled; + top[FPSTR(_i2cAddress)] = i2cAddress; + top[FPSTR(_presetLow)] = presetLow; + top[FPSTR(_presetHigh)] = presetHigh; + top[FPSTR(_applyToAll)] = applyToAll; + } + + void appendConfigData() override { + oappend(F("addInfo('I2C-Encoder-UI:i2c-address',1,'default: 64 (0x40)');")); + } + + bool readFromConfig(JsonObject &root) override { + JsonObject top = root[FPSTR(_name)]; + if (top.isNull()) { + DEBUG_PRINT(FPSTR(_name)); + DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); + return false; + } + + uint8_t newAddr = top[FPSTR(_i2cAddress)] | i2cAddress; + + presetHigh = top[FPSTR(_presetHigh)] | presetHigh; + presetLow = top[FPSTR(_presetLow)] | presetLow; + presetHigh = MIN(250, MAX(0, presetHigh)); + presetLow = MIN(250, MAX(0, presetLow)); + enabled = top[FPSTR(_enabled)] | enabled; + applyToAll = top[FPSTR(_applyToAll)] | applyToAll; + + if (!initDone) { + i2cAddress = newAddr; + } else if (i2cAddress != newAddr) { + i2cAddress = newAddr; + deviceFound = false; + setup(); + } + + return !top[FPSTR(_applyToAll)].isNull(); + } +}; + + +const char I2CEncoderUIUsermod::_name[] PROGMEM = "I2C-Encoder-UI"; +const char I2CEncoderUIUsermod::_enabled[] PROGMEM = "enabled"; +const char I2CEncoderUIUsermod::_i2cAddress[] PROGMEM = "i2c-address"; +const char I2CEncoderUIUsermod::_presetHigh[] PROGMEM = "preset-high"; +const char I2CEncoderUIUsermod::_presetLow[] PROGMEM = "preset-low"; +const char I2CEncoderUIUsermod::_applyToAll[] PROGMEM = "apply-2-all-seg"; + + +static I2CEncoderUIUsermod usermod_v2_i2c_encoder_ui; +REGISTER_USERMOD(usermod_v2_i2c_encoder_ui); diff --git a/wled00/const.h b/wled00/const.h index 70373316fd..3d4601339b 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -230,6 +230,8 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit"); #define USERMOD_ID_RF433 56 //Usermod "usermod_v2_RF433.h" #define USERMOD_ID_BRIGHTNESS_FOLLOW_SUN 57 //Usermod "usermod_v2_brightness_follow_sun.h" #define USERMOD_ID_USER_FX 58 //Usermod "user_fx" +#define USERMOD_ID_I2C_ENCODER_UI 59 //Usermod "usermod_v2_i2c_encoder_ui" +#define USERMOD_ID_GC9A01_DISPLAY 60 //Usermod "usermod_v2_gc9a01_display" //Wifi encryption type #ifdef WLED_ENABLE_WPA_ENTERPRISE diff --git a/wled00/pin_manager.h b/wled00/pin_manager.h index 7bdd5cfc20..8c872f65fe 100644 --- a/wled00/pin_manager.h +++ b/wled00/pin_manager.h @@ -76,7 +76,9 @@ enum struct PinOwner : uint8_t { UM_LDR_DUSK_DAWN = USERMOD_ID_LDR_DUSK_DAWN, // 0x2B // Usermod "usermod_LDR_Dusk_Dawn_v2.h" UM_MAX17048 = USERMOD_ID_MAX17048, // 0x2F // Usermod "usermod_max17048.h" UM_BME68X = USERMOD_ID_BME68X, // 0x31 // Usermod "usermod_bme68x.h -- Uses "standard" HW_I2C pins - UM_PIXELS_DICE_TRAY = USERMOD_ID_PIXELS_DICE_TRAY // 0x35 // Usermod "pixels_dice_tray.h" -- Needs compile time specified 6 pins for display including SPI. + UM_PIXELS_DICE_TRAY = USERMOD_ID_PIXELS_DICE_TRAY, // 0x35 // Usermod "pixels_dice_tray.h" -- Needs compile time specified 6 pins for display including SPI. + UM_I2CEncoderUI = USERMOD_ID_I2C_ENCODER_UI, // 0x3B // Usermod "usermod_v2_i2c_encoder_ui" + UM_GC9A01Display = USERMOD_ID_GC9A01_DISPLAY // 0x3C // Usermod "usermod_v2_gc9a01_display" -- Needs compile time SPI pins }; static_assert(0u == static_cast(PinOwner::None), "PinOwner::None must be zero, so default array initialization works as expected");