diff --git a/platformio.ini b/platformio.ini index 5ebc43f559..3dce1a7cf6 100644 --- a/platformio.ini +++ b/platformio.ini @@ -236,6 +236,7 @@ AR_lib_deps = kosme/arduinoFFT @ 2.0.1 ;; ;; please note that you can NOT update existing ESP32 installs with a "V4" build. Also updating by OTA will not work properly. ;; You need to completely erase your device (esptool erase_flash) first, then install the "V4" build from VSCode+platformio. +default_partitions = tools/WLED_ESP32_4MB_1MB_FS.csv platform = espressif32@ ~6.3.2 platform_packages = platformio/framework-arduinoespressif32 @ 3.20009.0 ;; select arduino-esp32 v2.0.9 (arduino-esp32 2.0.10 thru 2.0.14 are buggy so avoid them) build_flags = -g diff --git a/usermods/AT8870_I2C_PSPWM_v2/usermod.h b/usermods/AT8870_I2C_PSPWM_v2/usermod.h new file mode 100644 index 0000000000..9c8334b3ed --- /dev/null +++ b/usermods/AT8870_I2C_PSPWM_v2/usermod.h @@ -0,0 +1,83 @@ +#pragma once + +#include "wled.h" +#include "bus_usermod.h" + + +class UMB_AT8870_I2C_PSPWM : public UsermodBus +{ + public: + + uint16_t getId() override { return USERMOD_ID_BUS; } + void loop() override {} + + void initBus(BusUsermod* bus) override + { + allocateBusData(bus, bus->getLength() * 4); + setBusRGB(bus, true); + setBusWhite(bus, true); + } + + void setBusPixelColor(BusUsermod* bus, uint16_t pix, uint32_t c) override + { + uint8_t* data = getBusData(bus); + uint16_t i = pix * 4; + data[i++] = R(c); + data[i++] = G(c); + data[i++] = B(c); + data[i++] = W(c); + } + + uint32_t getBusPixelColor(const BusUsermod* bus, uint16_t pix) const override + { + uint8_t* data = getBusData(bus); + uint16_t i = pix * 4; + return RGBW32(data[i], data[i+1], data[i+2], data[i+3]); + } + + void showBus(BusUsermod* bus) override + { + const uint8_t* pins = getBusPins(bus); + uint32_t c = getBusPixelColor(bus, 0); + if ( bus->hasWhite() ) { + c = autoWhiteCalc(bus, c); + } + + uint8_t pwm_duty; + uint16_t pwm_delay; + uint8_t bri = getBusBrightness(bus); + + + Wire.beginTransmission(pins[0]); + + Wire.write(16); // 16 == start of PWM addresses + + pwm_delay = 0; // PWM delay for red = 0 + pwm_duty = (R(c) * bri) / 255; // PWM duty for red, scaled by brightness + Wire.write(pwm_duty); + Wire.write(pwm_delay); + + pwm_delay += pwm_duty; // PWM delay for green, start after red + if ( pwm_delay > 254 ) pwm_delay -= 255; + pwm_duty = (G(c) * bri) / 255; // PWM duty for green, scaled by brightness + Wire.write(pwm_duty); + Wire.write(pwm_delay); + + pwm_delay += pwm_duty; // PWM delay for blue, start after green + if ( pwm_delay > 254 ) pwm_delay -= 255; + pwm_duty = (B(c) * bri) / 255; // PWM duty for blue, scaled by brightness + Wire.write(pwm_duty); + Wire.write(pwm_delay); + + pwm_delay += pwm_duty; // PWM delay for white, start after blue + if ( pwm_delay > 254 ) pwm_delay -= 255; + pwm_duty = (W(c) * bri) / 255; // PWM duty for white, scaled by brightness + Wire.write(pwm_duty); + Wire.write(pwm_delay); + + Wire.endTransmission(); + + } + +}; + diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index e5918ce95f..fe6e08ea43 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -11,6 +11,7 @@ #include "pin_manager.h" #include "bus_wrapper.h" #include "bus_manager.h" +#include "bus_usermod.h" extern bool cctICused; @@ -699,7 +700,9 @@ uint32_t BusManager::memUsage(unsigned maxChannels, unsigned maxCount, unsigned int BusManager::add(BusConfig &bc) { if (getNumBusses() - getNumVirtualBusses() >= WLED_MAX_BUSSES) return -1; - if (Bus::isVirtual(bc.type)) { + if (bc.type == TYPE_USERMOD) { + busses[numBusses] = new BusUsermod(bc); + } else if (Bus::isVirtual(bc.type)) { busses[numBusses] = new BusNetwork(bc); } else if (Bus::isDigital(bc.type)) { busses[numBusses] = new BusDigital(bc, numBusses, colorOrderMap); @@ -749,13 +752,12 @@ String BusManager::getLEDTypesJSONString(void) { {TYPE_NET_ARTNET_RGB, "N", PSTR("Art-Net RGB (network)")}, {TYPE_NET_DDP_RGBW, "N", PSTR("DDP RGBW (network)")}, {TYPE_NET_ARTNET_RGBW, "N", PSTR("Art-Net RGBW (network)")}, - // hypothetical extensions - //{TYPE_VIRTUAL_I2C_W, "V", PSTR("I2C White (virtual)")}, // allows setting I2C address in _pin[0] - //{TYPE_VIRTUAL_I2C_CCT, "V", PSTR("I2C CCT (virtual)")}, // allows setting I2C address in _pin[0] - //{TYPE_VIRTUAL_I2C_RGB, "V", PSTR("I2C RGB (virtual)")}, // allows setting I2C address in _pin[0] + {TYPE_USERMOD, "V", PSTR("Usermod (virtual)")}, // virtual bus for usermods }; String json = "["; for (const auto &type : types) { + extern UsermodManager usermods; + if ( (type.id == TYPE_USERMOD) && (!usermods.lookup(USERMOD_ID_BUS))) continue; String id = String(type.id); // capabilities follows similar pattern as JSON API int capabilities = Bus::hasRGB(type.id) | Bus::hasWhite(type.id)<<1 | Bus::hasCCT(type.id)<<2 | Bus::is16bit(type.id)<<4; diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index 1d1136fd83..a16af79e0a 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -119,7 +119,8 @@ class Bus { type == TYPE_SK6812_RGBW || type == TYPE_TM1814 || type == TYPE_UCS8904 || type == TYPE_FW1906 || type == TYPE_WS2805 || type == TYPE_SM16825 || // digital types with white channel (type > TYPE_ONOFF && type <= TYPE_ANALOG_5CH && type != TYPE_ANALOG_3CH) || // analog types with white channel - type == TYPE_NET_DDP_RGBW || type == TYPE_NET_ARTNET_RGBW; // network types with white channel + type == TYPE_NET_DDP_RGBW || type == TYPE_NET_ARTNET_RGBW || + type == TYPE_USERMOD; // network types with white channel } static constexpr bool hasCCT(uint8_t type) { return type == TYPE_WS2812_2CH_X3 || type == TYPE_WS2812_WWA || @@ -132,7 +133,7 @@ class Bus { static constexpr bool is2Pin(uint8_t type) { return (type >= TYPE_2PIN_MIN && type <= TYPE_2PIN_MAX); } static constexpr bool isOnOff(uint8_t type) { return (type == TYPE_ONOFF); } static constexpr bool isPWM(uint8_t type) { return (type >= TYPE_ANALOG_MIN && type <= TYPE_ANALOG_MAX); } - static constexpr bool isVirtual(uint8_t type) { return (type >= TYPE_VIRTUAL_MIN && type <= TYPE_VIRTUAL_MAX); } + static constexpr bool isVirtual(uint8_t type) { return ((type >= TYPE_VIRTUAL_MIN && type <= TYPE_VIRTUAL_MAX) || type == TYPE_USERMOD); } static constexpr bool is16bit(uint8_t type) { return type == TYPE_UCS8903 || type == TYPE_UCS8904 || type == TYPE_SM16825; } static constexpr int numPWMPins(uint8_t type) { return (type - 40); } diff --git a/wled00/bus_usermod.cpp b/wled00/bus_usermod.cpp new file mode 100644 index 0000000000..44598a0e27 --- /dev/null +++ b/wled00/bus_usermod.cpp @@ -0,0 +1,75 @@ + +#include +#include + +#include "pin_manager.h" +#include "bus_wrapper.h" +#include "bus_manager.h" +#include "bus_usermod.h" + +#include "Wire.h" + +extern BusManager busses; +extern UsermodManager usermods; + +void BusUsermod::setPixelColor(uint16_t pix, uint32_t c) { + if (!_valid || !_usermod || pix >= _len) return; + return _usermod->setBusPixelColor(this, pix, c); +} + +uint32_t BusUsermod::getPixelColor(uint16_t pix) const { + if (!_valid || !_usermod || pix >= _len) return 0; + return _usermod->getBusPixelColor(this, pix); +} + +void BusUsermod::show() { + if (!_valid || !_usermod) return; + return _usermod->showBus(this); +} + +BusUsermod::BusUsermod(BusConfig &bc) +: Bus(bc.type, bc.start, bc.autoWhite, bc.count) +{ +// safe pin configuration for the actual Usermod + _pins[0] = bc.pins[0]; + _pins[1] = bc.pins[1]; + _pins[2] = bc.pins[2]; + _pins[3] = bc.pins[3]; + _pins[4] = bc.pins[4]; + + _valid = 1; +// error: 'dynamic_cast' not permitted with '-fno-rtti' +// _usermod = dynamic_cast (usermods.lookup(USERMOD_ID_BUS)); + _usermod = static_cast (usermods.lookup(USERMOD_ID_BUS)); + + if ( _usermod ) { + _usermod->initBus(this); + } +} + +void BusUsermod::cleanup(void) { + _type = I_NONE; + _valid = false; + freeData(); +} + +uint8_t BusUsermod::getPins(uint8_t* pinArray) const { + if (!_valid) return 0; + unsigned numPins = 5; + for (unsigned i = 0; i < numPins; i++) pinArray[i] = _pins[i]; + return numPins; +} + + +void UsermodBus::setup() { + uint8_t busNr; + for ( busNr = 0; busNr < busses.getNumBusses(); busNr++ ) { + BusUsermod* bus = static_cast (busses.getBus(busNr)); + if ( bus->getType() == TYPE_USERMOD ) { + if ( bus->_usermod ) continue; + bus->_usermod = this; + initBus(bus); + } + } +} + diff --git a/wled00/bus_usermod.h b/wled00/bus_usermod.h new file mode 100644 index 0000000000..93e8704778 --- /dev/null +++ b/wled00/bus_usermod.h @@ -0,0 +1,59 @@ +#ifndef BusUsermod_h +#define BusUsermod_h + +class JsonObject; + +#include "usermod.h" + +class UsermodBus; + +class BusUsermod : public Bus { + public: + BusUsermod(BusConfig &bc); + ~BusUsermod() { cleanup(); } + + void setPixelColor(uint16_t pix, uint32_t c) override; + uint32_t getPixelColor(uint16_t pix) const override; + void show(void) override; + void cleanup(void); + + uint8_t getPins(uint8_t* pinArray) const override; + + protected: + UsermodBus* _usermod; + uint8 _pins[5]; // used as configuration hints for the Usermod + + friend class UsermodBus; +}; + + + +class UsermodBus : public Usermod { + public: + + void setup() override; + + protected: + virtual void showBus(BusUsermod* bus) = 0; + virtual void setBusPixelColor(BusUsermod* bus, uint16_t pix, uint32_t c) = 0; + virtual uint32_t getBusPixelColor(const BusUsermod* bus, uint16_t pix) const = 0; + virtual void initBus(BusUsermod* bus) = 0; + +// helper functions, as C++ classes do not inherit friend class permissions + inline uint8_t* allocateBusData(BusUsermod* bus, size_t size = 1) { return bus->allocateData(size); } + inline uint8_t* getBusData(const BusUsermod* bus) const { return bus->_data; } + inline const uint8_t* getBusPins(const BusUsermod* bus) const { return bus->_pins; } + inline void setBusRGB(BusUsermod* bus, const bool hasRgb) { bus->_hasRgb = hasRgb; } + inline void setBusWhite(BusUsermod* bus, const bool hasWhite) { bus->_hasWhite = hasWhite; } + inline void setBusCCT(BusUsermod* bus, const bool hasCCT) { bus->_hasCCT = hasCCT; } + inline uint8_t getBusBrightness(const BusUsermod* bus) const { return bus->_bri; } + inline uint32_t autoWhiteCalc(const BusUsermod* bus, uint32_t c) const { return bus->autoWhiteCalc(c); } + + friend class BusUsermod; +}; + + + + + +#endif diff --git a/wled00/const.h b/wled00/const.h index bdc80aab93..08bc0d6a8e 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -204,6 +204,10 @@ #define USERMOD_ID_POV_DISPLAY 53 //Usermod "usermod_pov_display.h" #define USERMOD_ID_PIXELS_DICE_TRAY 54 //Usermod "pixels_dice_tray.h" +#define USERMOD_ID_BUS 127 //Special ID to be used by UsermodBus instances (only one may be used at once) + + + //Access point behavior #define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot #define AP_BEHAVIOR_NO_CONN 1 //Open when no connection (either after boot or if connection is lost) @@ -327,6 +331,7 @@ #define TYPE_NET_DDP_RGBW 88 //network DDP RGBW bus (master broadcast bus) #define TYPE_NET_ARTNET_RGBW 89 //network ArtNet RGB bus (master broadcast bus, unused) #define TYPE_VIRTUAL_MAX 95 +#define TYPE_USERMOD 127 //virtual type for usermods /* // old macros that have been moved to Bus class diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index 30709e12d1..a39ac7187a 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -81,8 +81,8 @@ let nm = LC.name.substring(0,2); let n = LC.name.substring(2); let t = parseInt(d.Sf["LT"+n].value, 10); // LED type SELECT - // ignore IP address - if (nm=="L0" || nm=="L1" || nm=="L2" || nm=="L3") { + // ignore IP address and virtual usermod + if (nm=="L0" || nm=="L1" || nm=="L2" || nm=="L3" || nm=="L4") { if (t>=80) return; } //check for pin conflicts @@ -259,7 +259,7 @@ gId("p1d"+n).innerText = p1d; gId("off"+n).innerText = off; // secondary pins show/hide (type string length is equivalent to number of pins used; except for network and on/off) - let pins = Math.min(gT(t).t.length,1) + 3*isNet(t); // fixes network pins to 4 + let pins = Math.min(gT(t).t.length,1) + 4*isVir(t) - 1*isNet(t); // fixes network pins to 4, other virtual to 5 for (let p=1; p<5; p++) { var LK = d.Sf["L"+p+n]; if (!LK) continue; diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index d95b8ef8e4..8b24b1f8bb 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -256,97 +256,8 @@ void espNowReceiveCB(uint8_t* address, uint8_t* data, uint8_t len, signed int rs int getSignalQuality(int rssi); void WiFiEvent(WiFiEvent_t event); -//um_manager.cpp -typedef enum UM_Data_Types { - UMT_BYTE = 0, - UMT_UINT16, - UMT_INT16, - UMT_UINT32, - UMT_INT32, - UMT_FLOAT, - UMT_DOUBLE, - UMT_BYTE_ARR, - UMT_UINT16_ARR, - UMT_INT16_ARR, - UMT_UINT32_ARR, - UMT_INT32_ARR, - UMT_FLOAT_ARR, - UMT_DOUBLE_ARR -} um_types_t; -typedef struct UM_Exchange_Data { - // should just use: size_t arr_size, void **arr_ptr, byte *ptr_type - size_t u_size; // size of u_data array - um_types_t *u_type; // array of data types - void **u_data; // array of pointers to data - UM_Exchange_Data() { - u_size = 0; - u_type = nullptr; - u_data = nullptr; - } - ~UM_Exchange_Data() { - if (u_type) delete[] u_type; - if (u_data) delete[] u_data; - } -} um_data_t; -const unsigned int um_data_size = sizeof(um_data_t); // 12 bytes - -class Usermod { - protected: - um_data_t *um_data; // um_data should be allocated using new in (derived) Usermod's setup() or constructor - public: - Usermod() { um_data = nullptr; } - virtual ~Usermod() { if (um_data) delete um_data; } - virtual void setup() = 0; // pure virtual, has to be overriden - virtual void loop() = 0; // pure virtual, has to be overriden - virtual void handleOverlayDraw() {} // called after all effects have been processed, just before strip.show() - virtual bool handleButton(uint8_t b) { return false; } // button overrides are possible here - virtual bool getUMData(um_data_t **data) { if (data) *data = nullptr; return false; }; // usermod data exchange [see examples for audio effects] - virtual void connected() {} // called when WiFi is (re)connected - virtual void appendConfigData() {} // helper function called from usermod settings page to add metadata for entry fields - virtual void addToJsonState(JsonObject& obj) {} // add JSON objects for WLED state - virtual void addToJsonInfo(JsonObject& obj) {} // add JSON objects for UI Info page - virtual void readFromJsonState(JsonObject& obj) {} // process JSON messages received from web server - virtual void addToConfig(JsonObject& obj) {} // add JSON entries that go to cfg.json - virtual bool readFromConfig(JsonObject& obj) { return true; } // Note as of 2021-06 readFromConfig() now needs to return a bool, see usermod_v2_example.h - virtual void onMqttConnect(bool sessionPresent) {} // fired when MQTT connection is established (so usermod can subscribe) - virtual bool onMqttMessage(char* topic, char* payload) { return false; } // fired upon MQTT message received (wled topic) - virtual bool onEspNowMessage(uint8_t* sender, uint8_t* payload, uint8_t len) { return false; } // fired upon ESP-NOW message received - virtual void onUpdateBegin(bool) {} // fired prior to and after unsuccessful firmware update - virtual void onStateChange(uint8_t mode) {} // fired upon WLED state change - virtual uint16_t getId() {return USERMOD_ID_UNSPECIFIED;} -}; - -class UsermodManager { - private: - Usermod* ums[WLED_MAX_USERMODS]; - byte numMods = 0; - public: - void loop(); - void handleOverlayDraw(); - bool handleButton(uint8_t b); - bool getUMData(um_data_t **um_data, uint8_t mod_id = USERMOD_ID_RESERVED); // USERMOD_ID_RESERVED will poll all usermods - void setup(); - void connected(); - void appendConfigData(); - void addToJsonState(JsonObject& obj); - void addToJsonInfo(JsonObject& obj); - void readFromJsonState(JsonObject& obj); - void addToConfig(JsonObject& obj); - bool readFromConfig(JsonObject& obj); -#ifndef WLED_DISABLE_MQTT - void onMqttConnect(bool sessionPresent); - bool onMqttMessage(char* topic, char* payload); -#endif -#ifndef WLED_DISABLE_ESPNOW - bool onEspNowMessage(uint8_t* sender, uint8_t* payload, uint8_t len); -#endif - void onUpdateBegin(bool); - void onStateChange(uint8_t); - bool add(Usermod* um); - Usermod* lookup(uint16_t mod_id); - byte getModCount() {return numMods;}; -}; +#include "usermod.h" //usermods_list.cpp void registerUsermods(); diff --git a/wled00/usermod.h b/wled00/usermod.h new file mode 100644 index 0000000000..10be910406 --- /dev/null +++ b/wled00/usermod.h @@ -0,0 +1,97 @@ +#ifndef Usermod_h +#define Usermod_h + +//um_manager.cpp +typedef enum UM_Data_Types { + UMT_BYTE = 0, + UMT_UINT16, + UMT_INT16, + UMT_UINT32, + UMT_INT32, + UMT_FLOAT, + UMT_DOUBLE, + UMT_BYTE_ARR, + UMT_UINT16_ARR, + UMT_INT16_ARR, + UMT_UINT32_ARR, + UMT_INT32_ARR, + UMT_FLOAT_ARR, + UMT_DOUBLE_ARR +} um_types_t; +typedef struct UM_Exchange_Data { + // should just use: size_t arr_size, void **arr_ptr, byte *ptr_type + size_t u_size; // size of u_data array + um_types_t *u_type; // array of data types + void **u_data; // array of pointers to data + UM_Exchange_Data() { + u_size = 0; + u_type = nullptr; + u_data = nullptr; + } + ~UM_Exchange_Data() { + if (u_type) delete[] u_type; + if (u_data) delete[] u_data; + } +} um_data_t; +const unsigned int um_data_size = sizeof(um_data_t); // 12 bytes + + +class Usermod { + protected: + um_data_t *um_data; // um_data should be allocated using new in (derived) Usermod's setup() or constructor + public: + Usermod() { um_data = nullptr; } + virtual ~Usermod() { if (um_data) delete um_data; } + virtual void setup() = 0; // pure virtual, has to be overriden + virtual void loop() = 0; // pure virtual, has to be overriden + virtual void handleOverlayDraw() {} // called after all effects have been processed, just before strip.show() + virtual bool handleButton(uint8_t b) { return false; } // button overrides are possible here + virtual bool getUMData(um_data_t **data) { if (data) *data = nullptr; return false; }; // usermod data exchange [see examples for audio effects] + virtual void connected() {} // called when WiFi is (re)connected + virtual void appendConfigData() {} // helper function called from usermod settings page to add metadata for entry fields + virtual void addToJsonState(JsonObject& obj) {} // add JSON objects for WLED state + virtual void addToJsonInfo(JsonObject& obj) {} // add JSON objects for UI Info page + virtual void readFromJsonState(JsonObject& obj) {} // process JSON messages received from web server + virtual void addToConfig(JsonObject& obj) {} // add JSON entries that go to cfg.json + virtual bool readFromConfig(JsonObject& obj) { return true; } // Note as of 2021-06 readFromConfig() now needs to return a bool, see usermod_v2_example.h + virtual void onMqttConnect(bool sessionPresent) {} // fired when MQTT connection is established (so usermod can subscribe) + virtual bool onMqttMessage(char* topic, char* payload) { return false; } // fired upon MQTT message received (wled topic) + virtual bool onEspNowMessage(uint8_t* sender, uint8_t* payload, uint8_t len) { return false; } // fired upon ESP-NOW message received + virtual void onUpdateBegin(bool) {} // fired prior to and after unsuccessful firmware update + virtual void onStateChange(uint8_t mode) {} // fired upon WLED state change + virtual uint16_t getId() {return USERMOD_ID_UNSPECIFIED;} +}; + +class UsermodManager { + private: + Usermod* ums[WLED_MAX_USERMODS]; + byte numMods = 0; + + public: + void loop(); + void handleOverlayDraw(); + bool handleButton(uint8_t b); + bool getUMData(um_data_t **um_data, uint8_t mod_id = USERMOD_ID_RESERVED); // USERMOD_ID_RESERVED will poll all usermods + void setup(); + void connected(); + void appendConfigData(); + void addToJsonState(JsonObject& obj); + void addToJsonInfo(JsonObject& obj); + void readFromJsonState(JsonObject& obj); + void addToConfig(JsonObject& obj); + bool readFromConfig(JsonObject& obj); +#ifndef WLED_DISABLE_MQTT + void onMqttConnect(bool sessionPresent); + bool onMqttMessage(char* topic, char* payload); +#endif +#ifndef WLED_DISABLE_ESPNOW + bool onEspNowMessage(uint8_t* sender, uint8_t* payload, uint8_t len); +#endif + void onUpdateBegin(bool); + void onStateChange(uint8_t); + bool add(Usermod* um); + Usermod* lookup(uint16_t mod_id); + byte getModCount() {return numMods;}; +}; + +#endif diff --git a/wled00/usermods_list.cpp b/wled00/usermods_list.cpp index 25d9ee9ab9..9d258556e7 100644 --- a/wled00/usermods_list.cpp +++ b/wled00/usermods_list.cpp @@ -242,6 +242,11 @@ #include "../usermods/LD2410_v2/usermod_ld2410.h" #endif +#ifdef USERMOD_AT8870_I2C_PSPWM +#include "../usermods/AT8870_I2C_PSPWM_v2/usermod.h" +#endif + + void registerUsermods() { /* @@ -470,4 +475,8 @@ void registerUsermods() #ifdef USERMOD_POV_DISPLAY usermods.add(new PovDisplayUsermod()); #endif + + #ifdef USERMOD_AT8870_I2C_PSPWM + usermods.add(new UMB_AT8870_I2C_PSPWM()); + #endif }