diff --git a/.gitignore b/.gitignore index c85fae0c2..e461ebbda 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,5 @@ wled-update.sh /wled00/my_config.h /wled00/Release /wled00/wled00.ino.cpp + +/tools/cdataCache.json \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index a285f1fe0..17c37c530 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,19 +1,42 @@ ## WLED changelog -#### Build 2309120 till build 2311120 +#### Build 2309120 till build 2201060 - WLED version 0.15.0-a0 +- Global JSON buffer guarding (#3648 by @willmmiles, resolves #3641, #3312, #3367, #3637, #3646, #3447) +- Effect: Fireworks 1D (fix for matrix trailing strip) +- BREAKING: Reduced number of segments (12) on ESP8266 due to less available RAM +- Increased available effect data buffer (increases more if board has PSRAM) +- Custom palette editor mobile UI enhancement (by @imeszaros) +- Per port Auto Brightness Limiter (ABL) +- Use PSRAM for JSON buffer (double size, larger ledmaps, up to 2k) +- Reduced heap fragmentation by allocating ledmap array only once and not deallocating effect buffer +- HTTP retries on failed UI load +- UI Search: scroll to top (#3587 by @WoodyLetsCode) +- Return to inline iro.js and rangetouch.js (#3597 by @WoodyLetsCode) +- Better caching (#3591 by @WoodyLetsCode) +- Do not send 404 for missing `skin.css` (#3590 by @WoodyLetsCode) +- Simplified UI rework (#3511 by @WoodyLetsCode) +- Domoticz device ID for PIR and Temperature usermods +- Bugfix for UCS8904 `hasWhite()` +- Better search in UI (#3540 by @WoodyLetsCode) +- Seeding FastLED PRNG (#3552 by @TripleWhy) +- WIZ Smart Button support (#3547 by @micw) +- New button type (button switch, fix for #3537) +- Pixel Magic Tool update (#3483 by @ajotanc) +- Effect: 2D Matrix fix for gaps +- Bugfix #3526, #3533, #3561 - Spookier Halloween Eyes (#3501) - Compile time options for Multi Relay usermod (#3498) -- Fix for Dissolve (#3502) +- Effect: Fix for Dissolve (#3502) - Better reverse proxy support (nested paths) - Implement global JSON API boolean toggle (i.e. instead of "var":true or "var":false -> "var":"t"). - Sort presets by ID -- Fix #3496 +- Fix for #3641, #3312, #3367, #3637, #3646, #3447, #3632, #3496, #2922, #3593, #3514, #3522, #3578 (partial), #3606 (@WoodyLetsCode) - Improved random bg image and added random bg image options (@WoodyLetsCode, #3481) - Audio palettes (Audioreactive usermod, credit @netmindz) - Better UI tooltips (@ajotnac, #3464) - Better effect filters (filter dropdown) -- Fix udp sync (fix for #3487) +- UDP sync fix (for #3487) - Power button override (solves #3431) - Additional HTTP request throttling (ESP8266) - Additional UI/UX improvements diff --git a/pio-scripts/build_ui.py b/pio-scripts/build_ui.py new file mode 100644 index 000000000..f3688a5d4 --- /dev/null +++ b/pio-scripts/build_ui.py @@ -0,0 +1,3 @@ +Import('env') + +env.Execute("npm run build") \ No newline at end of file diff --git a/platformio.ini b/platformio.ini index fa6bf3bb8..296ca2324 100644 --- a/platformio.ini +++ b/platformio.ini @@ -155,6 +155,7 @@ extra_scripts = post:pio-scripts/output_bins.py post:pio-scripts/strip-floats.py pre:pio-scripts/user_config_copy.py + pre:pio-scripts/build_ui.py # ------------------------------------------------------------------------------ # COMMON SETTINGS: @@ -198,6 +199,8 @@ lib_deps = #For ADS1115 sensor uncomment following ; adafruit/Adafruit BusIO @ 1.13.2 ; adafruit/Adafruit ADS1X15 @ 2.4.0 + #For MPU6050 IMU uncomment follwoing + ; electroniccats/MPU6050 @1.0.1 extra_scripts = ${scripts_defaults.extra_scripts} diff --git a/tools/cdata.js b/tools/cdata.js index bb9854608..5d9965634 100644 --- a/tools/cdata.js +++ b/tools/cdata.js @@ -22,6 +22,37 @@ const CleanCSS = require("clean-css"); const MinifyHTML = require("html-minifier-terser").minify; const packageJson = require("../package.json"); +const CACHE_FILE = __dirname + "/cdataCache.json"; +const cache = loadCache(); +cache.version = 1; + +function loadCache() { + try { + return JSON.parse(fs.readFileSync(CACHE_FILE)); + } catch (e) { + return {}; + } +} + +function saveCache(file) { + const stat = fs.statSync(file); + cache[file] = { + mtime: stat.mtimeMs, + size: stat.size, + }; + fs.writeFileSync(CACHE_FILE, JSON.stringify(cache)); +} + +function isCached(file) { + // If command line argument is set, always rebuild + if (process.argv[2] == "--force" || process.argv[2] == "-f") { + return false; + } + const stat = fs.statSync(file); + const cached = cache[file]; + return cached && cached.mtime == stat.mtimeMs && cached.size == stat.size; +} + /** * */ @@ -110,6 +141,10 @@ function filter(str, type) { } function writeHtmlGzipped(sourceFile, resultFile, page) { + if (isCached(sourceFile)) { + console.info(`Skipping ${resultFile} as it is cached`); + return; + } console.info("Reading " + sourceFile); new inliner(sourceFile, function (error, html) { console.info("Inlined " + html.length + " characters"); @@ -146,6 +181,7 @@ ${array} `; console.info("Writing " + resultFile); fs.writeFileSync(resultFile, src); + saveCache(sourceFile); }); }); } @@ -182,7 +218,7 @@ ${result} const result = hexdump(buf); const chunk = ` // Autogenerated from ${srcDir}/${s.file}, do not edit!! -const uint16_t ${s.name}_length = ${result.length}; +const uint16_t ${s.name}_length = ${buf.length}; const uint8_t ${s.name}[] PROGMEM = { ${result} }; @@ -196,6 +232,11 @@ ${result} } function writeChunks(srcDir, specs, resultFile) { + if (specs.every(s => isCached(srcDir + "/" + s.file))) { + console.info(`Skipping ${resultFile} as all files are cached`); + return; + } + let src = `/* * More web UI HTML source arrays. * This file is auto generated, please don't make any changes manually. @@ -204,12 +245,14 @@ function writeChunks(srcDir, specs, resultFile) { */ `; specs.forEach((s) => { + const file = srcDir + "/" + s.file; try { - console.info("Reading " + srcDir + "/" + s.file + " as " + s.name); + console.info("Reading " + file + " as " + s.name); src += specToChunk(srcDir, s); + saveCache(file); } catch (e) { console.warn( - "Failed " + s.name + " from " + srcDir + "/" + s.file, + "Failed " + s.name + " from " + file, e.message.length > 60 ? e.message.substring(0, 60) : e.message ); } diff --git a/usermods/ST7789_display/ST7789_display.h b/usermods/ST7789_display/ST7789_display.h index 144cccbfa..59e9cd231 100644 --- a/usermods/ST7789_display/ST7789_display.h +++ b/usermods/ST7789_display/ST7789_display.h @@ -307,7 +307,7 @@ class St7789DisplayUsermod : public Usermod { // Print estimated milliamp usage (must specify the LED type in LED prefs for this to be a reasonable estimate). tft.print("Current: "); tft.setTextColor(TFT_ORANGE); - tft.print(strip.currentMilliamps); + tft.print(BusManager::currentMilliamps()); tft.print("mA"); } diff --git a/usermods/mpu6050_imu/readme.md b/usermods/mpu6050_imu/readme.md index 412004151..b804ba602 100644 --- a/usermods/mpu6050_imu/readme.md +++ b/usermods/mpu6050_imu/readme.md @@ -20,14 +20,11 @@ react to the globes orientation. See the blog post on building it + +constexpr auto ESTIMATED_G = 9.801; // m/s^2 +constexpr auto ESTIMATED_G_COUNTS = 8350.; +constexpr auto ESTIMATED_ANGULAR_RATE = (M_PI * 2000) / (INT16_MAX * 180); // radians per second + +// Horribly lame digital filter code +// Currently implements a static IIR filter. +template +class xir_filter { + typedef Eigen::Array array_t; + const array_t a_coeff, b_coeff; + const T gain; + array_t x, y; + + public: + xir_filter(T gain_, array_t a, array_t b) : a_coeff(std::move(a)), b_coeff(std::move(b)), gain(gain_), x(array_t::Zero()), y(array_t::Zero()) {}; + + T operator()(T input) { + x.head(C-1) = x.tail(C-1); // shift by one + x(C-1) = input / gain; + y.head(C-1) = y.tail(C-1); // shift by one + y(C-1) = (x * b_coeff).sum(); + y(C-1) -= (y.head(C-1) * a_coeff.head(C-1)).sum(); + return y(C-1); + } + + T last() { return y(C-1); }; +}; + + + +class GyroSurge : public Usermod { + private: + static const char _name[]; + bool enabled = true; + + // Params + uint8_t max = 0; + float sensitivity = 0; + + // State + uint32_t last_sample; + // 100hz input + // butterworth low pass filter at 20hz + xir_filter filter = { 1., { -0.36952738, 0.19581571, 1.}, {0.20657208, 0.41314417, 0.20657208} }; + // { 1., { 0., 0., 1.}, { 0., 0., 1. } }; // no filter + + + public: + + /* + * setup() is called once at boot. WiFi is not yet connected at this point. + */ + void setup() {}; + + + /* + * addToConfig() can be used to add custom persistent settings to the cfg.json file in the "um" (usermod) object. + * It will be called by WLED when settings are actually saved (for example, LED settings are saved) + * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings! + */ + void addToConfig(JsonObject& root) + { + JsonObject top = root.createNestedObject(FPSTR(_name)); + + //save these vars persistently whenever settings are saved + top["max"] = max; + top["sensitivity"] = sensitivity; + } + + + /* + * readFromConfig() can be used to read back the custom settings you added with addToConfig(). + * This is called by WLED when settings are loaded (currently this only happens immediately after boot, or after saving on the Usermod Settings page) + * + * readFromConfig() is called BEFORE setup(). This means you can use your persistent values in setup() (e.g. pin assignments, buffer sizes), + * but also that if you want to write persistent values to a dynamic buffer, you'd need to allocate it here instead of in setup. + * If you don't know what that is, don't fret. It most likely doesn't affect your use case :) + * + * Return true in case the config values returned from Usermod Settings were complete, or false if you'd like WLED to save your defaults to disk (so any missing values are editable in Usermod Settings) + * + * getJsonValue() returns false if the value is missing, or copies the value into the variable provided and returns true if the value is present + * The configComplete variable is true only if the "exampleUsermod" object and all values are present. If any values are missing, WLED will know to call addToConfig() to save them + * + * This function is guaranteed to be called on boot, but could also be called every time settings are updated + */ + bool readFromConfig(JsonObject& root) + { + // default settings values could be set here (or below using the 3-argument getJsonValue()) instead of in the class definition or constructor + // setting them inside readFromConfig() is slightly more robust, handling the rare but plausible use case of single value being missing after boot (e.g. if the cfg.json was manually edited and a value was removed) + + JsonObject top = root[FPSTR(_name)]; + + bool configComplete = !top.isNull(); + + configComplete &= getJsonValue(top["max"], max, 0); + configComplete &= getJsonValue(top["sensitivity"], sensitivity, 10); + + return configComplete; + } + + void loop() { + // get IMU data + um_data_t *um_data; + if (!usermods.getUMData(&um_data, USERMOD_ID_IMU)) { + // Apply max + strip.getSegment(0).fadeToBlackBy(max); + return; + } + uint32_t sample_count = *(uint32_t*)(um_data->u_data[8]); + + if (sample_count != last_sample) { + last_sample = sample_count; + // Calculate based on new data + // We use the raw gyro data (angular rate) + auto gyros = (int16_t*)um_data->u_data[4]; // 16384 == 2000 deg/s + + // Compute the overall rotation rate + // For my application (a plasma sword) we ignore X axis rotations (eg. around the long axis) + auto gyro_q = Eigen::AngleAxis { + //Eigen::AngleAxis(ESTIMATED_ANGULAR_RATE * gyros[0], Eigen::Vector3f::UnitX()) * + Eigen::AngleAxis(ESTIMATED_ANGULAR_RATE * gyros[1], Eigen::Vector3f::UnitY()) * + Eigen::AngleAxis(ESTIMATED_ANGULAR_RATE * gyros[2], Eigen::Vector3f::UnitZ()) }; + + // Filter the results + filter(std::min(sensitivity * gyro_q.angle(), 1.0f)); // radians per second +/* + Serial.printf("[%lu] Gy: %d, %d, %d -- ", millis(), (int)gyros[0], (int)gyros[1], (int)gyros[2]); + Serial.print(gyro_q.angle()); + Serial.print(", "); + Serial.print(sensitivity * gyro_q.angle()); + Serial.print(" --> "); + Serial.println(filter.last()); +*/ + } + }; // noop + + /* + * handleOverlayDraw() is called just before every show() (LED strip update frame) after effects have set the colors. + * Use this to blank out some LEDs or set them to a different color regardless of the set effect mode. + * Commonly used for custom clocks (Cronixie, 7 segment) + */ + void handleOverlayDraw() + { + + // TODO: some kind of timing analysis for filtering ... + + // Calculate brightness boost + auto r_float = std::max(std::min(filter.last(), 1.0f), 0.f); + auto result = (uint8_t) (r_float * max); + //Serial.printf("[%lu] %d -- ", millis(), result); + //Serial.println(r_float); + // TODO - multiple segment handling?? + strip.getSegment(0).fadeToBlackBy(max - result); + } +}; + +const char GyroSurge::_name[] PROGMEM = "GyroSurge"; \ No newline at end of file diff --git a/usermods/mpu6050_imu/usermod_mpu6050_imu.h b/usermods/mpu6050_imu/usermod_mpu6050_imu.h index 748ddf1a6..a3301bfff 100644 --- a/usermods/mpu6050_imu/usermod_mpu6050_imu.h +++ b/usermods/mpu6050_imu/usermod_mpu6050_imu.h @@ -20,11 +20,11 @@ XCL not connected AD0 not connected INT D8 (GPIO15) Interrupt pin - + Using usermod: 1. Copy the usermod into the sketch folder (same folder as wled00.ino) 2. Register the usermod by adding #include "usermod_filename.h" in the top and registerUsermod(new MyUsermodClass()) in the bottom of usermods_list.cpp - 3. I2Cdev and MPU6050 must be installed as libraries, or else the .cpp/.h file + 3. I2Cdev and MPU6050 must be installed as libraries, or else the .cpp/.h file for both classes must be in the include path of your project. To install the libraries add I2Cdevlib-MPU6050@fbde122cc5 to lib_deps in the platformio.ini file. 4. You also need to change lib_compat_mode from strict to soft in platformio.ini (This ignores that I2Cdevlib-MPU6050 doesn't list platform compatibility) @@ -33,6 +33,9 @@ #include "I2Cdev.h" +#undef DEBUG_PRINT +#undef DEBUG_PRINTLN +#undef DEBUG_PRINTF #include "MPU6050_6Axis_MotionApps20.h" //#include "MPU6050.h" // not necessary if using MotionApps include file @@ -42,6 +45,23 @@ #include "Wire.h" #endif +// Restore debug macros +// MPU6050 unfortunately uses the same macro names as WLED :( +#undef DEBUG_PRINT +#undef DEBUG_PRINTLN +#undef DEBUG_PRINTF +#ifdef WLED_DEBUG + #define DEBUG_PRINT(x) DEBUGOUT.print(x) + #define DEBUG_PRINTLN(x) DEBUGOUT.println(x) + #define DEBUG_PRINTF(x...) DEBUGOUT.printf(x) +#else + #define DEBUG_PRINT(x) + #define DEBUG_PRINTLN(x) + #define DEBUG_PRINTF(x...) +#endif + + + // ================================================================ // === INTERRUPT DETECTION ROUTINE === // ================================================================ @@ -52,21 +72,31 @@ void IRAM_ATTR dmpDataReady() { } + class MPU6050Driver : public Usermod { private: MPU6050 mpu; - bool enabled = true; + + // configuration state + // default values are set in readFromConfig + // By making this a struct, we enable easy backup and comparison in the readFromConfig class + struct config_t { + bool enabled; + int8_t interruptPin; + int16_t gyro_offset[3]; + int16_t accel_offset[3]; + }; + config_t config; // MPU control/status vars + bool irqBound = false; // set true if we have bound the IRQ pin bool dmpReady = false; // set true if DMP init was successful - uint8_t mpuIntStatus; // holds actual interrupt status byte from MPU uint8_t devStatus; // return status after each device operation (0 = success, !0 = error) uint16_t packetSize; // expected DMP packet size (default is 42 bytes) uint16_t fifoCount; // count of all bytes currently in FIFO uint8_t fifoBuffer[64]; // FIFO storage buffer - //NOTE: some of these can be removed to save memory, processing time - // if the measurement isn't needed + // TODO: some of these can be removed to save memory, processing time if the measurement isn't needed Quaternion qat; // [w, x, y, z] quaternion container float euler[3]; // [psi, theta, phi] Euler angle container float ypr[3]; // [yaw, pitch, roll] yaw/pitch/roll container @@ -75,17 +105,67 @@ class MPU6050Driver : public Usermod { VectorInt16 aaReal; // [x, y, z] gravity-free accel sensor measurements VectorInt16 aaWorld; // [x, y, z] world-frame accel sensor measurements VectorFloat gravity; // [x, y, z] gravity vector + uint32 sample_count; - static const int INTERRUPT_PIN = 15; // use pin 15 on ESP8266 + // Usermod output + um_data_t um_data; + + // config element names as progmem strs + static const char _name[]; + static const char _enabled[]; + static const char _interrupt_pin[]; + static const char _x_acc_bias[]; + static const char _y_acc_bias[]; + static const char _z_acc_bias[]; + static const char _x_gyro_bias[]; + static const char _y_gyro_bias[]; + static const char _z_gyro_bias[]; public: - //Functions called by WLED + inline bool initDone() { return um_data.u_size != 0; }; // recycle this instead of storing an extra variable + + //Functions called by WLED /* * setup() is called once at boot. WiFi is not yet connected at this point. */ void setup() { - if (i2c_scl<0 || i2c_sda<0) { enabled = false; return; } + dmpReady = false; // Start clean + + // one time init + if (!initDone()) { + um_data.u_size = 9; + um_data.u_type = new um_types_t[um_data.u_size]; + um_data.u_data = new void*[um_data.u_size]; + um_data.u_data[0] = &qat; + um_data.u_type[0] = UMT_FLOAT_ARR; + um_data.u_data[1] = &euler; + um_data.u_type[1] = UMT_FLOAT_ARR; + um_data.u_data[2] = &ypr; + um_data.u_type[2] = UMT_FLOAT_ARR; + um_data.u_data[3] = &aa; + um_data.u_type[3] = UMT_INT16_ARR; + um_data.u_data[4] = &gy; + um_data.u_type[4] = UMT_INT16_ARR; + um_data.u_data[5] = &aaReal; + um_data.u_type[5] = UMT_INT16_ARR; + um_data.u_data[6] = &aaWorld; + um_data.u_type[6] = UMT_INT16_ARR; + um_data.u_data[7] = &gravity; + um_data.u_type[7] = UMT_FLOAT_ARR; + um_data.u_data[8] = &sample_count; + um_data.u_type[8] = UMT_UINT32; + } + + if (!config.enabled) return; + if (i2c_scl<0 || i2c_sda<0) { DEBUG_PRINTLN(F("MPU6050: I2C is no good.")); return; } + // Check the interrupt pin + if (config.interruptPin >= 0) { + irqBound = pinManager.allocatePin(config.interruptPin, false, PinOwner::UM_IMU); + if (!irqBound) { DEBUG_PRINTLN(F("MPU6050: IRQ pin already in use.")); return; } + pinMode(config.interruptPin, INPUT); + }; + #if I2CDEV_IMPLEMENTATION == I2CDEV_ARDUINO_WIRE Wire.setClock(400000U); // 400kHz I2C clock. Comment this line if having compilation difficulties #elif I2CDEV_IMPLEMENTATION == I2CDEV_BUILTIN_FASTWIRE @@ -95,7 +175,6 @@ class MPU6050Driver : public Usermod { // initialize device DEBUG_PRINTLN(F("Initializing I2C devices...")); mpu.initialize(); - pinMode(INTERRUPT_PIN, INPUT); // verify connection DEBUG_PRINTLN(F("Testing device connections...")); @@ -105,11 +184,16 @@ class MPU6050Driver : public Usermod { DEBUG_PRINTLN(F("Initializing DMP...")); devStatus = mpu.dmpInitialize(); - // supply your own gyro offsets here, scaled for min sensitivity - mpu.setXGyroOffset(220); - mpu.setYGyroOffset(76); - mpu.setZGyroOffset(-85); - mpu.setZAccelOffset(1788); // 1688 factory default for my test chip + // set offsets (from config) + mpu.setXGyroOffset(config.gyro_offset[0]); + mpu.setYGyroOffset(config.gyro_offset[1]); + mpu.setZGyroOffset(config.gyro_offset[2]); + mpu.setXAccelOffset(config.accel_offset[0]); + mpu.setYAccelOffset(config.accel_offset[1]); + mpu.setZAccelOffset(config.accel_offset[2]); + + // set sample rate + mpu.setRate(16); // ~100Hz // make sure it worked (returns 0 if so) if (devStatus == 0) { @@ -117,17 +201,19 @@ class MPU6050Driver : public Usermod { DEBUG_PRINTLN(F("Enabling DMP...")); mpu.setDMPEnabled(true); - // enable Arduino interrupt detection - DEBUG_PRINTLN(F("Enabling interrupt detection (Arduino external interrupt 0)...")); - attachInterrupt(digitalPinToInterrupt(INTERRUPT_PIN), dmpDataReady, RISING); - mpuIntStatus = mpu.getIntStatus(); - - // set our DMP Ready flag so the main loop() function knows it's okay to use it - DEBUG_PRINTLN(F("DMP ready! Waiting for first interrupt...")); - dmpReady = true; + mpuInterrupt = true; + if (irqBound) { + // enable Arduino interrupt detection + DEBUG_PRINTLN(F("Enabling interrupt detection (Arduino external interrupt 0)...")); + attachInterrupt(digitalPinToInterrupt(config.interruptPin), dmpDataReady, RISING); + } // get expected DMP packet size for later comparison packetSize = mpu.dmpGetFIFOPacketSize(); + + // set our DMP Ready flag so the main loop() function knows it's okay to use it + DEBUG_PRINTLN(F("DMP ready!")); + dmpReady = true; } else { // ERROR! // 1 = initial memory load failed @@ -137,6 +223,9 @@ class MPU6050Driver : public Usermod { DEBUG_PRINT(devStatus); DEBUG_PRINTLN(")"); } + + fifoCount = 0; + sample_count = 0; } /* @@ -153,28 +242,31 @@ class MPU6050Driver : public Usermod { */ void loop() { // if programming failed, don't try to do anything - if (!enabled || !dmpReady || strip.isUpdating()) return; + if (!config.enabled || !dmpReady || strip.isUpdating()) return; // wait for MPU interrupt or extra packet(s) available + // mpuInterrupt is fixed on if interrupt pin is disabled if (!mpuInterrupt && fifoCount < packetSize) return; // reset interrupt flag and get INT_STATUS byte - mpuInterrupt = false; - mpuIntStatus = mpu.getIntStatus(); - - // get current FIFO count + auto mpuIntStatus = mpu.getIntStatus(); + // Update current FIFO count fifoCount = mpu.getFIFOCount(); // check for overflow (this should never happen unless our code is too inefficient) if ((mpuIntStatus & 0x10) || fifoCount == 1024) { // reset so we can continue cleanly mpu.resetFIFO(); - DEBUG_PRINTLN(F("FIFO overflow!")); + DEBUG_PRINTLN(F("MPU6050: FIFO overflow!")); - // otherwise, check for DMP data ready interrupt (this should happen frequently) - } else if (mpuIntStatus & 0x02) { - // wait for correct available data length, should be a VERY short wait - while (fifoCount < packetSize) fifoCount = mpu.getFIFOCount(); + // otherwise, check for data ready + } else if (fifoCount >= packetSize) { + // clear local interrupt pending status, if not polling + mpuInterrupt = !irqBound; + + // DEBUG_PRINT(F("MPU6050: Processing packet: ")); + // DEBUG_PRINT(fifoCount); + // DEBUG_PRINTLN(F(" bytes in FIFO")); // read a packet from FIFO mpu.getFIFOBytes(fifoBuffer, packetSize); @@ -183,7 +275,6 @@ class MPU6050Driver : public Usermod { // (this lets us immediately read more without waiting for an interrupt) fifoCount -= packetSize; - //NOTE: some of these can be removed to save memory, processing time // if the measurement isn't needed mpu.dmpGetQuaternion(&qat, fifoBuffer); @@ -194,87 +285,141 @@ class MPU6050Driver : public Usermod { mpu.dmpGetLinearAccel(&aaReal, &aa, &gravity); mpu.dmpGetLinearAccelInWorld(&aaWorld, &aaReal, &qat); mpu.dmpGetYawPitchRoll(ypr, &qat, &gravity); + ++sample_count; } } - - void addToJsonInfo(JsonObject& root) { - int reading = 20; - //this code adds "u":{"Light":[20," lux"]} to the info object JsonObject user = root["u"]; if (user.isNull()) user = root.createNestedObject("u"); - JsonObject imu_meas = user.createNestedObject("IMU"); - JsonArray quat_json = imu_meas.createNestedArray("Quat"); + // Unfortunately the web UI doesn't know how to print sub-objects: you just see '[object Object]' + // For now, we just put everything in the root userdata object. + //auto imu_meas = user.createNestedObject("IMU"); + auto& imu_meas = user; + + // If an element is an array, the UI expects two elements in the form [value, unit] + // Since our /value/ is an array, wrap it, eg. [[a, b, c]] + JsonArray quat_json = imu_meas.createNestedArray("Quat").createNestedArray(); quat_json.add(qat.w); quat_json.add(qat.x); quat_json.add(qat.y); quat_json.add(qat.z); - JsonArray euler_json = imu_meas.createNestedArray("Euler"); + JsonArray euler_json = imu_meas.createNestedArray("Euler").createNestedArray(); euler_json.add(euler[0]); euler_json.add(euler[1]); euler_json.add(euler[2]); - JsonArray accel_json = imu_meas.createNestedArray("Accel"); + JsonArray accel_json = imu_meas.createNestedArray("Accel").createNestedArray(); accel_json.add(aa.x); accel_json.add(aa.y); accel_json.add(aa.z); - JsonArray gyro_json = imu_meas.createNestedArray("Gyro"); + JsonArray gyro_json = imu_meas.createNestedArray("Gyro").createNestedArray(); gyro_json.add(gy.x); gyro_json.add(gy.y); gyro_json.add(gy.z); - JsonArray world_json = imu_meas.createNestedArray("WorldAccel"); + JsonArray world_json = imu_meas.createNestedArray("WorldAccel").createNestedArray(); world_json.add(aaWorld.x); world_json.add(aaWorld.y); world_json.add(aaWorld.z); - JsonArray real_json = imu_meas.createNestedArray("RealAccel"); + JsonArray real_json = imu_meas.createNestedArray("RealAccel").createNestedArray(); real_json.add(aaReal.x); real_json.add(aaReal.y); real_json.add(aaReal.z); - JsonArray grav_json = imu_meas.createNestedArray("Gravity"); + JsonArray grav_json = imu_meas.createNestedArray("Gravity").createNestedArray(); grav_json.add(gravity.x); grav_json.add(gravity.y); grav_json.add(gravity.z); - JsonArray orient_json = imu_meas.createNestedArray("Orientation"); + JsonArray orient_json = imu_meas.createNestedArray("Orientation").createNestedArray(); orient_json.add(ypr[0]); orient_json.add(ypr[1]); orient_json.add(ypr[2]); } - /* - * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). - * Values in the state object may be modified by connected clients - */ - //void addToJsonState(JsonObject& root) - //{ - //root["user0"] = userVar0; - //} - - - /* - * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). - * Values in the state object may be modified by connected clients - */ - //void readFromJsonState(JsonObject& root) - //{ - //if (root["bri"] == 255) DEBUG_PRINTLN(F("Don't burn down your garage!")); - //} - - /* * addToConfig() can be used to add custom persistent settings to the cfg.json file in the "um" (usermod) object. * It will be called by WLED when settings are actually saved (for example, LED settings are saved) * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings! */ -// void addToConfig(JsonObject& root) -// { -// JsonObject top = root.createNestedObject("MPU6050_IMU"); -// JsonArray pins = top.createNestedArray("pin"); -// pins.add(HW_PIN_SCL); -// pins.add(HW_PIN_SDA); -// } + void addToConfig(JsonObject& root) + { + JsonObject top = root.createNestedObject(FPSTR(_name)); + + //save these vars persistently whenever settings are saved + top[FPSTR(_enabled)] = config.enabled; + top[FPSTR(_interrupt_pin)] = config.interruptPin; + top[FPSTR(_x_acc_bias)] = config.accel_offset[0]; + top[FPSTR(_y_acc_bias)] = config.accel_offset[1]; + top[FPSTR(_z_acc_bias)] = config.accel_offset[2]; + top[FPSTR(_x_gyro_bias)] = config.gyro_offset[0]; + top[FPSTR(_y_gyro_bias)] = config.gyro_offset[1]; + top[FPSTR(_z_gyro_bias)] = config.gyro_offset[2]; + } + + /* + * readFromConfig() can be used to read back the custom settings you added with addToConfig(). + * This is called by WLED when settings are loaded (currently this only happens immediately after boot, or after saving on the Usermod Settings page) + * + * readFromConfig() is called BEFORE setup(). This means you can use your persistent values in setup() (e.g. pin assignments, buffer sizes), + * but also that if you want to write persistent values to a dynamic buffer, you'd need to allocate it here instead of in setup. + * If you don't know what that is, don't fret. It most likely doesn't affect your use case :) + * + * Return true in case the config values returned from Usermod Settings were complete, or false if you'd like WLED to save your defaults to disk (so any missing values are editable in Usermod Settings) + * + * getJsonValue() returns false if the value is missing, or copies the value into the variable provided and returns true if the value is present + * The configComplete variable is true only if the "exampleUsermod" object and all values are present. If any values are missing, WLED will know to call addToConfig() to save them + * + * This function is guaranteed to be called on boot, but could also be called every time settings are updated + */ + bool readFromConfig(JsonObject& root) + { + // default settings values could be set here (or below using the 3-argument getJsonValue()) instead of in the class definition or constructor + // setting them inside readFromConfig() is slightly more robust, handling the rare but plausible use case of single value being missing after boot (e.g. if the cfg.json was manually edited and a value was removed) + auto old_cfg = config; + + JsonObject top = root[FPSTR(_name)]; + + bool configComplete = top.isNull(); + // Ensure default configuration is loaded + configComplete &= getJsonValue(top[FPSTR(_enabled)], config.enabled, true); + configComplete &= getJsonValue(top[FPSTR(_interrupt_pin)], config.interruptPin, -1); + configComplete &= getJsonValue(top[FPSTR(_x_acc_bias)], config.accel_offset[0], 0); + configComplete &= getJsonValue(top[FPSTR(_y_acc_bias)], config.accel_offset[1], 0); + configComplete &= getJsonValue(top[FPSTR(_z_acc_bias)], config.accel_offset[2], 0); + configComplete &= getJsonValue(top[FPSTR(_x_gyro_bias)], config.gyro_offset[0], 0); + configComplete &= getJsonValue(top[FPSTR(_y_gyro_bias)], config.gyro_offset[1], 0); + configComplete &= getJsonValue(top[FPSTR(_z_gyro_bias)], config.gyro_offset[2], 0); + + DEBUG_PRINT(FPSTR(_name)); + if (top.isNull()) { + DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); + } else if (!initDone()) { + DEBUG_PRINTLN(F(": config loaded.")); + } else if (memcmp(&config, &old_cfg, sizeof(config)) == 0) { + DEBUG_PRINTLN(F(": config unchanged.")); + } else { + DEBUG_PRINTLN(F(": config updated.")); + // Previously loaded and config changed + if (irqBound && ((old_cfg.interruptPin != config.interruptPin) || !config.enabled)) { + detachInterrupt(old_cfg.interruptPin); + pinManager.deallocatePin(old_cfg.interruptPin, PinOwner::UM_IMU); + irqBound = false; + } + + // Just re-init + setup(); + } + + return configComplete; + } + + bool getUMData(um_data_t **data) + { + if (!data || !config.enabled || !dmpReady) return false; // no pointer provided by caller or not enabled -> exit + *data = &um_data; + return true; + } /* * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). @@ -285,3 +430,14 @@ class MPU6050Driver : public Usermod { } }; + + +const char MPU6050Driver::_name[] PROGMEM = "MPU6050_IMU"; +const char MPU6050Driver::_enabled[] PROGMEM = "enabled"; +const char MPU6050Driver::_interrupt_pin[] PROGMEM = "interrupt_pin"; +const char MPU6050Driver::_x_acc_bias[] PROGMEM = "x_acc_bias"; +const char MPU6050Driver::_y_acc_bias[] PROGMEM = "y_acc_bias"; +const char MPU6050Driver::_z_acc_bias[] PROGMEM = "z_acc_bias"; +const char MPU6050Driver::_x_gyro_bias[] PROGMEM = "x_gyro_bias"; +const char MPU6050Driver::_y_gyro_bias[] PROGMEM = "y_gyro_bias"; +const char MPU6050Driver::_z_gyro_bias[] PROGMEM = "z_gyro_bias"; diff --git a/usermods/quinled-an-penta/quinled-an-penta.h b/usermods/quinled-an-penta/quinled-an-penta.h index 5153ee58a..10b784334 100644 --- a/usermods/quinled-an-penta/quinled-an-penta.h +++ b/usermods/quinled-an-penta/quinled-an-penta.h @@ -84,11 +84,11 @@ class QuinLEDAnPentaUsermod : public Usermod void getCurrentUsedLedPins() { for (int8_t lp = 0; lp <= 4; lp++) currentLedPins[lp] = 0; - byte numBusses = busses.getNumBusses(); + byte numBusses = BusManager::getNumBusses(); byte numUsedPins = 0; for (int8_t b = 0; b < numBusses; b++) { - Bus* curBus = busses.getBus(b); + Bus* curBus = BusManager::getBus(b); if (curBus != nullptr) { uint8_t pins[5] = {0, 0, 0, 0, 0}; currentBussesNumPins[b] = curBus->getPins(pins); @@ -104,11 +104,11 @@ class QuinLEDAnPentaUsermod : public Usermod void getCurrentLedcValues() { - byte numBusses = busses.getNumBusses(); + byte numBusses = BusManager::getNumBusses(); byte numLedc = 0; for (int8_t b = 0; b < numBusses; b++) { - Bus* curBus = busses.getBus(b); + Bus* curBus = BusManager::getBus(b); if (curBus != nullptr) { uint32_t curPixColor = curBus->getPixelColor(0); uint8_t _data[5] = {255, 255, 255, 255, 255}; diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 865c1ebac..a27fc4fbe 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -3444,8 +3444,8 @@ static const char _data_FX_MODE_STARBURST[] PROGMEM = "Fireworks Starburst@Chanc uint16_t mode_exploding_fireworks(void) { if (SEGLEN == 1) return mode_static(); - const uint16_t cols = strip.isMatrix ? SEGMENT.virtualWidth() : 1; - const uint16_t rows = strip.isMatrix ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); + const uint16_t cols = SEGMENT.is2D() ? SEGMENT.virtualWidth() : 1; + const uint16_t rows = SEGMENT.is2D() ? SEGMENT.virtualHeight() : SEGMENT.virtualLength(); //allocate segment data uint16_t maxData = FAIR_DATA_PER_SEG; //ESP8266: 256 ESP32: 640 @@ -3476,11 +3476,11 @@ uint16_t mode_exploding_fireworks(void) if (SEGENV.aux0 < 2) { //FLARE if (SEGENV.aux0 == 0) { //init flare flare->pos = 0; - flare->posX = strip.isMatrix ? random16(2,cols-3) : (SEGMENT.intensity > random8()); // will enable random firing side on 1D + flare->posX = SEGMENT.is2D() ? random16(2,cols-3) : (SEGMENT.intensity > random8()); // will enable random firing side on 1D uint16_t peakHeight = 75 + random8(180); //0-255 peakHeight = (peakHeight * (rows -1)) >> 8; flare->vel = sqrtf(-2.0f * gravity * peakHeight); - flare->velX = strip.isMatrix ? (random8(9)-4)/32.f : 0; // no X velocity on 1D + flare->velX = SEGMENT.is2D() ? (random8(9)-4)/64.0f : 0; // no X velocity on 1D flare->col = 255; //brightness SEGENV.aux0 = 1; } @@ -3488,12 +3488,14 @@ uint16_t mode_exploding_fireworks(void) // launch if (flare->vel > 12 * gravity) { // flare - if (strip.isMatrix) SEGMENT.setPixelColorXY(int(flare->posX), rows - uint16_t(flare->pos) - 1, flare->col, flare->col, flare->col); - else SEGMENT.setPixelColor(int(flare->posX) ? rows - int(flare->pos) - 1 : int(flare->pos), flare->col, flare->col, flare->col); + if (SEGMENT.is2D()) SEGMENT.setPixelColorXY(int(flare->posX), rows - uint16_t(flare->pos) - 1, flare->col, flare->col, flare->col); + else SEGMENT.setPixelColor((flare->posX > 0.0f) ? rows - int(flare->pos) - 1 : int(flare->pos), flare->col, flare->col, flare->col); flare->pos += flare->vel; - flare->posX += flare->velX; flare->pos = constrain(flare->pos, 0, rows-1); - flare->posX = constrain(flare->posX, 0, cols-strip.isMatrix); + if (SEGMENT.is2D()) { + flare->posX += flare->velX; + flare->posX = constrain(flare->posX, 0, cols-1); + } flare->vel += gravity; flare->col -= 2; } else { @@ -3516,12 +3518,12 @@ uint16_t mode_exploding_fireworks(void) sparks[i].posX = flare->posX; sparks[i].vel = (float(random16(20001)) / 10000.0f) - 0.9f; // from -0.9 to 1.1 sparks[i].vel *= rows<32 ? 0.5f : 1; // reduce velocity for smaller strips - sparks[i].velX = strip.isMatrix ? (float(random16(10001)) / 10000.0f) - 0.5f : 0; // from -0.5 to 0.5 + sparks[i].velX = SEGMENT.is2D() ? (float(random16(20001)) / 10000.0f) - 1.0f : 0; // from -1 to 1 sparks[i].col = 345;//abs(sparks[i].vel * 750.0); // set colors before scaling velocity to keep them bright //sparks[i].col = constrain(sparks[i].col, 0, 345); sparks[i].colIndex = random8(); sparks[i].vel *= flare->pos/rows; // proportional to height - sparks[i].velX *= strip.isMatrix ? flare->posX/cols : 0; // proportional to width + sparks[i].velX *= SEGMENT.is2D() ? flare->posX/cols : 0; // proportional to width sparks[i].vel *= -gravity *50; } //sparks[1].col = 345; // this will be our known spark @@ -3534,11 +3536,11 @@ uint16_t mode_exploding_fireworks(void) sparks[i].pos += sparks[i].vel; sparks[i].posX += sparks[i].velX; sparks[i].vel += *dying_gravity; - sparks[i].velX += strip.isMatrix ? *dying_gravity : 0; + sparks[i].velX += SEGMENT.is2D() ? *dying_gravity : 0; if (sparks[i].col > 3) sparks[i].col -= 4; if (sparks[i].pos > 0 && sparks[i].pos < rows) { - if (strip.isMatrix && !(sparks[i].posX >= 0 && sparks[i].posX < cols)) continue; + if (SEGMENT.is2D() && !(sparks[i].posX >= 0 && sparks[i].posX < cols)) continue; uint16_t prog = sparks[i].col; uint32_t spColor = (SEGMENT.palette) ? SEGMENT.color_wheel(sparks[i].colIndex) : SEGCOLOR(0); CRGB c = CRGB::Black; //HeatColor(sparks[i].col); @@ -3550,7 +3552,7 @@ uint16_t mode_exploding_fireworks(void) c.g = qsub8(c.g, cooling); c.b = qsub8(c.b, cooling * 2); } - if (strip.isMatrix) SEGMENT.setPixelColorXY(int(sparks[i].posX), rows - int(sparks[i].pos) - 1, c.red, c.green, c.blue); + if (SEGMENT.is2D()) SEGMENT.setPixelColorXY(int(sparks[i].posX), rows - int(sparks[i].pos) - 1, c.red, c.green, c.blue); else SEGMENT.setPixelColor(int(sparks[i].posX) ? rows - int(sparks[i].pos) - 1 : int(sparks[i].pos), c.red, c.green, c.blue); } } @@ -5292,7 +5294,6 @@ uint16_t mode_2Dmatrix(void) { // Matrix2D. By Jeremy Williams. if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed if (SEGENV.call == 0) { - memset(SEGMENT.data, 0, dataSize); // no falling spawns SEGMENT.fill(BLACK); SEGENV.step = 0; } diff --git a/wled00/FX.h b/wled00/FX.h index c200c45cd..c4725ec97 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -62,10 +62,10 @@ //#define FRAMETIME _frametime #define FRAMETIME strip.getFrameTime() -/* each segment uses 52 bytes of SRAM memory, so if you're application fails because of +/* each segment uses 82 bytes of SRAM memory, so if you're application fails because of insufficient memory, decreasing MAX_NUM_SEGMENTS may help */ #ifdef ESP8266 - #define MAX_NUM_SEGMENTS 16 + #define MAX_NUM_SEGMENTS 12 /* How much data bytes all segments combined may allocate */ #define MAX_SEGMENT_DATA 5120 #else @@ -73,9 +73,13 @@ #define MAX_NUM_SEGMENTS 32 #endif #if defined(ARDUINO_ARCH_ESP32S2) - #define MAX_SEGMENT_DATA 24576 + #if defined(BOARD_HAS_PSRAM) && defined(WLED_USE_PSRAM) + #define MAX_SEGMENT_DATA MAX_NUM_SEGMENTS*1024 // 32k by default + #else + #define MAX_SEGMENT_DATA MAX_NUM_SEGMENTS*768 // 24k by default + #endif #else - #define MAX_SEGMENT_DATA 32767 + #define MAX_SEGMENT_DATA MAX_NUM_SEGMENTS*1280 // 40k by default #endif #endif @@ -278,7 +282,7 @@ #define FX_MODE_RIPPLEPEAK 148 #define FX_MODE_2DFIRENOISE 149 #define FX_MODE_2DSQUAREDSWIRL 150 -#define FX_MODE_2DFIRE2012 151 +// #define FX_MODE_2DFIRE2012 151 #define FX_MODE_2DDNA 152 #define FX_MODE_2DMATRIX 153 #define FX_MODE_2DMETABALLS 154 @@ -288,7 +292,7 @@ #define FX_MODE_GRAVFREQ 158 #define FX_MODE_DJLIGHT 159 #define FX_MODE_2DFUNKYPLANK 160 -#define FX_MODE_2DCENTERBARS 161 +//#define FX_MODE_2DCENTERBARS 161 #define FX_MODE_2DPULSER 162 #define FX_MODE_BLURZ 163 #define FX_MODE_2DDRIFT 164 @@ -531,7 +535,7 @@ typedef struct Segment { #endif static void handleRandomPalette(); - void setUp(uint16_t i1, uint16_t i2, uint8_t grp=1, uint8_t spc=0, uint16_t ofs=UINT16_MAX, uint16_t i1Y=0, uint16_t i2Y=1, uint8_t segId = 255); + void setUp(uint16_t i1, uint16_t i2, uint8_t grp=1, uint8_t spc=0, uint16_t ofs=UINT16_MAX, uint16_t i1Y=0, uint16_t i2Y=1); bool setColor(uint8_t slot, uint32_t c); //returns true if changed void setCCT(uint16_t k); void setOpacity(uint8_t o); @@ -543,9 +547,9 @@ typedef struct Segment { // runtime data functions inline uint16_t dataSize(void) const { return _dataLen; } - bool allocateData(size_t len); - void deallocateData(void); - void resetIfRequired(void); + bool allocateData(size_t len); // allocates effect data buffer in heap and clears it + void deallocateData(void); // deallocates (frees) effect data buffer from heap + void resetIfRequired(void); // sets all SEGENV variables to 0 and clears data buffer /** * Flags that before the next effect is calculated, * the internal segment state should be reset. @@ -555,17 +559,17 @@ typedef struct Segment { inline void markForReset(void) { reset = true; } // setOption(SEG_OPTION_RESET, true) // transition functions - void startTransition(uint16_t dur); // transition has to start before actual segment values change - void stopTransition(void); + void startTransition(uint16_t dur); // transition has to start before actual segment values change + void stopTransition(void); // ends transition mode by destroying transition structure void handleTransition(void); #ifndef WLED_DISABLE_MODE_BLEND - void swapSegenv(tmpsegd_t &tmpSegD); - void restoreSegenv(tmpsegd_t &tmpSegD); + void swapSegenv(tmpsegd_t &tmpSegD); // copies segment data into specifed buffer, if buffer is not a transition buffer, segment data is overwritten from transition buffer + void restoreSegenv(tmpsegd_t &tmpSegD); // restores segment data from buffer, if buffer is not transition buffer, changed values are copied to transition buffer #endif - uint16_t progress(void); //transition progression between 0-65535 - uint8_t currentBri(bool useCct = false); - uint8_t currentMode(void); - uint32_t currentColor(uint8_t slot); + uint16_t progress(void); // transition progression between 0-65535 + uint8_t currentBri(bool useCct = false); // current segment brightness/CCT (blended while in transition) + uint8_t currentMode(void); // currently active effect/mode (while in transition) + uint32_t currentColor(uint8_t slot); // currently active segment color (blended while in transition) CRGBPalette16 &loadPalette(CRGBPalette16 &tgt, uint8_t pal); CRGBPalette16 ¤tPalette(CRGBPalette16 &tgt, uint8_t paletteID); @@ -594,9 +598,9 @@ typedef struct Segment { uint32_t color_wheel(uint8_t pos); // 2D matrix - uint16_t virtualWidth(void) const; - uint16_t virtualHeight(void) const; - uint16_t nrOfVStrips(void) const; + uint16_t virtualWidth(void) const; // segment width in virtual pixels (accounts for groupping and spacing) + uint16_t virtualHeight(void) const; // segment height in virtual pixels (accounts for groupping and spacing) + uint16_t nrOfVStrips(void) const; // returns number of virtual vertical strips in 2D matrix (used to expand 1D effects into 2D) #ifndef WLED_DISABLE_2D uint16_t XY(uint16_t x, uint16_t y); // support function to get relative index within segment void setPixelColorXY(int x, int y, uint32_t c); // set relative pixel within segment with color @@ -682,10 +686,7 @@ class WS2812FX { // 96 bytes WS2812FX() : paletteFade(0), paletteBlend(0), - milliampsPerLed(55), cctBlending(0), - ablMilliampsMax(ABL_MILLIAMPS_DEFAULT), - currentMilliamps(0), now(millis()), timebase(0), isMatrix(false), @@ -697,6 +698,7 @@ class WS2812FX { // 96 bytes _colors_t{0,0,0}, _virtualSegmentLength(0), // true private variables + _suspend(false), _length(DEFAULT_LED_COUNT), _brightness(DEFAULT_BRIGHTNESS), _transitionDur(750), @@ -745,38 +747,39 @@ class WS2812FX { // 96 bytes void #ifdef WLED_DEBUG - printSize(), + printSize(), // prints memory usage for strip components #endif - finalizeInit(), - service(void), - setMode(uint8_t segid, uint8_t m), - setColor(uint8_t slot, uint32_t c), - setCCT(uint16_t k), - setBrightness(uint8_t b, bool direct = false), - setRange(uint16_t i, uint16_t i2, uint32_t col), - purgeSegments(bool force = false), + finalizeInit(), // initialises strip components + service(void), // executes effect functions when due and calls strip.show() + setMode(uint8_t segid, uint8_t m), // sets effect/mode for given segment (high level API) + setColor(uint8_t slot, uint32_t c), // sets color (in slot) for given segment (high level API) + setCCT(uint16_t k), // sets global CCT (either in relative 0-255 value or in K) + setBrightness(uint8_t b, bool direct = false), // sets strip brightness + setRange(uint16_t i, uint16_t i2, uint32_t col), // used for clock overlay + purgeSegments(void), // removes inactive segments from RAM (may incure penalty and memory fragmentation but reduces vector footprint) setSegment(uint8_t n, uint16_t start, uint16_t stop, uint8_t grouping = 1, uint8_t spacing = 0, uint16_t offset = UINT16_MAX, uint16_t startY=0, uint16_t stopY=1), setMainSegmentId(uint8_t n), - resetSegments(), - makeAutoSegments(bool forceReset = false), - fixInvalidSegments(), - setPixelColor(int n, uint32_t c), - show(void), + resetSegments(), // marks all segments for reset + makeAutoSegments(bool forceReset = false), // will create segments based on configured outputs + fixInvalidSegments(), // fixes incorrect segment configuration + setPixelColor(unsigned n, uint32_t c), // paints absolute strip pixel with index n and color c + show(void), // initiates LED output setTargetFps(uint8_t fps), addEffect(uint8_t id, mode_ptr mode_fn, const char *mode_name), // add effect to the list; defined in FX.cpp - setupEffectData(void); // add default effects to the list; defined in FX.cpp + setupEffectData(void); // add default effects to the list; defined in FX.cpp - inline void restartRuntime() { for (Segment &seg : _segments) seg.markForReset(); } + inline void restartRuntime() { for (Segment &seg : _segments) seg.markForReset(); } inline void setTransitionMode(bool t) { for (Segment &seg : _segments) seg.startTransition(t ? _transitionDur : 0); } - inline void setColor(uint8_t slot, uint8_t r, uint8_t g, uint8_t b, uint8_t w = 0) { setColor(slot, RGBW32(r,g,b,w)); } - inline void fill(uint32_t c) { for (int i = 0; i < getLengthTotal(); i++) setPixelColor(i, c); } // fill whole strip with color (inline) - // outsmart the compiler :) by correctly overloading - inline void setPixelColor(int n, uint8_t r, uint8_t g, uint8_t b, uint8_t w = 0) { setPixelColor(n, RGBW32(r,g,b,w)); } - inline void setPixelColor(int n, CRGB c) { setPixelColor(n, c.red, c.green, c.blue); } - inline void trigger(void) { _triggered = true; } // Forces the next frame to be computed on all active segments. - inline void setShowCallback(show_callback cb) { _callback = cb; } - inline void setTransition(uint16_t t) { _transitionDur = t; } + inline void setColor(uint8_t slot, uint8_t r, uint8_t g, uint8_t b, uint8_t w = 0) { setColor(slot, RGBW32(r,g,b,w)); } + inline void setPixelColor(unsigned n, uint8_t r, uint8_t g, uint8_t b, uint8_t w = 0) { setPixelColor(n, RGBW32(r,g,b,w)); } + inline void setPixelColor(unsigned n, CRGB c) { setPixelColor(n, c.red, c.green, c.blue); } + inline void fill(uint32_t c) { for (unsigned i = 0; i < getLengthTotal(); i++) setPixelColor(i, c); } // fill whole strip with color (inline) + inline void trigger(void) { _triggered = true; } // Forces the next frame to be computed on all active segments. + inline void setShowCallback(show_callback cb) { _callback = cb; } + inline void setTransition(uint16_t t) { _transitionDur = t; } // sets transition time (in ms) inline void appendSegment(const Segment &seg = Segment()) { if (_segments.size() < getMaxSegments()) _segments.push_back(seg); } + inline void suspend(void) { _suspend = true; } // will suspend (and canacel) strip.service() execution + inline void resume(void) { _suspend = false; } // will resume strip.service() execution bool paletteFade, @@ -787,13 +790,14 @@ class WS2812FX { // 96 bytes isUpdating(void), deserializeMap(uint8_t n=0); - inline bool isServicing(void) { return _isServicing; } - inline bool hasWhiteChannel(void) {return _hasWhiteChannel;} - inline bool isOffRefreshRequired(void) {return _isOffRefreshRequired;} + inline bool isServicing(void) { return _isServicing; } // returns true if strip.service() is executing + inline bool hasWhiteChannel(void) { return _hasWhiteChannel; } // returns true if strip contains separate white chanel + inline bool isOffRefreshRequired(void) { return _isOffRefreshRequired; } // returns true if strip requires regular updates (i.e. TM1814 chipset) + inline bool isSuspended(void) { return _suspend; } // returns true if strip.service() execution is suspended + inline bool needsUpdate(void) { return _triggered; } // returns true if strip received a trigger() request uint8_t paletteBlend, - milliampsPerLed, cctBlending, getActiveSegmentsNum(void), getFirstSelectedSegId(void), @@ -801,35 +805,33 @@ class WS2812FX { // 96 bytes getActiveSegsLightCapabilities(bool selectedOnly = false), setPixelSegment(uint8_t n); - inline uint8_t getBrightness(void) { return _brightness; } - inline uint8_t getMaxSegments(void) { return MAX_NUM_SEGMENTS; } // returns maximum number of supported segments (fixed value) - inline uint8_t getSegmentsNum(void) { return _segments.size(); } // returns currently present segments - inline uint8_t getCurrSegmentId(void) { return _segment_index; } - inline uint8_t getMainSegmentId(void) { return _mainSegment; } - inline uint8_t getPaletteCount() { return 13 + GRADIENT_PALETTE_COUNT; } // will only return built-in palette count - inline uint8_t getTargetFps() { return _targetFps; } - inline uint8_t getModeCount() { return _modeCount; } + inline uint8_t getBrightness(void) { return _brightness; } // returns current strip brightness + inline uint8_t getMaxSegments(void) { return MAX_NUM_SEGMENTS; } // returns maximum number of supported segments (fixed value) + inline uint8_t getSegmentsNum(void) { return _segments.size(); } // returns currently present segments + inline uint8_t getCurrSegmentId(void) { return _segment_index; } // returns current segment index (only valid while strip.isServicing()) + inline uint8_t getMainSegmentId(void) { return _mainSegment; } // returns main segment index + inline uint8_t getPaletteCount() { return 13 + GRADIENT_PALETTE_COUNT; } // will only return built-in palette count + inline uint8_t getTargetFps() { return _targetFps; } // returns rough FPS value for las 2s interval + inline uint8_t getModeCount() { return _modeCount; } // returns number of registered modes/effects uint16_t - ablMilliampsMax, - currentMilliamps, getLengthPhysical(void), getLengthTotal(void), // will include virtual/nonexistent pixels in matrix getFps(), getMappedPixelIndex(uint16_t index); - inline uint16_t getFrameTime(void) { return _frametime; } - inline uint16_t getMinShowDelay(void) { return MIN_SHOW_DELAY; } - inline uint16_t getLength(void) { return _length; } // 2D matrix may have less pixels than W*H - inline uint16_t getTransition(void) { return _transitionDur; } + inline uint16_t getFrameTime(void) { return _frametime; } // returns amount of time a frame should take (in ms) + inline uint16_t getMinShowDelay(void) { return MIN_SHOW_DELAY; } // returns minimum amount of time strip.service() can be delayed (constant) + inline uint16_t getLength(void) { return _length; } // returns actual amount of LEDs on a strip (2D matrix may have less LEDs than W*H) + inline uint16_t getTransition(void) { return _transitionDur; } // returns currently set transition time (in ms) uint32_t now, timebase, getPixelColor(uint16_t); - inline uint32_t getLastShow(void) { return _lastShow; } - inline uint32_t segColor(uint8_t i) { return _colors_t[i]; } + inline uint32_t getLastShow(void) { return _lastShow; } // returns millis() timestamp of last strip.show() call + inline uint32_t segColor(uint8_t i) { return _colors_t[i]; } // returns currently valid color (for slot i) AKA SEGCOLOR(); may be blended between two colors while in transition const char * getModeData(uint8_t id = 0) { return (id && id<_modeCount) ? _modeData[id] : PSTR("Solid"); } @@ -838,9 +840,9 @@ class WS2812FX { // 96 bytes getModeDataSrc(void) { return &(_modeData[0]); } // vectors use arrays for underlying data Segment& getSegment(uint8_t id); - inline Segment& getFirstSelectedSeg(void) { return _segments[getFirstSelectedSegId()]; } - inline Segment& getMainSegment(void) { return _segments[getMainSegmentId()]; } - inline Segment* getSegments(void) { return &(_segments[0]); } + inline Segment& getFirstSelectedSeg(void) { return _segments[getFirstSelectedSegId()]; } // returns reference to first segment that is "selected" + inline Segment& getMainSegment(void) { return _segments[getMainSegmentId()]; } // returns reference to main segment + inline Segment* getSegments(void) { return &(_segments[0]); } // returns pointer to segment vector structure (warning: use carefully) // 2D support (panels) bool @@ -876,10 +878,10 @@ class WS2812FX { // 96 bytes std::vector panel; #endif - void setUpMatrix(); + void setUpMatrix(); // sets up automatic matrix ledmap from panel configuration // outsmart the compiler :) by correctly overloading - inline void setPixelColorXY(int x, int y, uint32_t c) { setPixelColor(y * Segment::maxWidth + x, c); } + inline void setPixelColorXY(int x, int y, uint32_t c) { setPixelColor((unsigned)(y * Segment::maxWidth + x), c); } inline void setPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0) { setPixelColorXY(x, y, RGBW32(r,g,b,w)); } inline void setPixelColorXY(int x, int y, CRGB c) { setPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0)); } @@ -900,6 +902,8 @@ class WS2812FX { // 96 bytes friend class Segment; private: + volatile bool _suspend; + uint16_t _length; uint8_t _brightness; uint16_t _transitionDur; @@ -933,9 +937,10 @@ class WS2812FX { // 96 bytes uint16_t _qStart, _qStop, _qStartY, _qStopY; uint8_t _qGrouping, _qSpacing; uint16_t _qOffset; - +/* void setUpSegmentFromQueuedChanges(void); +*/ }; extern const char JSON_mode_names[]; diff --git a/wled00/FX_2Dfcn.cpp b/wled00/FX_2Dfcn.cpp index d2622f901..6f5075d08 100644 --- a/wled00/FX_2Dfcn.cpp +++ b/wled00/FX_2Dfcn.cpp @@ -36,14 +36,9 @@ // so matrix should disable regular ledmap processing void WS2812FX::setUpMatrix() { #ifndef WLED_DISABLE_2D - // erase old ledmap, just in case. - if (customMappingTable != nullptr) delete[] customMappingTable; - customMappingTable = nullptr; - customMappingSize = 0; - // isMatrix is set in cfg.cpp or set.cpp if (isMatrix) { - // calculate width dynamically because it will have gaps + // calculate width dynamically because it may have gaps Segment::maxWidth = 1; Segment::maxHeight = 1; for (size_t i = 0; i < panel.size(); i++) { @@ -68,15 +63,17 @@ void WS2812FX::setUpMatrix() { return; } - customMappingTable = new uint16_t[Segment::maxWidth * Segment::maxHeight]; + customMappingSize = 0; // prevent use of mapping if anything goes wrong + + if (customMappingTable == nullptr) customMappingTable = new uint16_t[getLengthTotal()]; if (customMappingTable != nullptr) { - customMappingSize = Segment::maxWidth * Segment::maxHeight; + customMappingSize = getLengthTotal(); // fill with empty in case we don't fill the entire matrix - for (size_t i = 0; i< customMappingSize; i++) { - customMappingTable[i] = (uint16_t)-1; - } + unsigned matrixSize = Segment::maxWidth * Segment::maxHeight; + for (unsigned i = 0; i(); + JsonArray map = pDoc->as(); gapSize = map.size(); - if (!map.isNull() && gapSize >= customMappingSize) { // not an empty map + if (!map.isNull() && gapSize >= matrixSize) { // not an empty map gapTable = new int8_t[gapSize]; if (gapTable) for (size_t i = 0; i < gapSize; i++) { gapTable[i] = constrain(map[i], -1, 1); diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index d53c99f4a..96e397aba 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -143,27 +143,28 @@ Segment& Segment::operator= (Segment &&orig) noexcept { return *this; } +// allocates effect data buffer on heap and initialises (erases) it bool IRAM_ATTR Segment::allocateData(size_t len) { + if (len == 0) return false; // nothing to do if (data && _dataLen >= len) { // already allocated enough (reduce fragmentation) if (call == 0) memset(data, 0, len); // erase buffer if called during effect initialisation return true; } //DEBUG_PRINTF("-- Allocating data (%d): %p\n", len, this); - deallocateData(); - if (len == 0) return false; // nothing to do + deallocateData(); // if the old buffer was smaller release it first if (Segment::getUsedSegmentData() + len > MAX_SEGMENT_DATA) { // not enough memory DEBUG_PRINT(F("!!! Effect RAM depleted: ")); DEBUG_PRINTF("%d/%d !!!\n", len, Segment::getUsedSegmentData()); + errorFlag = ERR_NORAM; return false; } // do not use SPI RAM on ESP32 since it is slow - data = (byte*) malloc(len); + data = (byte*)calloc(len, sizeof(byte)); if (!data) { DEBUG_PRINTLN(F("!!! Allocation failed. !!!")); return false; } // allocation failed Segment::addUsedSegmentData(len); //DEBUG_PRINTF("--- Allocated data (%p): %d/%d -> %p\n", this, len, Segment::getUsedSegmentData(), data); _dataLen = len; - memset(data, 0, len); return true; } @@ -194,7 +195,7 @@ void IRAM_ATTR Segment::deallocateData() { void Segment::resetIfRequired() { if (!reset) return; //DEBUG_PRINTF("-- Segment reset: %p\n", this); - deallocateData(); + if (data && _dataLen > 0) memset(data, 0, _dataLen); // prevent heap fragmentation (just erase buffer instead of deallocateData()) next_time = 0; step = 0; call = 0; aux0 = 0; aux1 = 0; reset = false; } @@ -468,7 +469,7 @@ void Segment::handleRandomPalette() { } // segId is given when called from network callback, changes are queued if that segment is currently in its effect function -void Segment::setUp(uint16_t i1, uint16_t i2, uint8_t grp, uint8_t spc, uint16_t ofs, uint16_t i1Y, uint16_t i2Y, uint8_t segId) { +void Segment::setUp(uint16_t i1, uint16_t i2, uint8_t grp, uint8_t spc, uint16_t ofs, uint16_t i1Y, uint16_t i2Y) { // return if neither bounds nor grouping have changed bool boundsUnchanged = (start == i1 && stop == i2); #ifndef WLED_DISABLE_2D @@ -561,35 +562,36 @@ void Segment::setOption(uint8_t n, bool val) { } void Segment::setMode(uint8_t fx, bool loadDefaults) { + // skip reserved + while (fx < strip.getModeCount() && strncmp_P("RSVD", strip.getModeData(fx), 4) == 0) fx++; + if (fx >= strip.getModeCount()) fx = 0; // set solid mode // if we have a valid mode & is not reserved - if (fx < strip.getModeCount() && strncmp_P("RSVD", strip.getModeData(fx), 4)) { - if (fx != mode) { + if (fx != mode) { #ifndef WLED_DISABLE_MODE_BLEND - if (modeBlending) startTransition(strip.getTransition()); // set effect transitions + if (modeBlending) startTransition(strip.getTransition()); // set effect transitions #endif - mode = fx; - // load default values from effect string - if (loadDefaults) { - int16_t sOpt; - sOpt = extractModeDefaults(fx, "sx"); speed = (sOpt >= 0) ? sOpt : DEFAULT_SPEED; - sOpt = extractModeDefaults(fx, "ix"); intensity = (sOpt >= 0) ? sOpt : DEFAULT_INTENSITY; - sOpt = extractModeDefaults(fx, "c1"); custom1 = (sOpt >= 0) ? sOpt : DEFAULT_C1; - sOpt = extractModeDefaults(fx, "c2"); custom2 = (sOpt >= 0) ? sOpt : DEFAULT_C2; - sOpt = extractModeDefaults(fx, "c3"); custom3 = (sOpt >= 0) ? sOpt : DEFAULT_C3; - sOpt = extractModeDefaults(fx, "o1"); check1 = (sOpt >= 0) ? (bool)sOpt : false; - sOpt = extractModeDefaults(fx, "o2"); check2 = (sOpt >= 0) ? (bool)sOpt : false; - sOpt = extractModeDefaults(fx, "o3"); check3 = (sOpt >= 0) ? (bool)sOpt : false; - sOpt = extractModeDefaults(fx, "m12"); if (sOpt >= 0) map1D2D = constrain(sOpt, 0, 7); else map1D2D = M12_Pixels; // reset mapping if not defined (2D FX may not work) - sOpt = extractModeDefaults(fx, "si"); if (sOpt >= 0) soundSim = constrain(sOpt, 0, 3); - sOpt = extractModeDefaults(fx, "rev"); if (sOpt >= 0) reverse = (bool)sOpt; - sOpt = extractModeDefaults(fx, "mi"); if (sOpt >= 0) mirror = (bool)sOpt; // NOTE: setting this option is a risky business - sOpt = extractModeDefaults(fx, "rY"); if (sOpt >= 0) reverse_y = (bool)sOpt; - sOpt = extractModeDefaults(fx, "mY"); if (sOpt >= 0) mirror_y = (bool)sOpt; // NOTE: setting this option is a risky business - sOpt = extractModeDefaults(fx, "pal"); if (sOpt >= 0) setPalette(sOpt); //else setPalette(0); - } - markForReset(); - stateChanged = true; // send UDP/WS broadcast + mode = fx; + // load default values from effect string + if (loadDefaults) { + int16_t sOpt; + sOpt = extractModeDefaults(fx, "sx"); speed = (sOpt >= 0) ? sOpt : DEFAULT_SPEED; + sOpt = extractModeDefaults(fx, "ix"); intensity = (sOpt >= 0) ? sOpt : DEFAULT_INTENSITY; + sOpt = extractModeDefaults(fx, "c1"); custom1 = (sOpt >= 0) ? sOpt : DEFAULT_C1; + sOpt = extractModeDefaults(fx, "c2"); custom2 = (sOpt >= 0) ? sOpt : DEFAULT_C2; + sOpt = extractModeDefaults(fx, "c3"); custom3 = (sOpt >= 0) ? sOpt : DEFAULT_C3; + sOpt = extractModeDefaults(fx, "o1"); check1 = (sOpt >= 0) ? (bool)sOpt : false; + sOpt = extractModeDefaults(fx, "o2"); check2 = (sOpt >= 0) ? (bool)sOpt : false; + sOpt = extractModeDefaults(fx, "o3"); check3 = (sOpt >= 0) ? (bool)sOpt : false; + sOpt = extractModeDefaults(fx, "m12"); if (sOpt >= 0) map1D2D = constrain(sOpt, 0, 7); else map1D2D = M12_Pixels; // reset mapping if not defined (2D FX may not work) + sOpt = extractModeDefaults(fx, "si"); if (sOpt >= 0) soundSim = constrain(sOpt, 0, 3); + sOpt = extractModeDefaults(fx, "rev"); if (sOpt >= 0) reverse = (bool)sOpt; + sOpt = extractModeDefaults(fx, "mi"); if (sOpt >= 0) mirror = (bool)sOpt; // NOTE: setting this option is a risky business + sOpt = extractModeDefaults(fx, "rY"); if (sOpt >= 0) reverse_y = (bool)sOpt; + sOpt = extractModeDefaults(fx, "mY"); if (sOpt >= 0) mirror_y = (bool)sOpt; // NOTE: setting this option is a risky business + sOpt = extractModeDefaults(fx, "pal"); if (sOpt >= 0) setPalette(sOpt); //else setPalette(0); } + markForReset(); + stateChanged = true; // send UDP/WS broadcast } } @@ -751,10 +753,10 @@ void IRAM_ATTR Segment::setPixelColor(int i, uint32_t col) uint32_t tmpCol = col; // set all the pixels in the group for (int j = 0; j < grouping; j++) { - uint16_t indexSet = i + ((reverse) ? -j : j); + unsigned indexSet = i + ((reverse) ? -j : j); if (indexSet >= start && indexSet < stop) { if (mirror) { //set the corresponding mirrored pixel - uint16_t indexMir = stop - indexSet + start - 1; + unsigned indexMir = stop - indexSet + start - 1; indexMir += offset; // offset/phase if (indexMir >= stop) indexMir -= len; // wrap #ifndef WLED_DISABLE_MODE_BLEND @@ -897,8 +899,8 @@ void Segment::refreshLightCapabilities() { segStopIdx = stop; } - for (unsigned b = 0; b < busses.getNumBusses(); b++) { - Bus *bus = busses.getBus(b); + for (unsigned b = 0; b < BusManager::getNumBusses(); b++) { + Bus *bus = BusManager::getBus(b); if (bus == nullptr || bus->getLength()==0) break; if (!bus->isOk()) continue; if (bus->getStart() >= segStopIdx) continue; @@ -1083,7 +1085,7 @@ void WS2812FX::finalizeInit(void) { _hasWhiteChannel = _isOffRefreshRequired = false; //if busses failed to load, add default (fresh install, FS issue, ...) - if (busses.getNumBusses() == 0) { + if (BusManager::getNumBusses() == 0) { DEBUG_PRINTLN(F("No busses, init default")); const uint8_t defDataPins[] = {DATA_PINS}; const uint16_t defCounts[] = {PIXEL_COUNTS}; @@ -1096,13 +1098,13 @@ void WS2812FX::finalizeInit(void) { uint16_t count = defCounts[(i < defNumCounts) ? i : defNumCounts -1]; prevLen += count; BusConfig defCfg = BusConfig(DEFAULT_LED_TYPE, defPin, start, count, DEFAULT_LED_COLOR_ORDER, false, 0, RGBW_MODE_MANUAL_ONLY); - if (busses.add(defCfg) == -1) break; + if (BusManager::add(defCfg) == -1) break; } } _length = 0; - for (int i=0; igetStart() + bus->getLength() > MAX_LEDS) break; //RGBW mode is enabled if at least one of the strips is RGBW @@ -1120,29 +1122,28 @@ void WS2812FX::finalizeInit(void) { #endif } - if (isMatrix) setUpMatrix(); - else { - Segment::maxWidth = _length; - Segment::maxHeight = 1; - } + Segment::maxWidth = _length; + Segment::maxHeight = 1; //segments are created in makeAutoSegments(); DEBUG_PRINTLN(F("Loading custom palettes")); loadCustomPalettes(); // (re)load all custom palettes DEBUG_PRINTLN(F("Loading custom ledmaps")); - deserializeMap(); // (re)load default ledmap + deserializeMap(); // (re)load default ledmap (will also setUpMatrix() if ledmap does not exist) } void WS2812FX::service() { unsigned long nowUp = millis(); // Be aware, millis() rolls over every 49 days now = nowUp + timebase; - if (nowUp - _lastShow < MIN_SHOW_DELAY) return; + if (nowUp - _lastShow < MIN_SHOW_DELAY || _suspend) return; bool doShow = false; _isServicing = true; _segment_index = 0; Segment::handleRandomPalette(); // move it into for loop when each segment has individual random palette for (segment &seg : _segments) { + if (_suspend) return; // immediately stop processing segments if suspend requested during service() + // process transition (mode changes in the middle of transition) seg.handleTransition(); // reset the segment runtime data if needed @@ -1162,7 +1163,7 @@ void WS2812FX::service() { _colors_t[1] = gamma32(seg.currentColor(1)); _colors_t[2] = gamma32(seg.currentColor(2)); seg.currentPalette(_currentPalette, seg.palette); // we need to pass reference - if (!cctFromRgb || correctWB) busses.setSegmentCCT(seg.currentBri(true), correctWB); + if (!cctFromRgb || correctWB) BusManager::setSegmentCCT(seg.currentBri(true), correctWB); // Effect blending // When two effects are being blended, each may have different segment data, this // data needs to be saved first and then restored before running previous mode. @@ -1189,11 +1190,11 @@ void WS2812FX::service() { seg.next_time = nowUp + delay; } - if (_segment_index == _queuedChangesSegId) setUpSegmentFromQueuedChanges(); +// if (_segment_index == _queuedChangesSegId) setUpSegmentFromQueuedChanges(); _segment_index++; } _virtualSegmentLength = 0; - busses.setSegmentCCT(-1); + BusManager::setSegmentCCT(-1); _isServicing = false; _triggered = false; @@ -1209,16 +1210,16 @@ void WS2812FX::service() { #endif } -void IRAM_ATTR WS2812FX::setPixelColor(int i, uint32_t col) { +void IRAM_ATTR WS2812FX::setPixelColor(unsigned i, uint32_t col) { i = getMappedPixelIndex(i); if (i >= _length) return; - busses.setPixelColor(i, col); + BusManager::setPixelColor(i, col); } uint32_t IRAM_ATTR WS2812FX::getPixelColor(uint16_t i) { i = getMappedPixelIndex(i); if (i >= _length) return 0; - return busses.getPixelColor(i); + return BusManager::getPixelColor(i); } void WS2812FX::show(void) { @@ -1229,7 +1230,7 @@ void WS2812FX::show(void) { // some buses send asynchronously and this method will return before // all of the data has been sent. // See https://github.com/Makuna/NeoPixelBus/wiki/ESP32-NeoMethods#neoesp32rmt-methods - busses.show(); + BusManager::show(); unsigned long showNow = millis(); size_t diff = showNow - _lastShow; @@ -1244,7 +1245,7 @@ void WS2812FX::show(void) { * On some hardware (ESP32), strip updates are done asynchronously. */ bool WS2812FX::isUpdating() { - return !busses.canAllShow(); + return !BusManager::canAllShow(); } /** @@ -1303,7 +1304,7 @@ void WS2812FX::setBrightness(uint8_t b, bool direct) { } // setting brightness with NeoPixelBusLg has no effect on already painted pixels, // so we need to force an update to existing buffer - busses.setBrightness(b); + BusManager::setBrightness(b); if (!direct) { unsigned long t = millis(); if (_segments[0].next_time > t + 22 && t - _lastShow > MIN_SHOW_DELAY) trigger(); //apply brightness change immediately if no refresh soon @@ -1359,8 +1360,8 @@ uint16_t WS2812FX::getLengthTotal(void) { uint16_t WS2812FX::getLengthPhysical(void) { uint16_t len = 0; - for (size_t b = 0; b < busses.getNumBusses(); b++) { - Bus *bus = busses.getBus(b); + for (size_t b = 0; b < BusManager::getNumBusses(); b++) { + Bus *bus = BusManager::getBus(b); if (bus->getType() >= TYPE_NET_DDP_RGB) continue; //exclude non-physical network busses len += bus->getLength(); } @@ -1371,8 +1372,8 @@ uint16_t WS2812FX::getLengthPhysical(void) { //returns if there is an RGBW bus (supports RGB and White, not only white) //not influenced by auto-white mode, also true if white slider does not affect output white channel bool WS2812FX::hasRGBWBus(void) { - for (size_t b = 0; b < busses.getNumBusses(); b++) { - Bus *bus = busses.getBus(b); + for (size_t b = 0; b < BusManager::getNumBusses(); b++) { + Bus *bus = BusManager::getBus(b); if (bus == nullptr || bus->getLength()==0) break; if (bus->hasRGB() && bus->hasWhite()) return true; } @@ -1381,8 +1382,8 @@ bool WS2812FX::hasRGBWBus(void) { bool WS2812FX::hasCCTBus(void) { if (cctFromRgb && !correctWB) return false; - for (size_t b = 0; b < busses.getNumBusses(); b++) { - Bus *bus = busses.getBus(b); + for (size_t b = 0; b < BusManager::getNumBusses(); b++) { + Bus *bus = BusManager::getBus(b); if (bus == nullptr || bus->getLength()==0) break; switch (bus->getType()) { case TYPE_ANALOG_5CH: @@ -1393,18 +1394,18 @@ bool WS2812FX::hasCCTBus(void) { return false; } -void WS2812FX::purgeSegments(bool force) { +void WS2812FX::purgeSegments() { // remove all inactive segments (from the back) int deleted = 0; if (_segments.size() <= 1) return; for (size_t i = _segments.size()-1; i > 0; i--) - if (_segments[i].stop == 0 || force) { + if (_segments[i].stop == 0) { deleted++; _segments.erase(_segments.begin() + i); } if (deleted) { _segments.shrink_to_fit(); - /*if (_mainSegment >= _segments.size())*/ setMainSegmentId(0); + setMainSegmentId(0); } } @@ -1419,7 +1420,7 @@ void WS2812FX::setSegment(uint8_t segId, uint16_t i1, uint16_t i2, uint8_t group appendSegment(Segment(0, strip.getLengthTotal())); segId = getSegmentsNum()-1; // segments are added at the end of list } - +/* if (_queuedChangesSegId == segId) _queuedChangesSegId = 255; // cancel queued change if already queued for this segment if (segId < getMaxSegments() && segId == getCurrSegmentId() && isServicing()) { // queue change to prevent concurrent access @@ -1431,17 +1432,19 @@ void WS2812FX::setSegment(uint8_t segId, uint16_t i1, uint16_t i2, uint8_t group DEBUG_PRINT(F("Segment queued: ")); DEBUG_PRINTLN(segId); return; // queued changes are applied immediately after effect function returns } - +*/ + suspend(); _segments[segId].setUp(i1, i2, grouping, spacing, offset, startY, stopY); + resume(); if (segId > 0 && segId == getSegmentsNum()-1 && i2 <= i1) _segments.pop_back(); // if last segment was deleted remove it from vector } - +/* void WS2812FX::setUpSegmentFromQueuedChanges() { if (_queuedChangesSegId >= getSegmentsNum()) return; - getSegment(_queuedChangesSegId).setUp(_qStart, _qStop, _qGrouping, _qSpacing, _qOffset, _qStartY, _qStopY); + _segments[_queuedChangesSegId].setUp(_qStart, _qStop, _qGrouping, _qSpacing, _qOffset, _qStartY, _qStopY); _queuedChangesSegId = 255; } - +*/ void WS2812FX::resetSegments() { _segments.clear(); // destructs all Segment as part of clearing #ifndef WLED_DISABLE_2D @@ -1468,8 +1471,8 @@ void WS2812FX::makeAutoSegments(bool forceReset) { } #endif - for (size_t i = s; i < busses.getNumBusses(); i++) { - Bus* b = busses.getBus(i); + for (size_t i = s; i < BusManager::getNumBusses(); i++) { + Bus* b = BusManager::getBus(i); segStarts[s] = b->getStart(); segStops[s] = segStarts[s] + b->getLength(); @@ -1558,8 +1561,8 @@ void WS2812FX::fixInvalidSegments() { bool WS2812FX::checkSegmentAlignment() { bool aligned = false; for (segment &seg : _segments) { - for (unsigned b = 0; bgetStart() && seg.stop == bus->getStart() + bus->getLength()) aligned = true; } if (seg.start == 0 && seg.stop == _length) aligned = true; @@ -1657,40 +1660,30 @@ bool WS2812FX::deserializeMap(uint8_t n) { strcat_P(fileName, PSTR(".json")); bool isFile = WLED_FS.exists(fileName); - if (!isFile) { - // erase custom mapping if selecting nonexistent ledmap.json (n==0) - if (!isMatrix && !n && customMappingTable != nullptr) { - customMappingSize = 0; - delete[] customMappingTable; - customMappingTable = nullptr; - } + customMappingSize = 0; // prevent use of mapping if anything goes wrong + + if (!isFile && n==0 && isMatrix) { + setUpMatrix(); return false; } - if (!requestJSONBufferLock(7)) return false; + if (!isFile || !requestJSONBufferLock(7)) return false; // this will trigger setUpMatrix() when called from wled.cpp - if (!readObjectFromFile(fileName, nullptr, &doc)) { + if (!readObjectFromFile(fileName, nullptr, pDoc)) { + DEBUG_PRINT(F("ERROR Invalid ledmap in ")); DEBUG_PRINTLN(fileName); releaseJSONBufferLock(); - return false; //if file does not exist just exit + return false; // if file does not load properly then exit } - DEBUG_PRINT(F("Reading LED map from ")); - DEBUG_PRINTLN(fileName); + DEBUG_PRINT(F("Reading LED map from ")); DEBUG_PRINTLN(fileName); - // erase old custom ledmap - if (customMappingTable != nullptr) { - customMappingSize = 0; - delete[] customMappingTable; - customMappingTable = nullptr; - } + if (customMappingTable == nullptr) customMappingTable = new uint16_t[getLengthTotal()]; - JsonArray map = doc[F("map")]; + JsonObject root = pDoc->as(); + JsonArray map = root[F("map")]; if (!map.isNull() && map.size()) { // not an empty map - customMappingSize = map.size(); - customMappingTable = new uint16_t[customMappingSize]; - for (unsigned i=0; i getLength()) { //each LED uses about 1mA in standby, exclude that from power budget + powerBudget -= getLength(); + } else { + powerBudget = 0; + } uint32_t busPowerSum = 0; for (unsigned i = 0; i < getLength(); i++) { //sum up the usage of each LED @@ -178,29 +183,26 @@ uint8_t BusDigital::estimateCurrentAndLimitBri() { busPowerSum >>= 2; //same as /= 4 } - if (powerBudget > getLength()) { //each LED uses about 1mA in standby, exclude that from power budget - powerBudget -= getLength(); - } else { - powerBudget = 0; - } - // powerSum has all the values of channels summed (max would be getLength()*765 as white is excluded) so convert to milliAmps busPowerSum = (busPowerSum * actualMilliampsPerLed) / 765; + _milliAmpsTotal = busPowerSum * _bri / 255; uint8_t newBri = _bri; if (busPowerSum * _bri / 255 > powerBudget) { //scale brightness down to stay in current limit float scale = (float)(powerBudget * 255) / (float)(busPowerSum * _bri); - uint16_t scaleI = scale * 255; - uint8_t scaleB = (scaleI > 255) ? 255 : scaleI; + if (scale >= 1.0f) return _bri; + _milliAmpsTotal = ceilf((float)_milliAmpsTotal * scale); + uint8_t scaleB = min((int)(scale * 255), 255); newBri = unsigned(_bri * scaleB) / 256 + 1; } return newBri; } void BusDigital::show() { + _milliAmpsTotal = 0; if (!_valid) return; - uint8_t newBri = estimateCurrentAndLimitBri(); + uint8_t newBri = estimateCurrentAndLimitBri(); // will fill _milliAmpsTotal if (newBri < _bri) PolyBus::setBrightness(_busPtr, _iType, newBri); // limit brightness to stay within current limits if (_data) { // use _buffering this causes ~20% FPS drop @@ -258,23 +260,8 @@ void BusDigital::setBrightness(uint8_t b) { if (_pins[0] == LED_BUILTIN || _pins[1] == LED_BUILTIN) reinit(); } #endif - uint8_t prevBri = _bri; Bus::setBrightness(b); PolyBus::setBrightness(_busPtr, _iType, b); -/* - if (_data) return; // use _buffering this causes ~20% FPS drop - - // must update/repaint every LED in the NeoPixelBus buffer to the new brightness - // the only case where repainting is unnecessary is when all pixels are set after the brightness change but before the next show - // (which we can't rely on) - uint16_t hwLen = _len; - if (_type == TYPE_WS2812_1CH_X3) hwLen = NUM_ICS_WS2812_1CH_3X(_len); // only needs a third of "RGB" LEDs for NeoPixelBus - for (unsigned i = 0; i < hwLen; i++) { - // use 0 as color order, actual order does not matter here as we just update the channel values as-is - uint32_t c = restoreColorLossy(PolyBus::getPixelColor(_busPtr, _iType, i, 0), prevBri); - PolyBus::setPixelColor(_busPtr, _iType, i, c, 0); - } -*/ } //If LEDs are skipped, it is possible to use the first as a status LED. @@ -661,9 +648,12 @@ void BusManager::removeAll() { } void BusManager::show() { + _milliAmpsUsed = 0; for (unsigned i = 0; i < numBusses; i++) { busses[i]->show(); + _milliAmpsUsed += busses[i]->getUsedCurrent(); } + if (_milliAmpsUsed) _milliAmpsUsed += MA_FOR_ESP; } void BusManager::setStatusPixel(uint32_t c) { @@ -729,3 +719,11 @@ uint16_t BusManager::getTotalLength() { int16_t Bus::_cct = -1; uint8_t Bus::_cctBlend = 0; uint8_t Bus::_gAWM = 255; + +uint16_t BusDigital::_milliAmpsTotal = 0; + +uint8_t BusManager::numBusses = 0; +Bus* BusManager::busses[WLED_MAX_BUSSES+WLED_MIN_VIRTUAL_BUSSES]; +ColorOrderMap BusManager::colorOrderMap = {}; +uint16_t BusManager::_milliAmpsUsed = 0; +uint16_t BusManager::_milliAmpsMax = ABL_MILLIAMPS_DEFAULT; \ No newline at end of file diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index 9d322749c..12fb81b39 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -137,6 +137,7 @@ class Bus { virtual uint8_t skippedLeds() { return 0; } virtual uint16_t getFrequency() { return 0U; } virtual uint16_t getLEDCurrent() { return 0; } + virtual uint16_t getUsedCurrent() { return 0; } virtual uint16_t getMaxCurrent() { return 0; } inline void setReversed(bool reversed) { _reversed = reversed; } inline uint16_t getStart() { return _start; } @@ -219,6 +220,7 @@ class BusDigital : public Bus { uint16_t getFrequency() { return _frequencykHz; } uint8_t estimateCurrentAndLimitBri(); uint16_t getLEDCurrent() { return _milliAmpsPerLed; } + uint16_t getUsedCurrent() { return _milliAmpsTotal; } uint16_t getMaxCurrent() { return _milliAmpsMax; } void reinit(); void cleanup(); @@ -233,7 +235,8 @@ class BusDigital : public Bus { uint16_t _milliAmpsMax; void * _busPtr; const ColorOrderMap &_colorOrderMap; - //bool _buffering; // temporary until we figure out why comparison "_data" causes severe FPS drop + + static uint16_t _milliAmpsTotal; // is overwitten/recalculated on each show() inline uint32_t restoreColorLossy(uint32_t c, uint8_t restoreBri) { if (restoreBri < 255) { @@ -314,39 +317,44 @@ class BusNetwork : public Bus { class BusManager { public: - BusManager() : numBusses(0) {}; + BusManager() {}; //utility to get the approx. memory usage of a given BusConfig static uint32_t memUsage(BusConfig &bc); + static uint16_t currentMilliamps(void) { return _milliAmpsUsed; } + static uint16_t ablMilliampsMax(void) { return _milliAmpsMax; } - int add(BusConfig &bc); + static int add(BusConfig &bc); //do not call this method from system context (network callback) - void removeAll(); + static void removeAll(); - void show(); - bool canAllShow(); - void setStatusPixel(uint32_t c); - void setPixelColor(uint16_t pix, uint32_t c); - void setBrightness(uint8_t b); - void setSegmentCCT(int16_t cct, bool allowWBCorrection = false); - uint32_t getPixelColor(uint16_t pix); + static void show(); + static bool canAllShow(); + static void setStatusPixel(uint32_t c); + static void setPixelColor(uint16_t pix, uint32_t c); + static void setBrightness(uint8_t b); + static void setSegmentCCT(int16_t cct, bool allowWBCorrection = false); + static void setMilliampsMax(uint16_t max) { _milliAmpsMax = max;} + static uint32_t getPixelColor(uint16_t pix); - Bus* getBus(uint8_t busNr); + static Bus* getBus(uint8_t busNr); //semi-duplicate of strip.getLengthTotal() (though that just returns strip._length, calculated in finalizeInit()) - uint16_t getTotalLength(); - inline uint8_t getNumBusses() const { return numBusses; } + static uint16_t getTotalLength(); + static uint8_t getNumBusses() { return numBusses; } - inline void updateColorOrderMap(const ColorOrderMap &com) { memcpy(&colorOrderMap, &com, sizeof(ColorOrderMap)); } - inline const ColorOrderMap& getColorOrderMap() const { return colorOrderMap; } + static void updateColorOrderMap(const ColorOrderMap &com) { memcpy(&colorOrderMap, &com, sizeof(ColorOrderMap)); } + static const ColorOrderMap& getColorOrderMap() { return colorOrderMap; } private: - uint8_t numBusses; - Bus* busses[WLED_MAX_BUSSES+WLED_MIN_VIRTUAL_BUSSES]; - ColorOrderMap colorOrderMap; + static uint8_t numBusses; + static Bus* busses[WLED_MAX_BUSSES+WLED_MIN_VIRTUAL_BUSSES]; + static ColorOrderMap colorOrderMap; + static uint16_t _milliAmpsUsed; + static uint16_t _milliAmpsMax; - inline uint8_t getNumVirtualBusses() { + static uint8_t getNumVirtualBusses() { int j = 0; for (int i=0; igetType() >= TYPE_NET_DDP_RGB && busses[i]->getType() < 96) j++; return j; diff --git a/wled00/button.cpp b/wled00/button.cpp index f1487396a..b1e7f2a79 100644 --- a/wled00/button.cpp +++ b/wled00/button.cpp @@ -347,10 +347,10 @@ void handleButton() void esp32RMTInvertIdle() { bool idle_out; - for (uint8_t u = 0; u < busses.getNumBusses(); u++) + for (uint8_t u = 0; u < BusManager::getNumBusses(); u++) { if (u > 7) return; // only 8 RMT channels, TODO: ESP32 variants have less RMT channels - Bus *bus = busses.getBus(u); + Bus *bus = BusManager::getBus(u); if (!bus || bus->getLength()==0 || !IS_DIGITAL(bus->getType()) || IS_2PIN(bus->getType())) continue; //assumes that bus number to rmt channel mapping stays 1:1 rmt_channel_t ch = static_cast(u); diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index 250f79af9..bea7ef237 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -79,7 +79,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { noWifiSleep = doc[F("wifi")][F("sleep")] | !noWifiSleep; // inverted noWifiSleep = !noWifiSleep; - //int wifi_phy = doc[F("wifi")][F("phy")]; //force phy mode n? + force802_3g = doc[F("wifi")][F("phy")] | force802_3g; //force phy mode g? JsonObject hw = doc[F("hw")]; @@ -87,8 +87,8 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { JsonObject hw_led = hw["led"]; uint16_t total = hw_led[F("total")] | strip.getLengthTotal(); - CJSON(strip.ablMilliampsMax, hw_led[F("maxpwr")]); - CJSON(strip.milliampsPerLed, hw_led[F("ledma")]); // no longer used + uint16_t ablMilliampsMax = hw_led[F("maxpwr")] | BusManager::ablMilliampsMax(); + BusManager::setMilliampsMax(ablMilliampsMax); Bus::setGlobalAWMode(hw_led[F("rgbwm")] | AW_GLOBAL_DISABLED); CJSON(correctWB, hw_led["cct"]); CJSON(cctFromRgb, hw_led[F("cr")]); @@ -138,7 +138,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { if (fromFS || !ins.isNull()) { uint8_t s = 0; // bus iterator - if (fromFS) busses.removeAll(); // can't safely manipulate busses directly in network callback + if (fromFS) BusManager::removeAll(); // can't safely manipulate busses directly in network callback uint32_t mem = 0, globalBufMem = 0; uint16_t maxlen = 0; bool busesChanged = false; @@ -164,8 +164,8 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { bool refresh = elm["ref"] | false; uint16_t freqkHz = elm[F("freq")] | 0; // will be in kHz for DotStar and Hz for PWM (not yet implemented fully) uint8_t AWmode = elm[F("rgbwm")] | RGBW_MODE_MANUAL_ONLY; - uint8_t maPerLed = elm[F("ledma")] | strip.milliampsPerLed; // replace with 55 when removing strip.milliampsPerLed - uint16_t maMax = elm[F("maxpwr")] | (strip.ablMilliampsMax * length) / total; // rough (incorrect?) per strip ABL calculation when no config exists + uint8_t maPerLed = elm[F("ledma")] | 55; + uint16_t maMax = elm[F("maxpwr")] | (ablMilliampsMax * length) / total; // rough (incorrect?) per strip ABL calculation when no config exists // To disable brightness limiter we either set output max current to 0 or single LED current to 0 (we choose output max current) if ((ledType > TYPE_TM1814 && ledType < TYPE_WS2801) || ledType >= TYPE_NET_DDP_RGB) { // analog and virtual maPerLed = 0; @@ -179,7 +179,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { maxlen = start + length; globalBufMem = maxlen * 4; } - if (mem + globalBufMem <= MAX_LED_MEMORY) if (busses.add(bc) == -1) break; // finalization will be done in WLED::beginStrip() + if (mem + globalBufMem <= MAX_LED_MEMORY) if (BusManager::add(bc) == -1) break; // finalization will be done in WLED::beginStrip() } else { if (busConfigs[s] != nullptr) delete busConfigs[s]; busConfigs[s] = new BusConfig(ledType, pins, start, length, colorOrder, reversed, skipFirst, AWmode, freqkHz, useGlobalLedBuffer, maPerLed, maMax); @@ -190,7 +190,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { doInitBusses = busesChanged; // finalization done in beginStrip() } - if (hw_led["rev"]) busses.getBus(0)->setReversed(true); //set 0.11 global reversed setting for first bus + if (hw_led["rev"]) BusManager::getBus(0)->setReversed(true); //set 0.11 global reversed setting for first bus // read color order map configuration JsonArray hw_com = hw[F("com")]; @@ -205,7 +205,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { com.add(start, len, colorOrder); s++; } - busses.updateColorOrderMap(com); + BusManager::updateColorOrderMap(com); } // read multiple button configuration @@ -610,7 +610,7 @@ void deserializeConfigFromFS() { DEBUG_PRINTLN(F("Reading settings from /cfg.json...")); - success = readObjectFromFile("/cfg.json", nullptr, &doc); + success = readObjectFromFile("/cfg.json", nullptr, pDoc); if (!success) { // if file does not exist, optionally try reading from EEPROM and then save defaults to FS releaseJSONBufferLock(); #ifdef WLED_ADD_EEPROM_SUPPORT @@ -631,7 +631,8 @@ void deserializeConfigFromFS() { // NOTE: This routine deserializes *and* applies the configuration // Therefore, must also initialize ethernet from this function - bool needsSave = deserializeConfig(doc.as(), true); + JsonObject root = pDoc->as(); + bool needsSave = deserializeConfig(root, true); releaseJSONBufferLock(); if (needsSave) serializeConfig(); // usermods required new parameters @@ -644,19 +645,21 @@ void serializeConfig() { if (!requestJSONBufferLock(2)) return; - JsonArray rev = doc.createNestedArray("rev"); + JsonObject root = pDoc->to(); + + JsonArray rev = root.createNestedArray("rev"); rev.add(1); //major settings revision rev.add(0); //minor settings revision - doc[F("vid")] = VERSION; + root[F("vid")] = VERSION; - JsonObject id = doc.createNestedObject("id"); + JsonObject id = root.createNestedObject("id"); id[F("mdns")] = cmDNS; id[F("name")] = serverDescription; id[F("inv")] = alexaInvocationName; id[F("sui")] = simplifiedUI; - JsonObject nw = doc.createNestedObject("nw"); + JsonObject nw = root.createNestedObject("nw"); #ifndef WLED_DISABLE_ESPNOW nw[F("espnow")] = enableESPNow; nw[F("linked_remote")] = linked_remote; @@ -678,7 +681,7 @@ void serializeConfig() { nw_ins_0_sn.add(staticSubnet[i]); } - JsonObject ap = doc.createNestedObject("ap"); + JsonObject ap = root.createNestedObject("ap"); ap[F("ssid")] = apSSID; ap[F("pskl")] = strlen(apPass); ap[F("chan")] = apChannel; @@ -691,12 +694,12 @@ void serializeConfig() { ap_ip.add(2); ap_ip.add(1); - JsonObject wifi = doc.createNestedObject("wifi"); + JsonObject wifi = root.createNestedObject("wifi"); wifi[F("sleep")] = !noWifiSleep; - //wifi[F("phy")] = 1; + wifi[F("phy")] = (int)force802_3g; #ifdef WLED_USE_ETHERNET - JsonObject ethernet = doc.createNestedObject("eth"); + JsonObject ethernet = root.createNestedObject("eth"); ethernet["type"] = ethernetType; if (ethernetType != WLED_ETH_NONE && ethernetType < WLED_NUM_ETH_TYPES) { JsonArray pins = ethernet.createNestedArray("pin"); @@ -719,12 +722,12 @@ void serializeConfig() { } #endif - JsonObject hw = doc.createNestedObject("hw"); + JsonObject hw = root.createNestedObject("hw"); JsonObject hw_led = hw.createNestedObject("led"); hw_led[F("total")] = strip.getLengthTotal(); //provided for compatibility on downgrade and per-output ABL - hw_led[F("maxpwr")] = strip.ablMilliampsMax; - hw_led[F("ledma")] = strip.milliampsPerLed; // no longer used + hw_led[F("maxpwr")] = BusManager::ablMilliampsMax(); + hw_led[F("ledma")] = 0; // no longer used hw_led["cct"] = correctWB; hw_led[F("cr")] = cctFromRgb; hw_led[F("cb")] = strip.cctBlending; @@ -754,8 +757,8 @@ void serializeConfig() { JsonArray hw_led_ins = hw_led.createNestedArray("ins"); - for (uint8_t s = 0; s < busses.getNumBusses(); s++) { - Bus *bus = busses.getBus(s); + for (uint8_t s = 0; s < BusManager::getNumBusses(); s++) { + Bus *bus = BusManager::getBus(s); if (!bus || bus->getLength()==0) break; JsonObject ins = hw_led_ins.createNestedObject(); ins["start"] = bus->getStart(); @@ -776,7 +779,7 @@ void serializeConfig() { } JsonArray hw_com = hw.createNestedArray(F("com")); - const ColorOrderMap& com = busses.getColorOrderMap(); + const ColorOrderMap& com = BusManager::getColorOrderMap(); for (uint8_t s = 0; s < com.count(); s++) { const ColorOrderMapEntry *entry = com.get(s); if (!entry) break; @@ -831,7 +834,7 @@ void serializeConfig() { //JsonObject hw_status = hw.createNestedObject("status"); //hw_status["pin"] = -1; - JsonObject light = doc.createNestedObject(F("light")); + JsonObject light = root.createNestedObject(F("light")); light[F("scale-bri")] = briMultiplier; light[F("pal-mode")] = strip.paletteBlend; light[F("aseg")] = autoSegments; @@ -854,12 +857,12 @@ void serializeConfig() { light_nl[F("tbri")] = nightlightTargetBri; light_nl["macro"] = macroNl; - JsonObject def = doc.createNestedObject("def"); + JsonObject def = root.createNestedObject("def"); def["ps"] = bootPreset; def["on"] = turnOnAtBoot; def["bri"] = briS; - JsonObject interfaces = doc.createNestedObject("if"); + JsonObject interfaces = root.createNestedObject("if"); JsonObject if_sync = interfaces.createNestedObject("sync"); if_sync[F("port0")] = udpPort; @@ -963,7 +966,7 @@ void serializeConfig() { if_ntp[F("ln")] = longitude; if_ntp[F("lt")] = latitude; - JsonObject ol = doc.createNestedObject("ol"); + JsonObject ol = root.createNestedObject("ol"); ol[F("clock")] = overlayCurrent; ol[F("cntdwn")] = countdownMode; @@ -973,7 +976,7 @@ void serializeConfig() { ol[F("o5m")] = analogClock5MinuteMarks; ol[F("osec")] = analogClockSecondsTrail; - JsonObject timers = doc.createNestedObject(F("timers")); + JsonObject timers = root.createNestedObject(F("timers")); JsonObject cntdwn = timers.createNestedObject(F("cntdwn")); JsonArray goal = cntdwn.createNestedArray(F("goal")); @@ -1001,14 +1004,14 @@ void serializeConfig() { } } - JsonObject ota = doc.createNestedObject("ota"); + JsonObject ota = root.createNestedObject("ota"); ota[F("lock")] = otaLock; ota[F("lock-wifi")] = wifiLock; ota[F("pskl")] = strlen(otaPass); ota[F("aota")] = aOtaEnabled; #ifdef WLED_ENABLE_DMX - JsonObject dmx = doc.createNestedObject("dmx"); + JsonObject dmx = root.createNestedObject("dmx"); dmx[F("chan")] = DMXChannels; dmx[F("gap")] = DMXGap; dmx["start"] = DMXStart; @@ -1022,11 +1025,11 @@ void serializeConfig() { dmx[F("e131proxy")] = e131ProxyUniverse; #endif - JsonObject usermods_settings = doc.createNestedObject("um"); + JsonObject usermods_settings = root.createNestedObject("um"); usermods.addToConfig(usermods_settings); File f = WLED_FS.open("/cfg.json", "w"); - if (f) serializeJson(doc, f); + if (f) serializeJson(root, f); f.close(); releaseJSONBufferLock(); @@ -1039,19 +1042,21 @@ bool deserializeConfigSec() { if (!requestJSONBufferLock(3)) return false; - bool success = readObjectFromFile("/wsec.json", nullptr, &doc); + bool success = readObjectFromFile("/wsec.json", nullptr, pDoc); if (!success) { releaseJSONBufferLock(); return false; } - JsonObject nw_ins_0 = doc["nw"]["ins"][0]; + JsonObject root = pDoc->as(); + + JsonObject nw_ins_0 = root["nw"]["ins"][0]; getStringFromJson(clientPass, nw_ins_0["psk"], 65); - JsonObject ap = doc["ap"]; + JsonObject ap = root["ap"]; getStringFromJson(apPass, ap["psk"] , 65); - [[maybe_unused]] JsonObject interfaces = doc["if"]; + [[maybe_unused]] JsonObject interfaces = root["if"]; #ifdef WLED_ENABLE_MQTT JsonObject if_mqtt = interfaces["mqtt"]; @@ -1062,10 +1067,10 @@ bool deserializeConfigSec() { getStringFromJson(hueApiKey, interfaces["hue"][F("key")], 47); #endif - getStringFromJson(settingsPIN, doc["pin"], 5); + getStringFromJson(settingsPIN, root["pin"], 5); correctPIN = !strlen(settingsPIN); - JsonObject ota = doc["ota"]; + JsonObject ota = root["ota"]; getStringFromJson(otaPass, ota[F("pwd")], 33); CJSON(otaLock, ota[F("lock")]); CJSON(wifiLock, ota[F("lock-wifi")]); @@ -1080,17 +1085,19 @@ void serializeConfigSec() { if (!requestJSONBufferLock(4)) return; - JsonObject nw = doc.createNestedObject("nw"); + JsonObject root = pDoc->to(); + + JsonObject nw = root.createNestedObject("nw"); JsonArray nw_ins = nw.createNestedArray("ins"); JsonObject nw_ins_0 = nw_ins.createNestedObject(); nw_ins_0["psk"] = clientPass; - JsonObject ap = doc.createNestedObject("ap"); + JsonObject ap = root.createNestedObject("ap"); ap["psk"] = apPass; - [[maybe_unused]] JsonObject interfaces = doc.createNestedObject("if"); + [[maybe_unused]] JsonObject interfaces = root.createNestedObject("if"); #ifdef WLED_ENABLE_MQTT JsonObject if_mqtt = interfaces.createNestedObject("mqtt"); if_mqtt["psk"] = mqttPass; @@ -1100,16 +1107,16 @@ void serializeConfigSec() { if_hue[F("key")] = hueApiKey; #endif - doc["pin"] = settingsPIN; + root["pin"] = settingsPIN; - JsonObject ota = doc.createNestedObject("ota"); + JsonObject ota = root.createNestedObject("ota"); ota[F("pwd")] = otaPass; ota[F("lock")] = otaLock; ota[F("lock-wifi")] = wifiLock; ota[F("aota")] = aOtaEnabled; File f = WLED_FS.open("/wsec.json", "w"); - if (f) serializeJson(doc, f); + if (f) serializeJson(root, f); f.close(); releaseJSONBufferLock(); } diff --git a/wled00/const.h b/wled00/const.h index ef1b31c01..da6076742 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -256,10 +256,11 @@ #define TYPE_NET_ARTNET_RGB 82 //network ArtNet RGB bus (master broadcast bus, unused) #define TYPE_NET_DDP_RGBW 88 //network DDP RGBW bus (master broadcast bus) -#define IS_DIGITAL(t) ((t) & 0x10) //digital are 16-31 and 48-63 -#define IS_PWM(t) ((t) > 40 && (t) < 46) -#define NUM_PWM_PINS(t) ((t) - 40) //for analog PWM 41-45 only +#define IS_DIGITAL(t) ((t) < 80 && ((t) & 0x10)) //digital are 16-31 and 48-63 +#define IS_PWM(t) ((t) > 40 && (t) < 46) +#define NUM_PWM_PINS(t) ((t) - 40) //for analog PWM 41-45 only #define IS_2PIN(t) ((t) > 47) +#define IS_VIRTUAL(t) ((t) >= 80) //Color orders #define COL_ORDER_GRB 0 //GRB(w),defaut @@ -345,6 +346,7 @@ #define ERR_CONCURRENCY 2 // Conurrency (client active) #define ERR_NOBUF 3 // JSON buffer was not released in time, request cannot be handled at this time #define ERR_NOT_IMPL 4 // Not implemented +#define ERR_NORAM 8 // effect RAM depleted #define ERR_JSON 9 // JSON parsing failed (input too large?) #define ERR_FS_BEGIN 10 // Could not init filesystem (no partition?) #define ERR_FS_QUOTA 11 // The FS is full or the maximum file size is reached @@ -449,7 +451,11 @@ #ifdef ESP8266 #define JSON_BUFFER_SIZE 10240 #else - #define JSON_BUFFER_SIZE 24576 + #if defined(ARDUINO_ARCH_ESP32S2) + #define JSON_BUFFER_SIZE 24576 + #else + #define JSON_BUFFER_SIZE 32767 + #endif #endif //#define MIN_HEAP_SIZE (8k for AsyncWebServer) diff --git a/wled00/data/cpal/cpal.htm b/wled00/data/cpal/cpal.htm index 5a8c801e5..873252bad 100644 --- a/wled00/data/cpal/cpal.htm +++ b/wled00/data/cpal/cpal.htm @@ -1,6 +1,7 @@ + @@ -45,6 +46,7 @@ width: 7px; top: 50%; transform: translateY(-50%); + touch-action: none; } .color-picker-marker { height: 7px; @@ -94,9 +96,14 @@ line-height: 1; } .wrap { - width: 800px; + width: 100%; margin: 0 auto; } + @media (min-width: 800px) { + .wrap { + width: 800px; + } + } .palette { height: 20px; } @@ -136,6 +143,9 @@ .sendSpan, .editSpan{ cursor: pointer; } + h1 { + font-size: 1.6rem; + } @@ -349,24 +359,31 @@ var gradientLength = maxX - minX + 1; elmnt.onmousedown = dragMouseDown; + elmnt.ontouchstart = dragMouseDown; function dragMouseDown(e) { removeTrashcan(event) e = e || window.event; - e.preventDefault(); + var isTouch = e.type.startsWith('touch'); + if (!isTouch) e.preventDefault(); // get the mouse cursor position at startup: - mousePos = e.clientX; + mousePos = isTouch ? e.touches[0].clientX : e.clientX; d.onmouseup = closeDragElement; + d.ontouchcancel = closeDragElement; + d.ontouchend = closeDragElement; // call a function whenever the cursor moves: d.onmousemove = elementDrag; + d.ontouchmove = elementDrag; } function elementDrag(e) { e = e || window.event; - e.preventDefault(); + var isTouch = e.type.startsWith('touch'); + if (!isTouch) e.preventDefault(); // calculate the new cursor position: - posNew = mousePos - e.clientX; - mousePos = e.clientX; + var clientX = isTouch ? e.touches[0].clientX : e.clientX; + posNew = mousePos - clientX; + mousePos = clientX; mousePosInGradient = mousePos - (minX + 1) truePos = Math.round((mousePosInGradient/gradientLength)*256); @@ -393,7 +410,10 @@ function closeDragElement() { /* stop moving when mouse button is released:*/ d.onmouseup = null; + d.ontouchcancel = null; + d.ontouchend = null; d.onmousemove = null; + d.ontouchmove = null; } } diff --git a/wled00/data/index.css b/wled00/data/index.css index 0ffdf68d3..095de572a 100644 --- a/wled00/data/index.css +++ b/wled00/data/index.css @@ -785,7 +785,7 @@ input[type=range]::-moz-range-thumb { #picker { margin-top: 8px !important; - max-width: 260px; + max-width: max-content; } /* buttons */ @@ -1566,9 +1566,6 @@ dialog { max-width: 280px; font-size: 18px; } - #picker { - width: 230px; - } #putil .btn-s { width: 114px; } diff --git a/wled00/data/index.js b/wled00/data/index.js index 03516c370..ec8fcb23a 100644 --- a/wled00/data/index.js +++ b/wled00/data/index.js @@ -32,13 +32,14 @@ var cfg = { labels:true, pcmbot:false, pid:true, seglen:false, segpwr:false, segexp:false, css:true, hdays:false, fxdef:true, on:0, off:0, idsort: false} }; +// [year, month (0 -> January, 11 -> December), day, duration in days, image url] var hol = [ - [0,11,24,4,"https://aircoookie.github.io/xmas.png"], // christmas - [0,2,17,1,"https://images.alphacoders.com/491/491123.jpg"], // st. Patrick's day - [2025,3,20,2,"https://aircoookie.github.io/easter.png"], - [2024,2,31,2,"https://aircoookie.github.io/easter.png"], - [0,6,4,1,"https://images.alphacoders.com/516/516792.jpg"], // 4th of July - [0,0,1,1,"https://images.alphacoders.com/119/1198800.jpg"] // new year + [0, 11, 24, 4, "https://aircoookie.github.io/xmas.png"], // christmas + [0, 2, 17, 1, "https://images.alphacoders.com/491/491123.jpg"], // st. Patrick's day + [2025, 3, 20, 2, "https://aircoookie.github.io/easter.png"], // easter 2025 + [2024, 2, 31, 2, "https://aircoookie.github.io/easter.png"], // easter 2024 + [0, 6, 4, 1, "https://images.alphacoders.com/516/516792.jpg"], // 4th of July + [0, 0, 1, 1, "https://images.alphacoders.com/119/1198800.jpg"] // new year ]; var cpick = new iro.ColorPicker("#picker", { @@ -175,19 +176,19 @@ function cTheme(light) { } } -function loadBg(iUrl) -{ - let bg = gId('bg'); - let img = d.createElement("img"); +function loadBg() { + const { url: iUrl, rnd: iRnd } = cfg.theme.bg; + const bg = gId('bg'); + const img = d.createElement("img"); img.src = iUrl; - if (iUrl == "" || iUrl==="https://picsum.photos/1920/1080") { - var today = new Date(); - for (var h of (hol||[])) { - var yr = h[0]==0 ? today.getFullYear() : h[0]; - var hs = new Date(yr,h[1],h[2]); - var he = new Date(hs); - he.setDate(he.getDate() + h[3]); - if (today>=hs && today<=he) img.src = h[4]; + if (!iUrl || iRnd) { + const today = new Date(); + for (const holiday of (hol || [])) { + const year = holiday[0] == 0 ? today.getFullYear() : holiday[0]; + const holidayStart = new Date(year, holiday[1], holiday[2]); + const holidayEnd = new Date(holidayStart); + holidayEnd.setDate(holidayEnd.getDate() + holiday[3]); + if (today >= holidayStart && today <= holidayEnd) img.src = holiday[4]; } } img.addEventListener('load', (e) => { @@ -195,7 +196,6 @@ function loadBg(iUrl) if (isNaN(a)) a = 0.6; bg.style.opacity = a; bg.style.backgroundImage = `url(${img.src})`; - img = null; gId('namelabel').style.color = "var(--c-c)"; // improve namelabel legibility on background image }); } @@ -266,10 +266,10 @@ function onLoad() console.log("No array of holidays in holidays.json. Defaults loaded."); }) .finally(()=>{ - loadBg(cfg.theme.bg.url); + loadBg(); }); } else - loadBg(cfg.theme.bg.url); + loadBg(); selectSlot(0); updateTablinks(0); @@ -329,7 +329,6 @@ function openTab(tabI, force = false) var timeout; function showToast(text, error = false) { - if (error) gId('connind').style.backgroundColor = "var(--c-r)"; var x = gId('toast'); //if (error) text += ''; x.innerHTML = text; @@ -342,6 +341,7 @@ function showToast(text, error = false) function showErrorToast() { + gId('connind').style.backgroundColor = "var(--c-r)"; showToast('Connection to light failed!', true); } @@ -1492,25 +1492,31 @@ function readState(s,command=false) gId('checkO3').checked = !(!i.o3); if (s.error && s.error != 0) { - var errstr = ""; - switch (s.error) { - case 10: - errstr = "Could not mount filesystem!"; - break; - case 11: - errstr = "Not enough space to save preset!"; - break; - case 12: - errstr = "Preset not found."; - break; - case 13: - errstr = "Missing ir.json."; - break; - case 19: - errstr = "A filesystem error has occured."; - break; + var errstr = ""; + switch (s.error) { + case 8: + errstr = "Effect RAM depleted!"; + break; + case 9: + errstr = "JSON parsing error!"; + break; + case 10: + errstr = "Could not mount filesystem!"; + break; + case 11: + errstr = "Not enough space to save preset!"; + break; + case 12: + errstr = "Preset not found."; + break; + case 13: + errstr = "Missing ir.json."; + break; + case 19: + errstr = "A filesystem error has occured."; + break; } - showToast('Error ' + s.error + ": " + errstr, true); + showToast('Error ' + s.error + ": " + errstr, true); } selectedPal = i.pal; diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index 835b396fa..97e0f00d6 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -16,6 +16,11 @@ function B(){window.open(getURL("/settings"),"_self");} function gId(n){return d.getElementById(n);} function off(n){d.getElementsByName(n)[0].value = -1;} + function isPWM(t) { return t > 40 && t < 46; } // is PWM type + function isAna(t) { return t == 40 || isPWM(t); } // is analog type + function isDig(t) { return t < 80 && (t & 0x10); }// is digital type + function isD2P(t) { return t > 47 && t < 64; } // is digital 2 pin type + function isVir(t) { return t >= 80; } // is virtual type // https://www.educative.io/edpresso/how-to-dynamically-load-a-js-file-in-javascript function loadJS(FILE_URL, async = true) { let scE = d.createElement("script"); @@ -26,8 +31,8 @@ // success event scE.addEventListener("load", () => { GetV(); - setABL(); checkSi(); + setABL(); d.Sf.addEventListener("submit", trySubmit); if (d.um_p[0]==-1) d.um_p.shift(); pinDropdowns(); @@ -107,44 +112,50 @@ e.preventDefault(); if (!pinsOK()) {e.stopPropagation();return false;} // Prevent form submission and contact with server if (bquot > 100) {var msg = "Too many LEDs for me to handle!"; if (maxM < 10000) msg += "\n\rConsider using an ESP32."; alert(msg);} + if (!d.Sf.ABL.checked || d.Sf.PPL.checked) d.Sf.MA.value = 0; // submit 0 as ABL (PPL will handle it) if (d.Sf.checkValidity()) d.Sf.submit(); //https://stackoverflow.com/q/37323914 } function enABL() { - var en = d.Sf["ABL"].checked; - d.Sf["MA"].min = en ? 250 : 0; + var en = d.Sf.ABL.checked; gId('abl').style.display = (en) ? 'inline':'none'; gId('psu2').style.display = (en) ? 'inline':'none'; - if (!en) d.Sf["PPL"].checked = false; + if (!en) d.Sf.PPL.checked = false; enPPL(); UI(); } function enPPL() { - const abl = d.Sf["ABL"].checked; - const en = d.Sf["PPL"].checked; - d.Sf["MA"].readonly = en; - gId("ppldis").style.display = en ? 'inline' : 'none'; + const abl = d.Sf.ABL.checked; + const ppl = d.Sf.PPL.checked; + let sumMA = 0; + d.Sf.MA.readonly = ppl; + d.Sf.MA.min = abl && !ppl ? 250 : 0; + gId("psuMA").style.display = ppl ? 'none' : 'inline'; + gId("ppldis").style.display = ppl ? 'inline' : 'none'; + // set PPL minimum value and clear actual PPL limit if ABL disabled d.Sf.querySelectorAll("#mLC input[name^=MA]").forEach((i,n)=>{ - gId("PSU"+n).style.display = en ? "inline" : "none"; + gId("PSU"+n).style.display = ppl ? "inline" : "none"; const t = parseInt(d.Sf["LT"+n].value); // LED type SELECT - i.min = en && !((t >= 80 && t < 96) || (t >= 40 && t < 48)) ? 250 : 0; + i.min = ppl && !(isVir(t) || isAna(t)) ? 250 : 0; if (!abl) i.value = 0; + else if (ppl) sumMA += parseInt(i.value,10); }); + if (ppl) d.Sf.MA.value = sumMA; // populate UI ABL value if PPL used } function enLA(s,n) { const t = parseInt(d.Sf["LT"+n].value); // LED type SELECT gId('LAdis'+n).style.display = s.selectedIndex==5 ? "inline" : "none"; d.Sf["LA"+n].value = s.value==="0" ? 55 : s.value; - d.Sf["LA"+n].min = ((t >= 80 && t < 96) || (t >= 40 && t < 48)) ? 0 : 1; + d.Sf["LA"+n].min = (isVir(t) || isAna(t)) ? 0 : 1; } function setABL() { - d.Sf["ABL"].checked = false; + d.Sf.ABL.checked = parseInt(d.Sf.MA.value) > 0; // check if ABL is enabled (max mA entered per output) d.Sf.querySelectorAll("#mLC input[name^=MA]").forEach((i,n)=>{ - if (parseInt(i.value) > 0) d.Sf["ABL"].checked = true; + if (parseInt(i.value) > 0) d.Sf.ABL.checked = true; }); // select appropriate LED current d.Sf.querySelectorAll("#mLC select[name^=LAsel]").forEach((sel,n)=>{ @@ -190,20 +201,17 @@ let isRGBW = false, gRGBW = false, memu = 0; let sumMA = 0, busMA = 0; let sLC = 0, sPC = 0, sDI = 0, maxLC = 0; - const ablEN = d.Sf["ABL"].checked; - const pplEN = d.Sf["PPL"].checked; + const ablEN = d.Sf.ABL.checked; + const pplEN = d.Sf.PPL.checked; // enable/disable LED fields d.Sf.querySelectorAll("#mLC select[name^=LT]").forEach((s)=>{ // is the field a LED type? var n = s.name.substring(2); var t = parseInt(s.value); - let isDig = ((t >= 16 && t < 32) || (t >= 50 && t < 64)); - let isVir = (t >= 80 && t < 96); - let isPwm = (t >= 40 && t < 48); - gId("p0d"+n).innerHTML = (t>=80 && t<96) ? "IP address:" : (t > 49) ? "Data GPIO:" : (t > 41) ? "GPIOs:" : "GPIO:"; - gId("p1d"+n).innerHTML = (t> 49 && t<64) ? "Clk GPIO:" : ""; - gId("abl"+n).style.display = ((t >= 80 && t < 96) || (t >= 40 && t < 48)) ? "none" : "inline"; + gId("p0d"+n).innerHTML = isVir(t) ? "IP address:" : isD2P(t) ? "Data GPIO:" : (t > 41) ? "GPIOs:" : "GPIO:"; + gId("p1d"+n).innerHTML = isD2P(t) ? "Clk GPIO:" : ""; + gId("abl"+n).style.display = (!ablEN || isVir(t) || isAna(t)) ? "none" : "inline"; //var LK = d.getElementsByName("L1"+n)[0]; // clock pin memu += getMem(t, n); // calc memory @@ -212,7 +220,7 @@ for (p=1; p<5; p++) { var LK = d.Sf["L"+p+n]; // secondary pins if (!LK) continue; - if (((t>=80 && t<96) && p<4) || (t>49 && p==1) || (t>41 && t < 50 && (p+40 < t))) // TYPE_xxxx values from const.h + if ((isVir(t) && p<4) || (isD2P(t) && p==1) || (isPWM(t) && (p+40 < t))) // TYPE_xxxx values from const.h { // display pin field LK.style.display = "inline"; @@ -226,25 +234,25 @@ } if (change) { gId("rf"+n).checked = (gId("rf"+n).checked || t == 31); // LEDs require data in off state - if (t > 31 && t < 48) d.Sf["LC"+n].value = 1; // for sanity change analog count just to 1 LED - d.Sf["LA"+n].min = ((t >= 80 && t < 96) || (t >= 40 && t < 48)) ? 0 : 1; - d.Sf["MA"+n].min = ((t >= 80 && t < 96) || (t >= 40 && t < 48)) ? 0 : 250; + if (isAna(t)) d.Sf["LC"+n].value = 1; // for sanity change analog count just to 1 LED + d.Sf["LA"+n].min = (isVir(t) || isAna(t)) ? 0 : 1; + d.Sf["MA"+n].min = (isVir(t) || isAna(t)) ? 0 : 250; } gId("rf"+n).onclick = (t == 31) ? (()=>{return false}) : (()=>{}); // prevent change for TM1814 gRGBW |= isRGBW = ((t > 17 && t < 22) || (t > 28 && t < 32) || (t > 40 && t < 46 && t != 43) || t == 88); // RGBW checkbox, TYPE_xxxx values from const.h - gId("co"+n).style.display = ((t >= 80 && t < 96) || (t >= 40 && t < 48)) ? "none":"inline"; // hide color order for PWM - gId("dig"+n+"w").style.display = (t > 28 && t < 32) ? "inline":"none"; // show swap channels dropdown - if (!(t > 28 && t < 32)) d.Sf["WO"+n].value = 0; // reset swapping - gId("dig"+n+"c").style.display = (t >= 40 && t < 48) ? "none":"inline"; // hide count for analog - gId("dig"+n+"r").style.display = (t >= 80 && t < 96) ? "none":"inline"; // hide reversed for virtual - gId("dig"+n+"s").style.display = ((t >= 80 && t < 96) || (t >= 40 && t < 48)) ? "none":"inline"; // hide skip 1st for virtual & analog - gId("dig"+n+"f").style.display = ((t >= 16 && t < 32) || (t >= 50 && t < 64)) ? "inline":"none"; // hide refresh - gId("dig"+n+"a").style.display = (isRGBW && t != 40) ? "inline":"none"; // auto calculate white - gId("dig"+n+"l").style.display = (t > 48 && t < 64) ? "inline":"none"; // bus clock speed - gId("rev"+n).innerHTML = (t >= 40 && t < 48) ? "Inverted output":"Reversed (rotated 180°)"; // change reverse text for analog - gId("psd"+n).innerHTML = (t >= 40 && t < 48) ? "Index:":"Start:"; // change analog start description - if (ablEN && pplEN && !((t >= 80 && t < 96) || (t >= 40 && t < 48))) { - sumMA += parseInt(d.Sf["MA"+n].value); + gId("co"+n).style.display = (isVir(t) || isAna(t)) ? "none":"inline"; // hide color order for PWM + gId("dig"+n+"w").style.display = (isDig(t) && isRGBW) ? "inline":"none"; // show swap channels dropdown + if (!(isDig(t) && isRGBW)) d.Sf["WO"+n].value = 0; // reset swapping + gId("dig"+n+"c").style.display = (isAna(t)) ? "none":"inline"; // hide count for analog + gId("dig"+n+"r").style.display = (isVir(t)) ? "none":"inline"; // hide reversed for virtual + gId("dig"+n+"s").style.display = (isVir(t) || isAna(t)) ? "none":"inline"; // hide skip 1st for virtual & analog + gId("dig"+n+"f").style.display = (isDig(t)) ? "inline":"none"; // hide refresh + gId("dig"+n+"a").style.display = (isRGBW) ? "inline":"none"; // auto calculate white + gId("dig"+n+"l").style.display = (isD2P(t) || isPWM(t)) ? "inline":"none"; // bus clock speed / PWM speed (relative) (not On/Off) + gId("rev"+n).innerHTML = isAna(t) ? "Inverted output":"Reversed (rotated 180°)"; // change reverse text for analog + //gId("psd"+n).innerHTML = isAna(t) ? "Index:":"Start:"; // change analog start description + if (ablEN && pplEN && !(isVir(t) || isAna(t))) { + sumMA += parseInt(d.Sf["MA"+n].value); // summarize PPL ABL limit (fields) } }); // display global white channel overrides @@ -268,12 +276,13 @@ let s = parseInt(gId("ls"+n).value); //start value if (s+c > sLC) sLC = s+c; //update total count if (c > maxLC) maxLC = c; //max per output - if (t < 80) sPC += c; //virtual out busses do not count towards physical LEDs - if (!((t >= 80 && t < 96) || (t >= 40 && t < 48))) sDI += c; - if (!((t >= 80 && t < 96) || (t >= 40 && t < 48))) { + if (!isVir(t)) sPC += c; //virtual out busses do not count towards physical LEDs + //if (!(isVir(t) || isPWM(t))) sDI += c; + if (!(isVir(t) || isAna(t))) { + sDI += c; // summarize digital LED count let maPL = parseInt(d.Sf["LA"+n].value); if (maPL == 255) maPL = 12; - busMA += maPL*c; + busMA += maPL*c; // summarize maximum bus current (calculated) } } // increase led count return; @@ -284,7 +293,7 @@ } // ignore IP address (stored in pins for virtual busses) if (nm=="L0" || nm=="L1" || nm=="L2" || nm=="L3") { - if (t>=80) { + if (isVir(t)) { LC.max = 255; LC.min = 0; LC.style.color="#fff"; @@ -306,7 +315,7 @@ if (n2.substring(0,1)==="L") { let m = nList[j].name.substring(2); let t2 = parseInt(d.Sf["LT"+m].value, 10); - if (t2 >= 80) continue; + if (isVir(t2)) continue; } if (nList[j].value!="" && nList[j].value!="-1") p.push(parseInt(nList[j].value,10)); // add current pin } @@ -316,14 +325,18 @@ else LC.style.color = d.ro_gpio.some((e)=>e==parseInt(LC.value)) ? "orange" : "#fff"; } }); + // distribute ABL current if not using PPL, otherwise sumMA contains summarized ABL limit d.Sf.querySelectorAll("#mLC input[name^=LC]").forEach((s,n)=>{ let c = parseInt(s.value,10); //get LED count - let t = parseInt(d.Sf["LT"+n].value); - if (ablEN) { - let v = Math.round(parseInt(d.Sf["MA"].value,10)*c/sDI); - if (!pplEN && !((t >= 80 && t < 96) || (t >= 40 && t < 48))) d.Sf["MA"+n].value = v; - } else d.Sf["MA"+n].value = 0; + let t = parseInt(d.Sf["LT"+n].value); //get LED type + if (!ablEN || isVir(t) || isAna(t)) { + // virtual and analog LEDs have no limiter + d.Sf["MA"+n].value = 0; + return; + } + if (!pplEN) d.Sf["MA"+n].value = Math.round(parseInt(d.Sf.MA.value,10)*c/sDI); }); + if (pplEN) d.Sf.MA.value = sumMA; // update global ABL if using PPL // update total led count gId("lc").textContent = sLC; gId("pc").textContent = (sLC == sPC) ? "":"(" + sPC + " physical)"; @@ -336,7 +349,6 @@ gId('ledwarning').style.color = (maxLC > Math.max(maxPB,800) || bquot > 100) ? 'red':'orange'; gId('wreason').innerHTML = (bquot > 80) ? "80% of max. LED memory" +(bquot>100 ? ` (ERROR: Using over ${maxM}B!)` : "") : "800 LEDs per output"; // calculate power - if (pplEN) d.Sf.MA.value = sumMA; gId('ampwarning').style.display = (parseInt(d.Sf.MA.value,10) > 7200) ? 'inline':'none'; var val = Math.ceil((100 + busMA)/500)/2; val = (val > 5) ? Math.ceil(val) : val; @@ -409,7 +421,7 @@ mA/LED:
-
PSU: mA
+
PSU: mA
Color Order: gId("+").style.display = (i0) ? "inline":"none"; - if (!init) UI(); + if (!init) { + enPPL(); + UI(); + } } function addCOM(start=0,len=1,co=0) { @@ -750,7 +765,7 @@ Length: mA
+
Maximum PSU Current: mA
Use per-output limiter: