diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 080e70d0..8057bc70 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,7 +1,6 @@ { - // See http://go.microsoft.com/fwlink/?LinkId=827846 - // for the documentation about the extensions.json format "recommendations": [ + "pioarduino.pioarduino-ide", "platformio.platformio-ide" ], "unwantedRecommendations": [ diff --git a/usermods/pca9685/readme.md b/usermods/pca9685/readme.md new file mode 100644 index 00000000..041a8ca3 --- /dev/null +++ b/usermods/pca9685/readme.md @@ -0,0 +1,95 @@ +# PCA9685 Usermod + +This usermod allows you to control a PCA9685 16-channel 12-bit PWM Servo Driver via I2C. + +## Features +- **Specific Control**: 4x RGB LEDs + 2x White LEDs via JSON API. +- **Animation Support**: Mirrors WLED internal pixels to PCA9685 channels, allowing WLED effects and segments to work on I2C LEDs. + +## Installation + +1. Copy the `usermods/pca9685` folder to your WLED `usermods` directory (if not already there). +2. Add the `Adafruit PWM Servo Driver Library` to your `lib_deps`. + In `platformio_override.ini`: + ```ini + [env:usermods] + lib_deps = + adafruit/Adafruit PWM Servo Driver Library @ ^2.4.1 + ``` +3. Register the usermod by adding `#include "usermods/pca9685/usermod_pca9685.h"` in `wled00/usermods_list.cpp` or ensuring it is picked up by the build system if configured that way (auto-discovery depends on WLED version, manual include is safer). + *However, WLED's `REGISTER_USERMOD` macro usually handles the hook if the file is included in the build.* + + **Recommended:** Add `-D USERMOD_PCA9685` to your build flags and use `wled00/usermods_list.cpp` to conditionally include it if you want to keep it optional, OR just include the header if compiling a custom build. + +## Configuration + +Navigate to the "Usermods" settings page in WLED. +- **enabled**: Enable/Disable the usermod. +- **i2c_addr**: I2C address of the PCA9685 (default 64 / 0x40). +- **pwm_freq**: PWM frequency in Hz (default 50). + +## Usage (JSON API) + +### Smart Control (RGB & White) +Control 4 RGB LEDs and 2 White LEDs using standard 0-255 values. +* **RGB 1-4**: Mapped to channels 0-11 in groups of 3 (RGB). +* **White 1-2**: Mapped to channels 12 and 13. + +**Example Payload:** +```json +{ + "pca9685": { + "rgb": [ + [255, 0, 0], // RGB 1 (Red) + [0, 255, 0], // RGB 2 (Green) + [0, 0, 255], // RGB 3 (Blue) + [255, 255, 0] // RGB 4 (Yellow) + ], + "white": [ + 255, // White 1 (Full Brightness) + 128 // White 2 (50% Brightness) + ] + } +} +``` + +### Raw Channel Control +Directly control any channel (0-15) with raw PWM values (0-4096). +* `4096` = Fully On +* `0` = Fully Off +* `0-4095` = PWM Duty Cycle + +**Example:** +```json +{ + "pca9685": { + "14": 4096, // Turn Channel 14 ON + "15": 1024 // Set Channel 15 to 25% brightness + } +} +``` +**Note:** Raw control overrides Smart Control if both are sent for the same channel. + +## Configuration Guide for Animations + +### 1. Simple Setup (PCA9685 ONLY) +If you only have the PCA9685 connected: +* Set **LED Preferences > Length** to `6`. +* Effect segments will map 1:1. + +### 2. Hybrid Setup (NeoPixel Strip + PCA9685) +Example: 300 LED Strip + PCA9685. +1. **LED Preferences**: + * Set **Length** to `306` (300 for Strip + 6 for PCA). + * Set **LED Output 1** to control the first 300 LEDs (your physical strip). +2. **Usermod Config**: + * Set **pixelOffset** to `300`. + * *Result:* Pixels 0-299 are your strip. Pixels 300-305 are mirrored to PCA9685. +3. **Manual Control (White LEDs)**: + * If you want to control White LEDs manually via JSON API but keep RGB animations: + * Set **mirrorWhite** to `false`. + * Set **mirrorRgb** to `true`. +4. **Segments**: + * **Segment 0**: 0-300 (Main Strip) + * **Segment 1**: 300-304 (PCA RGB) -> Apply Rainbow effect here. + * **Segment 2**: 304-306 (PCA White) -> Manual control or Breathe effect. diff --git a/usermods/pca9685/usermod_pca9685.h b/usermods/pca9685/usermod_pca9685.h new file mode 100644 index 00000000..6269bab8 --- /dev/null +++ b/usermods/pca9685/usermod_pca9685.h @@ -0,0 +1,239 @@ +#pragma once + +#include "wled.h" +#include +#include + +class Pca9685Usermod : public Usermod { + public: + // Configurable parameters + bool enabled = true; + uint8_t i2c_addr = 0x40; + uint16_t pwm_freq = 50; + + // Scaling & Mirroring config + uint16_t pixelOffset = 0; // Start reading from this pixel index + bool mirrorRgb = true; // Mirror WLED pixels to PCA RGB channels + bool mirrorWhite = true; // Mirror WLED pixels to PCA White channels + + // Runtime variables + Adafruit_PWMServoDriver *pwm = nullptr; + bool initDone = false; + + // Strings for JSON keys to save flash + static const char _name[]; + static const char _enabled[]; + static const char _i2c_addr[]; + static const char _pwm_freq[]; + static const char _pixelOffset[]; + static const char _mirrorRgb[]; + static const char _mirrorWhite[]; + + void setup() { + if (pwm == nullptr) { + pwm = new Adafruit_PWMServoDriver(i2c_addr); + } + + // Initialize I2C if not already done by WLED + if (i2c_scl >= 0 && i2c_sda >= 0) { + if (!Wire.getClock()) { // Check if Wire is already initialized + Wire.begin(i2c_sda, i2c_scl); + } + } else { + enabled = false; + return; + } + + initPca9685(); + } + + void initPca9685() { + if (!enabled) return; + + if (pwm) { + pwm->begin(); + pwm->setOscillatorFrequency(27000000); + pwm->setPWMFreq(pwm_freq); // This is the maximum PWM frequency + initDone = true; + DEBUG_PRINTLN(F("PCA9685 init done")); + } + } + + void loop() { + if (!enabled || !initDone || strip.isUpdating()) return; + + // Mirror WLED pixels to PCA9685 + // Uses pixelOffset to determine where to read from in the WLED strip + + // RGB Channels (0-11) + if (mirrorRgb) { + // RGB 1 (Channels 0,1,2) <- Pixel Offset + 0 + uint32_t c = strip.getPixelColor(pixelOffset + 0); + setPwmValue(0, (uint16_t)(((c >> 16) & 0xFF) * 16.06)); // R + setPwmValue(1, (uint16_t)(((c >> 8) & 0xFF) * 16.06)); // G + setPwmValue(2, (uint16_t)((c & 0xFF) * 16.06)); // B + + // RGB 2 (Channels 3,4,5) <- Pixel Offset + 1 + c = strip.getPixelColor(pixelOffset + 1); + setPwmValue(3, (uint16_t)(((c >> 16) & 0xFF) * 16.06)); + setPwmValue(4, (uint16_t)(((c >> 8) & 0xFF) * 16.06)); + setPwmValue(5, (uint16_t)((c & 0xFF) * 16.06)); + + // RGB 3 (Channels 6,7,8) <- Pixel Offset + 2 + c = strip.getPixelColor(pixelOffset + 2); + setPwmValue(6, (uint16_t)(((c >> 16) & 0xFF) * 16.06)); + setPwmValue(7, (uint16_t)(((c >> 8) & 0xFF) * 16.06)); + setPwmValue(8, (uint16_t)((c & 0xFF) * 16.06)); + + // RGB 4 (Channels 9,10,11) <- Pixel Offset + 3 + c = strip.getPixelColor(pixelOffset + 3); + setPwmValue(9, (uint16_t)(((c >> 16) & 0xFF) * 16.06)); + setPwmValue(10, (uint16_t)(((c >> 8) & 0xFF) * 16.06)); + setPwmValue(11, (uint16_t)((c & 0xFF) * 16.06)); + } + + // White Channels (12, 13) + if (mirrorWhite) { + // White 1 (Channel 12) <- Pixel Offset + 4 + uint32_t c = strip.getPixelColor(pixelOffset + 4); + uint8_t w = (c >> 24) & 0xFF; // Try to get White component + if (w == 0) { + uint8_t r = (c >> 16) & 0xFF; + uint8_t g = (c >> 8) & 0xFF; + uint8_t b = c & 0xFF; + w = max(r, max(g, b)); + } + setPwmValue(12, (uint16_t)(w * 16.06)); + + // White 2 (Channel 13) <- Pixel Offset + 5 + c = strip.getPixelColor(pixelOffset + 5); + w = (c >> 24) & 0xFF; // Try to get White component + if (w == 0) { + uint8_t r = (c >> 16) & 0xFF; + uint8_t g = (c >> 8) & 0xFF; + uint8_t b = c & 0xFF; + w = max(r, max(g, b)); + } + setPwmValue(13, (uint16_t)(w * 16.06)); + } + } + + // Helper to set PWM value safely + void setPwmValue(int channel, uint16_t value) { + if (channel < 0 || channel > 15) return; + if (value > 4095) value = 4096; + + if (value == 4096) { + pwm->setPWM(channel, 4096, 0); + } else if (value == 0) { + pwm->setPWM(channel, 0, 4096); + } else { + pwm->setPWM(channel, 0, value); + } + } + + // JSON API to set PWM values + // usage: {"pca9685":{"0": 2048, "1": 4096}} + // values 0-4096. 4096 = fully on, 0 = fully off. + void readFromJsonState(JsonObject& root) { + if (!initDone || !enabled) return; + + if (root["pca9685"].is()) { + JsonObject pcaJson = root["pca9685"]; + + // Handle specific RGB/White control + // Format: "rgb": [[r,g,b], [r,g,b], [r,g,b], [r,g,b]] + if (pcaJson["rgb"].is()) { + JsonArray rgbArr = pcaJson["rgb"]; + for (int i = 0; i < 4 && i < rgbArr.size(); i++) { + if (rgbArr[i].is()) { + JsonArray color = rgbArr[i]; + // Map RGB i to channels + // RGB 1: 0,1,2 | RGB 2: 3,4,5 | RGB 3: 6,7,8 | RGB 4: 9,10,11 + int baseChannel = i * 3; + if (color.size() >= 3) { + setPwmValue(baseChannel, (uint16_t)(color[0].as() * 16.06)); // R 0-255 -> 0-4096 + setPwmValue(baseChannel+1, (uint16_t)(color[1].as() * 16.06)); // G + setPwmValue(baseChannel+2, (uint16_t)(color[2].as() * 16.06)); // B + } + } + } + } + + // Format: "white": [w1, w2] + if (pcaJson["white"].is()) { + JsonArray whiteArr = pcaJson["white"]; + // White 1: 12 | White 2: 13 + if (whiteArr.size() >= 1) setPwmValue(12, (uint16_t)(whiteArr[0].as() * 16.06)); + if (whiteArr.size() >= 2) setPwmValue(13, (uint16_t)(whiteArr[1].as() * 16.06)); + } + + // Handle raw channel control (overrides specific if both present) + for (JsonPair kv : pcaJson) { + // Skip "rgb" and "white" keys + if (strcmp(kv.key().c_str(), "rgb") == 0 || strcmp(kv.key().c_str(), "white") == 0) continue; + + int channel = atoi(kv.key().c_str()); + uint16_t value = kv.value().as(); + setPwmValue(channel, value); + } + } + } + + void addToConfig(JsonObject& root) { + JsonObject top = root.createNestedObject(FPSTR(_name)); + top[FPSTR(_enabled)] = enabled; + top[FPSTR(_i2c_addr)] = i2c_addr; + top[FPSTR(_pwm_freq)] = pwm_freq; + top[FPSTR(_pixelOffset)] = pixelOffset; + top[FPSTR(_mirrorRgb)] = mirrorRgb; + top[FPSTR(_mirrorWhite)] = mirrorWhite; + } + + bool readFromConfig(JsonObject& root) { + JsonObject top = root[FPSTR(_name)]; + if (top.isNull()) { + return false; + } + + bool configComplete = !top.isNull(); + configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled); + configComplete &= getJsonValue(top[FPSTR(_i2c_addr)], i2c_addr); + configComplete &= getJsonValue(top[FPSTR(_pwm_freq)], pwm_freq); + configComplete &= getJsonValue(top[FPSTR(_pixelOffset)], pixelOffset); + configComplete &= getJsonValue(top[FPSTR(_mirrorRgb)], mirrorRgb); + configComplete &= getJsonValue(top[FPSTR(_mirrorWhite)], mirrorWhite); + + if (initDone && !enabled) { + // If disabled at runtime, maybe we should de-init? + // For now just stop updating. + // There is no easy "end()" in the library. + } + + if (!initDone && enabled) { + // Re-init if re-enabled or params changed? + // Simpler to just assume reboot for major param changes like address + // But we can update freq + if (pwm) { + pwm->setPWMFreq(pwm_freq); + } + } + + return configComplete; + } + + uint16_t getId() { + return USERMOD_ID_UNSPECIFIED; // We don't have a reserved ID, using generic + } +}; + +const char Pca9685Usermod::_name[] PROGMEM = "PCA9685"; +const char Pca9685Usermod::_enabled[] PROGMEM = "enabled"; +const char Pca9685Usermod::_i2c_addr[] PROGMEM = "i2c_addr"; +const char Pca9685Usermod::_pwm_freq[] PROGMEM = "pwm_freq"; +const char Pca9685Usermod::_pixelOffset[] PROGMEM = "pixelOffset"; +const char Pca9685Usermod::_mirrorRgb[] PROGMEM = "mirrorRgb"; +const char Pca9685Usermod::_mirrorWhite[] PROGMEM = "mirrorWhite"; + +static Pca9685Usermod pca9685Usermod; +REGISTER_USERMOD(pca9685Usermod); diff --git a/wled00/usermods_list.cpp b/wled00/usermods_list.cpp new file mode 100644 index 00000000..11c1ab94 --- /dev/null +++ b/wled00/usermods_list.cpp @@ -0,0 +1,2 @@ +#include "wled.h" +#include "../usermods/pca9685/usermod_pca9685.h"