/* WS2812FX_fcn.cpp contains all utility functions Harm Aldick - 2016 www.aldick.org Modified heavily for WLED */ #include "wled.h" #include "FX.h" #include "palettes.h" #ifdef ARDUINO_ARCH_ESP32 #include // WLEDMM to get esp_timer_get_time() #include "freertos/FreeRTOS.h" #include "freertos/portmacro.h" WLED_create_spinlock(ledsrgb_mux); // to protect deleting Segment::_globalLeds and Segment::ledsrgb #endif /* Custom per-LED mapping has moved! Create a file "ledmap.json" using the edit page. this is just an example (30 LEDs). It will first set all even, then all uneven LEDs. {"map":[ 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29]} another example. Switches direction every 5 LEDs. {"map":[ 0, 1, 2, 3, 4, 9, 8, 7, 6, 5, 10, 11, 12, 13, 14, 19, 18, 17, 16, 15, 20, 21, 22, 23, 24, 29, 28, 27, 26, 25]} */ //factory defaults LED setup //#define PIXEL_COUNTS 30, 30, 30, 30 //#define DATA_PINS 16, 1, 3, 4 //#define DEFAULT_LED_TYPE TYPE_WS2812_RGB #ifndef PIXEL_COUNTS #define PIXEL_COUNTS DEFAULT_LED_COUNT #endif #ifndef DATA_PINS #define DATA_PINS LEDPIN #endif #ifndef DEFAULT_LED_TYPE #define DEFAULT_LED_TYPE TYPE_WS2812_RGB #endif #ifndef DEFAULT_LED_COLOR_ORDER #define DEFAULT_LED_COLOR_ORDER COL_ORDER_GRB //default to GRB #endif #if MAX_NUM_SEGMENTS < WLED_MAX_BUSSES #error "Max segments must be at least max number of busses!" #endif // WLEDMM experimental . this is a "C style" wrapper for strip.waitUntilIdle(); // This workaround is just needed for the segment class, that does't know about "strip" void strip_wait_until_idle(String whoCalledMe) { #if defined(ARDUINO_ARCH_ESP32) && defined(WLEDMM_PROTECT_SERVICE) // WLEDMM experimental if (strip.isServicing() && (strncmp(pcTaskGetTaskName(NULL), "loopTask", 8) != 0)) { // if we are in looptask (arduino loop), its safe to proceed without waiting DEBUG_PRINTLN(whoCalledMe + String(": strip is still drawing effects.")); strip.waitUntilIdle(); } #endif } // WLEDMM another helper for segment class bool strip_uses_global_leds(void) { return strip.useLedsArray; } /////////////////////////////////////////////////////////////////////////////// // Segment class implementation /////////////////////////////////////////////////////////////////////////////// size_t Segment::_usedSegmentData = 0U; // amount of RAM all segments use for their data[] CRGB *Segment::_globalLeds = nullptr; uint16_t Segment::maxWidth = DEFAULT_LED_COUNT; uint16_t Segment::maxHeight = 1; CRGBPalette16 Segment::_currentPalette = CRGBPalette16(CRGB::Black); // copy constructor - creates a new segment by copy from orig, but does not copy buffers. Does not modify orig! Segment::Segment(const Segment &orig) { DEBUG_PRINTLN(F("-- Copy segment constructor --")); memcpy((void*)this, (void*)&orig, sizeof(Segment)); //WLEDMM copy to this transitional = false; // copied segment cannot be in transition // WLEDMM temporarily prevent any fast draw calls to the new segment // _isValid2D = false; _isSimpleSegment = false; _isSuperSimpleSegment = false; name = nullptr; data = nullptr; _dataLen = 0; _t = nullptr; if (ledsrgb && !Segment::_globalLeds) {ledsrgb = nullptr; ledsrgbSize = 0;} // WLEDMM if (orig.name) { name = new(std::nothrow) char[strlen(orig.name)+1]; if (name) strcpy(name, orig.name); } if (orig.data) { if (allocateData(orig._dataLen, true)) memcpy(data, orig.data, orig._dataLen); } //if (orig._t) { _t = new(std::nothrow) Transition(orig._t->_dur, orig._t->_briT, orig._t->_cctT, orig._t->_colorT); } //else markForReset(); // WLEDMM // if (orig.ledsrgb && !Segment::_globalLeds) { allocLeds(); if (ledsrgb) memcpy(ledsrgb, orig.ledsrgb, sizeof(CRGB)*length()); } // WLEDMM jMap = nullptr; //WLEDMM jMap } //WLEDMM: recreate ledsrgb if more space needed (will not free ledsrgb!) void Segment::allocLeds() { size_t size = sizeof(CRGB)*max((size_t) length(), ledmapMaxSize); // TroyHacks if ((size < sizeof(CRGB)) || (size > 164000)) { //softhack too small (<3) or too large (>160Kb) DEBUG_PRINTF("allocLeds warning: size == %u !!\n", size); if (ledsrgb && (ledsrgbSize == 0)) { USER_PRINTLN("allocLeds warning: ledsrgbSize == 0 but ledsrgb!=NULL"); free(ledsrgb); ledsrgb=nullptr; } // softhack007 clean up buffer } if ((size > 0) && (!ledsrgb || size > ledsrgbSize)) { //softhack dont allocate zero bytes DEBUG_PRINTF("allocLeds (%d,%d to %d,%d), %u from %u\n", start, startY, stop, stopY, size, ledsrgb?ledsrgbSize:0); // DONG - Valkyrie needs food, badly [Gauntlet, 1985] // WLEDMM this looks a bit over-compilicated, but it makes the re-allocation step an atomic and threadsafe operation CRGB* oldLedsRgb = ledsrgb; portENTER_CRITICAL(&ledsrgb_mux); ledsrgb = nullptr; portEXIT_CRITICAL(&ledsrgb_mux); if (oldLedsRgb) free(oldLedsRgb); // we need a bigger buffer, so free the old one first CRGB* newLedsRgb = (CRGB*)calloc(size, 1); // WLEDMM This is an OS call, so we should not wrap it in portEnterCRITICAL portENTER_CRITICAL(&ledsrgb_mux); ledsrgb = newLedsRgb; portEXIT_CRITICAL(&ledsrgb_mux); ledsrgbSize = ledsrgb?size:0; if (ledsrgb == nullptr) { USER_PRINTLN("allocLeds failed!!"); errorFlag = ERR_LOW_BUF; // WLEDMM raise errorflag } } else { //USER_PRINTF("reuse Leds %u from %u\n", size, ledsrgb?ledsrgbSize:0); } } // move constructor --> moves everything (including buffer) from orig to this Segment::Segment(Segment &&orig) noexcept { DEBUG_PRINTLN(F("-- Move segment constructor --")); // WLEDMM temporarily prevent any fast draw calls to old and new segment orig._isSimpleSegment = false; orig._isSuperSimpleSegment = false; memcpy((void*)this, (void*)&orig, sizeof(Segment)); orig.transitional = false; // old segment cannot be in transition any more #ifdef WLEDMM_FASTPATH // WLEDMM prevent any draw calls to old segment orig._isValid2D = false; #endif orig.name = nullptr; orig.data = nullptr; orig._dataLen = 0; orig._t = nullptr; orig.ledsrgb = nullptr; //WLEDMM orig.ledsrgbSize = 0; // WLEDMM orig.jMap = nullptr; //WLEDMM jMap } // copy assignment --> overwrite segment with orig - deletes old buffers in "this", but does not change orig! Segment& Segment::operator= (const Segment &orig) { DEBUG_PRINTLN(F("-- Copy-assignment segment --")); if (this != &orig) { // clean destination transitional = false; // copied segment cannot be in transition if (name) delete[] name; if (_t) delete _t; CRGB* oldLeds = ledsrgb; size_t oldLedsSize = ledsrgbSize; if (ledsrgb && !Segment::_globalLeds) free(ledsrgb); deallocateData(); // copy source memcpy((void*)this, (void*)&orig, sizeof(Segment)); transitional = false; // WLEDMM prevent any fast draw calls to this segment until the next frame starts //_isValid2D = false; _isSimpleSegment = false; _isSuperSimpleSegment = false; // erase pointers to allocated data name = nullptr; data = nullptr; _dataLen = 0; _t = nullptr; //if (!Segment::_globalLeds) {ledsrgb = oldLeds; ledsrgbSize = oldLedsSize;}; // WLEDMM reuse leds instead of ledsrgb = nullptr; if (!Segment::_globalLeds) {ledsrgb = nullptr; ledsrgbSize = 0;}; // WLEDMM copy has no buffers (yet) // copy source data if (orig.name) { name = new(std::nothrow) char[strlen(orig.name)+1]; if (name) strcpy(name, orig.name); } if (orig.data) { if (allocateData(orig._dataLen, true)) memcpy(data, orig.data, orig._dataLen); } //if (orig._t) { _t = new(std::nothrow) Transition(orig._t->_dur, orig._t->_briT, orig._t->_cctT, orig._t->_colorT); } //else markForReset(); // WLEDMM //if (orig.ledsrgb && !Segment::_globalLeds) { allocLeds(); if (ledsrgb) memcpy(ledsrgb, orig.ledsrgb, sizeof(CRGB)*length()); } // WLEDMM don't copy old buffer jMap = nullptr; //WLEDMM jMap } return *this; } // move assignment --> moves everything (including buffers) from "orig" to "this" Segment& Segment::operator= (Segment &&orig) noexcept { DEBUG_PRINTLN(F("-- Move-assignment segment --")); if (this != &orig) { transitional = false; // just temporary if (name) { delete[] name; name = nullptr; } // free old name deallocateData(); // free old runtime data if (_t) { delete _t; _t = nullptr; } if (ledsrgb && !Segment::_globalLeds) free(ledsrgb); //WLEDMM: not needed anymore as we will use leds from copy. no need to nullify ledsrgb as it gets new value in memcpy // WLEDMM temporarily prevent any fast draw calls to old and new segment orig._isSimpleSegment = false; orig._isSuperSimpleSegment = false; memcpy((void*)this, (void*)&orig, sizeof(Segment)); #ifdef WLEDMM_FASTPATH // WLEDMM temporarily prevent any draw calls to old segment orig._isValid2D = false; #endif orig.name = nullptr; orig.data = nullptr; orig._dataLen = 0; orig._t = nullptr; orig.ledsrgb = nullptr; //WLEDMM: do not free as moved to here orig.ledsrgbSize = 0; //WLEDMM orig.jMap = nullptr; //WLEDMM jMap } return *this; } bool Segment::allocateData(size_t len, bool allowOverdraft) { // WLEDMM allowOverdraft for temporary overdraft by segment copy constructor // WLEDMM if (data && _dataLen >= len) { // already allocated enough (reduce fragmentation) if ((call == 0) && (len > 0)) memset(data, 0, len); // erase buffer if called during effect initialisation return true; } //DEBUG_PRINTF("allocateData(%u) start %d, stop %d, vlen %d\n", len, start, stop, virtualLength()); deallocateData(); if (len == 0) return false; // nothing to do // limit to MAX_SEGMENT_DATA if there is no PSRAM, otherwise prefer functionality over speed #ifndef BOARD_HAS_PSRAM if (Segment::getUsedSegmentData() + len > MAX_SEGMENT_DATA) { if (!allowOverdraft || (Segment::getUsedSegmentData() + len > MAX_SEGMENT_OVERDATA)) { // WLEDMM 50% overdraft allowed temporarily static unsigned lastMsgTime = 0; if (millis() - lastMsgTime > 5000) { USER_PRINTF("Segment::allocateData: Segment data quota exceeded! used:%u request:%u max:%d\n", Segment::getUsedSegmentData(), len, MAX_SEGMENT_DATA); lastMsgTime = millis(); } if (len > 0) errorFlag = ERR_LOW_SEG_MEM; // WLEDMM raise errorflag return false; //not enough memory } } #endif // do not use SPI RAM on ESP32 since it is slow //#if defined(ARDUINO_ARCH_ESP32) && defined(BOARD_HAS_PSRAM) && defined(WLED_USE_PSRAM) //if (psramFound()) // data = (byte*) ps_malloc(len); //else //#endif data = (byte*) malloc(len); if (!data) { _dataLen = 0; // WLEDMM reset dataLen if ((errorFlag != ERR_LOW_MEM) && (errorFlag != ERR_LOW_SEG_MEM)) { // spam filter USER_PRINT(F("Segment::allocateData: FAILED to allocate ")); USER_PRINT(len); USER_PRINTLN(F(" bytes.")); } errorFlag = ERR_LOW_MEM; // WLEDMM raise errorflag return false; } //allocation failed Segment::addUsedSegmentData(len); DEBUG_PRINTF("Segment::allocateData: %u bytes allocated (%u used)\n", len, Segment::getUsedSegmentData()); _dataLen = len; memset(data, 0, len); if ((errorFlag == ERR_LOW_SEG_MEM) || (errorFlag == ERR_LOW_MEM) || (errorFlag == ERR_NORAM_PX)) errorFlag = ERR_NONE; // WLEDMM reset errorflag on success return true; } void Segment::deallocateData() { if (!data) { if (_dataLen>0) { Segment::addUsedSegmentData(-_dataLen); // WLEDMM fix housekeeping DEBUG_PRINTF("Segment::deallocateData unregistering %u bytes as unused.", _dataLen); } _dataLen = 0; return; } // WLEDMM reset dataLen free(data); data = nullptr; DEBUG_PRINTF("Segment::deallocateData: free'd %d bytes.\n", _dataLen); Segment::addUsedSegmentData(-_dataLen); _dataLen = 0; } /** * If reset of this segment was requested, clears runtime * settings of this segment. * Must not be called while an effect mode function is running * because it could access the data buffer and this method * may free that data buffer. */ void Segment::resetIfRequired() { if (reset) { if (ledsrgb && !Segment::_globalLeds) { free(ledsrgb); ledsrgb = nullptr; ledsrgbSize=0;} // WLEDMM segment has changed, so we need a fresh buffer. if (transitional && _t) { transitional = false; delete _t; _t = nullptr; } deallocateData(); next_time = 0; step = 0; call = 0; aux0 = 0; aux1 = 0; reset = false; // setOption(SEG_OPTION_RESET, false); startFrame(); // WLEDMM update cached propoerties if (isActive() && !freeze) { fill(BLACK); needsBlank = false; } // WLEDMM start clean #ifdef WLED_ENABLE_GIF endImagePlayback(this); #endif DEBUG_PRINTLN("Segment reset"); } else if (needsBlank) { startFrame(); // WLEDMM update cached propoerties if (isActive() && !freeze) { fill(BLACK); // WLEDMM start clean DEBUG_PRINTLN("Segment blanked"); needsBlank = false; } } } void Segment::setUpLeds() { // deallocation happens in resetIfRequired() as it is called when segment changes or in destructor if (Segment::_globalLeds) { #ifndef WLED_DISABLE_2D ledsrgb = &Segment::_globalLeds[start + startY*Segment::maxWidth]; ledsrgbSize = length() * sizeof(CRGB); // also set this when using global leds. //USER_PRINTF("\nsetUpLeds() Global LEDs: startX=%d stopx=%d startY=%d stopy=%d maxwidth=%d; length=%d, size=%d\n\n", start, stop, startY, stopY, Segment::maxWidth, length(), ledsrgbSize/3); #else ledsrgb = &Segment::_globalLeds[start]; ledsrgbSize = length() * sizeof(CRGB); // also set this when using global leds. #endif } else if (length() > 0) { //WLEDMM we always want a new buffer //softhack007 quickfix - avoid malloc(0) which is undefined behaviour (should not happen, but i've seen it) //#if defined(ARDUINO_ARCH_ESP32) && defined(BOARD_HAS_PSRAM) && defined(WLED_USE_PSRAM) //if (psramFound()) // ledsrgb = (CRGB*)ps_malloc(sizeof(CRGB)*length()); // softhack007 disabled; putting leds into psram leads to horrible slowdown on WROVER boards //else //#endif allocLeds(); //WLEDMM //USER_PRINTF("\nsetUpLeds() local LEDs: startX=%d stopx=%d startY=%d stopy=%d maxwidth=%d; length=%d, size=%d\n\n", start, stop, startY, stopY, Segment::maxWidth, length(), ledsrgbSize/3); } } CRGBPalette16 &Segment::loadPalette(CRGBPalette16 &targetPalette, uint8_t pal) const { static unsigned long _lastPaletteChange = millis() - 990000; // perhaps it should be per segment //WLEDMM changed init value to avoid pure orange after startup static CRGBPalette16 randomPalette = CRGBPalette16(DEFAULT_COLOR); static CRGBPalette16 prevRandomPalette = CRGBPalette16(CRGB(BLACK)); byte tcp[76] = { 255 }; //WLEDMM: prevent out-of-range access in loadDynamicGradientPalette() if (pal < 245 && pal > GRADIENT_PALETTE_COUNT+13) pal = 0; if (pal > 245 && (strip.customPalettes.size() == 0 || 255U-pal > strip.customPalettes.size()-1)) pal = 0; // TODO remove strip dependency by moving customPalettes out of strip //default palette. Differs depending on effect if (pal == 0) switch (mode) { case FX_MODE_FIRE_2012 : pal = 35; break; // heat palette case FX_MODE_COLORWAVES : pal = 26; break; // landscape 33 case FX_MODE_FILLNOISE8 : pal = 9; break; // ocean colors case FX_MODE_NOISE16_1 : pal = 20; break; // Drywet case FX_MODE_NOISE16_2 : pal = 43; break; // Blue cyan yellow case FX_MODE_NOISE16_3 : pal = 35; break; // heat palette case FX_MODE_NOISE16_4 : pal = 26; break; // landscape 33 case FX_MODE_GLITTER : pal = 11; break; // rainbow colors case FX_MODE_SUNRISE : pal = 35; break; // heat palette case FX_MODE_RAILWAY : pal = 3; break; // prim + sec case FX_MODE_2DSOAP : pal = 11; break; // rainbow colors } switch (pal) { case 0: //default palette. Exceptions for specific effects above targetPalette = PartyColors_p; break; case 1: {//Random smooth: periodically replace palette with a random one. Transition palette change in 500ms uint32_t timeSinceLastChange = millis() - _lastPaletteChange; if (timeSinceLastChange > randomPaletteChangeTime * 1000U) { prevRandomPalette = randomPalette; randomPalette = CRGBPalette16( CHSV(random8(), random8(160, 255), random8(128, 255)), CHSV(random8(), random8(160, 255), random8(128, 255)), CHSV(random8(), random8(160, 255), random8(128, 255)), CHSV(random8(), random8(160, 255), random8(128, 255))); _lastPaletteChange = millis(); timeSinceLastChange = 0; } //WLEDMM: smooth transitions of palettes instead of every 5 sec with short transition for (int i=0; i< 16; i++) { targetPalette[i].r = prevRandomPalette[i].r*(5000-timeSinceLastChange)/5000 + randomPalette[i].r*timeSinceLastChange/5000; targetPalette[i].g = prevRandomPalette[i].g*(5000-timeSinceLastChange)/5000 + randomPalette[i].g*timeSinceLastChange/5000; targetPalette[i].b = prevRandomPalette[i].b*(5000-timeSinceLastChange)/5000 + randomPalette[i].b*timeSinceLastChange/5000; } break;} case 74: {//periodically replace palette with a random one. Transition palette change in 500ms uint32_t timeSinceLastChange = millis() - _lastPaletteChange; if (timeSinceLastChange > randomPaletteChangeTime * 1000U) { prevRandomPalette = randomPalette; randomPalette = CRGBPalette16( CHSV(random8(), random8(160, 255), random8(128, 255)), CHSV(random8(), random8(160, 255), random8(128, 255)), CHSV(random8(), random8(160, 255), random8(128, 255)), CHSV(random8(), random8(160, 255), random8(128, 255))); _lastPaletteChange = millis(); timeSinceLastChange = 0; } if (timeSinceLastChange <= 250) { targetPalette = prevRandomPalette; // there needs to be 255 palette blends (48) for full blend but that is too resource intensive // so 128 is a compromise (we need to perform full blend of the two palettes as each segment can have random // palette selected but only 2 static palettes are used) size_t noOfBlends = ((128U * timeSinceLastChange) / 250U); for (size_t i=0; i245) { targetPalette = strip.customPalettes[255-pal]; // we checked bounds above } else { memcpy_P(tcp, (byte*)pgm_read_dword(&(gGradientPalettes[pal-13])), 72); targetPalette.loadDynamicGradientPalette(tcp); } break; } return targetPalette; } void Segment::startTransition(uint16_t dur) { if (transitional || _t) return; // already in transition no need to store anything // starting a transition has to occur before change so we get current values 1st uint8_t _briT = currentBri(on ? opacity : 0); uint8_t _cctT = currentBri(cct, true); CRGBPalette16 _palT = CRGBPalette16(DEFAULT_COLOR); loadPalette(_palT, palette); uint8_t _modeP = mode; uint32_t _colorT[NUM_COLORS]; for (size_t i=0; i_briT = _briT; _t->_cctT = _cctT; _t->_palT = _palT; _t->_modeP = _modeP; for (size_t i=0; i_colorT[i] = _colorT[i]; transitional = true; // setOption(SEG_OPTION_TRANSITIONAL, true); } // WLEDMM Segment::progress() is declared inline, see FX.h #if 0 // transition progression between 0-65535 uint16_t IRAM_ATTR_YN Segment::progress() const { if (!transitional || !_t) return 0xFFFFU; unsigned long timeNow = millis(); if (timeNow - _t->_start > _t->_dur || _t->_dur == 0) return 0xFFFFU; return (timeNow - _t->_start) * 0xFFFFU / _t->_dur; } #endif // WLEDMM Segment::currentBri() is declared inline, see FX.h #if 0 uint8_t IRAM_ATTR_YN Segment::currentBri(uint8_t briNew, bool useCct) const { uint32_t prog = (transitional && _t) ? progress() : 0xFFFFU; if (transitional && _t && prog < 0xFFFFU) { if (useCct) return ((briNew * prog) + _t->_cctT * (0xFFFFU - prog)) >> 16; else return ((briNew * prog) + _t->_briT * (0xFFFFU - prog)) >> 16; } else { return (useCct ? briNew : (on ? briNew : 0)); // WLEDMM aligned with upstream } } #endif inline uint8_t Segment::currentMode(uint8_t newMode) { return (progress()>32767U) ? newMode : (_t ? _t->_modeP : newMode); // change effect in the middle of transition } // WLEDMM: Segment::currentColor() moved to FX.h for better optimization by the compiler void Segment::setCurrentPalette() { loadPalette(_currentPalette, palette); if (transitional && _t && progress() < 0xFFFFU) { // blend palettes // there are about 255 blend passes of 48 "blends" to completely blend two palettes (in _dur time) // minimum blend time is 100ms maximum is 65535ms unsigned long timeMS = millis() - _t->_start; uint16_t noOfBlends = min(64UL, (255U * timeMS / _t->_dur) - _t->_prevPaletteBlends); // WLEDMM limit to 64 blends at once, prevent rollover for (unsigned i = 0; i < noOfBlends; i++, _t->_prevPaletteBlends++) nblendPaletteTowardPalette(_t->_palT, _currentPalette, 48); _currentPalette = _t->_palT; // copy transitioning/temporary palette } } void Segment::handleTransition() { if (!transitional || !_t) return; // Early exit if no transition active unsigned long maxWait = millis() + 20; if (mode == FX_MODE_STATIC && next_time > maxWait) next_time = maxWait; if (progress() == 0xFFFFU) { if (_t) { //if (_t->_modeP != mode) markForReset(); // WLEDMM effect transition disabled as it does not work (flashes due to double effect restart) delete _t; _t = nullptr; } transitional = false; // finish transitioning segment } } 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 if (Segment::maxHeight>1) boundsUnchanged &= (startY == i1Y && stopY == i2Y); // 2D #endif if (boundsUnchanged && (!grp || (grouping == grp && spacing == spc)) && (ofs == UINT16_MAX || ofs == offset)) return; stateChanged = true; // send UDP/WS broadcast if (stop>start) markForBlank(); //turn old segment range off // WLEDMM stop > start // toDo: check if this can be skipped when boundsUnchanged if (i2 <= i1) { //disable segment #ifdef WLED_ENABLE_GIF endImagePlayback(this); #endif stop = 0; markForReset(); return; } if (esp32SemTake(segmentMux, 2100) == pdTRUE) { // wait long, but don't wait forever // WLEDMM acquire lock before doing critical changes to segment if (i1 < Segment::maxWidth || (i1 >= Segment::maxWidth*Segment::maxHeight && i1 < strip.getLengthTotal())) start = i1; // Segment::maxWidth equals strip.getLengthTotal() for 1D stop = i2 > Segment::maxWidth*Segment::maxHeight ? min(i2,strip.getLengthTotal()) : (i2 > Segment::maxWidth ? Segment::maxWidth : max((uint16_t)1,i2)); // WLEDMM: use native min/max startY = 0; stopY = 1; #ifndef WLED_DISABLE_2D if (Segment::maxHeight>1) { // 2D if (i1Y < Segment::maxHeight) startY = i1Y; stopY = i2Y > Segment::maxHeight ? Segment::maxHeight : max((uint16_t)1,i2Y); // WLEDMM: use native min/max } #endif if (grp) { grouping = grp; spacing = spc; } if (ofs < UINT16_MAX) offset = ofs; esp32SemGive(segmentMux); } else { DEBUG_PRINTLN(F("Segment::setUp: Failed to acquire segmentMux, skipping bounds update.")); boundsUnchanged = true; } if (!boundsUnchanged) { markForReset(); refreshLightCapabilities(); } } bool Segment::setColor(uint8_t slot, uint32_t c) { //returns true if changed if (slot >= NUM_COLORS || c == colors[slot]) return false; if (!_isRGB && !_hasW) { if (slot == 0 && c == BLACK) return false; // on/off segment cannot have primary color black if (slot == 1 && c != BLACK) return false; // on/off segment cannot have secondary color non black } if (fadeTransition && on) startTransition(strip.getTransition()); // start transition prior to change // WLEDMM only on real change colors[slot] = c; stateChanged = true; // send UDP/WS broadcast return true; } void Segment::setCCT(uint16_t k) { if (k > 255) { //kelvin value, convert to 0-255 if (k < 1900) k = 1900; if (k > 10091) k = 10091; k = (k - 1900) >> 5; } if (cct == k) return; if (fadeTransition && on) startTransition(strip.getTransition()); // start transition prior to change cct = k; stateChanged = true; // send UDP/WS broadcast } void Segment::setOpacity(uint8_t o) { if (opacity == o) return; if (fadeTransition) startTransition(strip.getTransition()); // start transition prior to change opacity = o; stateChanged = true; // send UDP/WS broadcast } void Segment::setOption(uint8_t n, bool val) { bool prevOn = on; if (fadeTransition && n == SEG_OPTION_ON && val != prevOn) startTransition(strip.getTransition()); // start transition prior to change if (val) options |= 0x01 << n; else options &= ~(0x01 << n); if (!(n == SEG_OPTION_SELECTED || n == SEG_OPTION_RESET || n == SEG_OPTION_TRANSITIONAL)) stateChanged = true; // send UDP/WS broadcast } void Segment::setMode(uint8_t fx, bool loadDefaults, bool sliderDefaultsOnly) { //WLEDMM: return to old setting if not explicitly set static int16_t oldMap = -1; static int16_t oldSim = -1; static int16_t oldPalette = -1; // if we have a valid mode & is not reserved if (fx < strip.getModeCount() && strncmp_P("RSVD", strip.getModeData(fx), 4)) { if (fx != mode) { startTransition(strip.getTransition()); // set effect transitions //markForReset(); // transition will handle this 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; if (!sliderDefaultsOnly) { //WLEDMM: return to old setting if not explicitly set sOpt = extractModeDefaults(fx, "m12"); if (sOpt >= 0) {if (oldMap==-1) oldMap = map1D2D; map1D2D = constrain(sOpt, 0, 7);} else {if (oldMap!=-1) map1D2D = oldMap; oldMap = -1;} sOpt = extractModeDefaults(fx, "si"); if (sOpt >= 0) {if (oldSim==-1) oldSim = soundSim; soundSim = constrain(sOpt, 0, 1);} else {if (oldSim!=-1) soundSim = oldSim; oldSim = -1;} 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) {if (oldPalette==-1) oldPalette = palette; setPalette(sOpt);} else {if (oldPalette!=-1) setPalette(oldPalette); oldPalette = -1;} } } /*if (!fadeTransition)*/ markForReset(); // WLEDMM quickfix for effect "double startup" bug. stateChanged = true; // send UDP/WS broadcast } } } void Segment::setPalette(uint8_t pal) { if (pal < 245 && pal > GRADIENT_PALETTE_COUNT+13) pal = 0; // built in palettes if (pal > 245 && (strip.customPalettes.size() == 0 || 255U-pal > strip.customPalettes.size()-1)) pal = 0; // custom palettes if (pal != palette) { if (strip.paletteFade && on) startTransition(strip.getTransition()); palette = pal; stateChanged = true; // send UDP/WS broadcast } } // 2D matrix // // WLEDMM Segment::virtualWidth() and Segment::virtualHeight() are declared inline, see FX.h // uint16_t Segment::nrOfVStrips() const { uint16_t vLen = 1; #ifndef WLED_DISABLE_2D if (is2D()) { switch (map1D2D) { case M12_pBar: vLen = calc_virtualWidth(); break; case M12_sCircle: //WLEDMM vLen = (calc_virtualWidth() + calc_virtualHeight()) / 6; // take third of the average width break; case M12_sBlock: //WLEDMM vLen = (calc_virtualWidth() + calc_virtualHeight()) / 8; // take half of the average width break; } } #endif return vLen; } //WLEDMM jMap struct XandY { uint8_t x; uint8_t y; }; struct ArrayAndSize { uint16_t size; XandY *array; }; class JMapC { public: char previousSegmentName[WLED_MAX_SEGNAME_LEN+12] = ""; ~JMapC() { DEBUG_PRINTLN("~JMapC"); deletejVectorMap(); } void deletejVectorMap() { if (jVectorMap.size() > 0) { DEBUG_PRINTLN("delete jVectorMap"); for (size_t i=0; i 0) return size; else return SEGMENT.calc_virtualWidth() * SEGMENT.calc_virtualHeight(); // calc pixel sizes } void setPixelColor(uint16_t i, uint32_t col) { updatejMapDoc(); if (jVectorMap.size() > i) { if (i==0) { SEGMENT.fadeToBlackBy(10); //as not all pixels used } for (int j=0; j i) // implies jVectorMap.size() > 0, because i is unsigned return SEGMENT.getPixelColorXY(jVectorMap[i].array[0].x * scale, jVectorMap[i].array[0].y * scale); else return 0; } private: std::vector jVectorMap; StaticJsonDocument<4096> docChunk; //must fit forks with about 32 points each uint8_t scale=1; void updatejMapDoc() { if (SEGMENT.name == nullptr && jVectorMap.size() > 0) { deletejVectorMap(); } else if (SEGMENT.name != nullptr && strcmp(SEGMENT.name, previousSegmentName) != 0) { uint32_t dataSize = 0; deletejVectorMap(); DEBUG_PRINT("New "); DEBUG_PRINTLN(SEGMENT.name); char jMapFileName[WLED_MAX_SEGNAME_LEN+12] = {'\0'}; // we need at most 32 + 7 bytes strcpy(jMapFileName, "/"); strcat(jMapFileName, SEGMENT.name); strcat(jMapFileName, ".json"); File jMapFile; jMapFile = WLED_FS.open(jMapFileName, "r"); uint_fast16_t maxWidth = 0; // WLEDMM fix uint8 overflow for large width/height uint_fast16_t maxHeight = 0; // WLEDMM //https://arduinojson.org/v6/how-to/deserialize-a-very-large-document/ jMapFile.find("["); do { //for each element in the array DeserializationError err = deserializeJson(docChunk, jMapFile); // serializeJson(docChunk, Serial); USER_PRINTLN(); // USER_PRINTF("docChunk %u / %u%% (%u %u %u) %u\n", (unsigned int)docChunk.memoryUsage(), 100 * docChunk.memoryUsage() / docChunk.capacity(), (unsigned int)docChunk.size(), docChunk.overflowed(), (unsigned int)docChunk.nesting(), jMapFile.size()); if (err) { USER_PRINTF("deserializeJson() of parseTree failed with code %s\n", err.c_str()); USER_FLUSH(); // softhack007: DO NOT delete SEGMENT.name - it's owned by Segment class, deleting it from outside can lead to use-after-free // if (SEGMENT.name) delete[] SEGMENT.name; SEGMENT.name = nullptr; //need to clear the name as otherwise continuously loaded // softhack007 avoid deleting nullptr strlcpy(previousSegmentName, SEGMENT.name, sizeof(previousSegmentName)); // Mark name as processed to avoid reload loop if (jMapFile) jMapFile.close(); // make sure the file is closed return; } if (docChunk.is()) { //each item is or an array of arrays (fork) or an array of x,y (no fork) //fill the vector with arrays and get the width and height of the jMap JsonArray arrayChunk = docChunk.as(); ArrayAndSize arrayAndSize; arrayAndSize.size = 0; if (arrayChunk[0].is()) { //if array of arrays arrayAndSize.array = new(std::nothrow) XandY[arrayChunk.size()]; for (JsonVariant arrayElement: arrayChunk) { maxWidth = max((uint16_t)maxWidth, arrayElement[0].as()); // WLEDMM use native min/max maxHeight = max((uint16_t)maxHeight, arrayElement[1].as()); // WLEDMM arrayAndSize.array[arrayAndSize.size].x = arrayElement[0].as(); arrayAndSize.array[arrayAndSize.size].y = arrayElement[1].as(); arrayAndSize.size++; dataSize += sizeof(XandY); } } else { // if array (of x and y) arrayAndSize.array = new(std::nothrow) XandY[1]; maxWidth = max((uint16_t)maxWidth, arrayChunk[0].as()); // WLEDMM use native min/max maxHeight = max((uint16_t)maxHeight, arrayChunk[1].as()); // WLEDMM arrayAndSize.array[arrayAndSize.size].x = arrayChunk[0].as(); arrayAndSize.array[arrayAndSize.size].y = arrayChunk[1].as(); arrayAndSize.size++; dataSize += sizeof(XandY); } jVectorMap.push_back(arrayAndSize); dataSize += sizeof(arrayAndSize); } } while (jMapFile.findUntil(",", "]")); jMapFile.close(); maxWidth++; maxHeight++; scale = min(SEGMENT.calc_virtualWidth() / maxWidth, SEGMENT.calc_virtualHeight() / maxHeight); // WLEDMM re-calc width/heiht from active settings dataSize += sizeof(jVectorMap); USER_PRINT("dataSize "); USER_PRINT(dataSize); USER_PRINT(" scale "); USER_PRINTLN(scale); strlcpy(previousSegmentName, SEGMENT.name, sizeof(previousSegmentName)); } } //updatejMapDoc }; //class JMapC //WLEDMM jMap void Segment::createjMap() { if (!jMap) { DEBUG_PRINTLN("createjMap"); jMap = new(std::nothrow) JMapC(); } } //WLEDMM jMap void Segment::deletejMap() { //Should be called from ~Segment but causes crash (and ~Segment is called quite often...) if (jMap) { DEBUG_PRINTLN("deletejMap"); delete (JMapC *)jMap; jMap = nullptr; } } // Constants for mapping mode "Pinwheel" #ifndef WLED_DISABLE_2D constexpr int Pinwheel_Steps_Small = 72; // no holes up to 16x16 constexpr int Pinwheel_Size_Small = 16; // larger than this -> use "Medium" constexpr int Pinwheel_Steps_Medium = 192; // no holes up to 32x32 constexpr int Pinwheel_Size_Medium = 32; // larger than this -> use "Big" constexpr int Pinwheel_Steps_Big = 304; // no holes up to 50x50 constexpr int Pinwheel_Size_Big = 50; // larger than this -> use "XL" constexpr int Pinwheel_Steps_XL = 368; constexpr int Pinwheel_Size_XL = 58; // larger than this -> use "XXL" constexpr int Pinwheel_Steps_XXL = 456; constexpr int Pinwheel_Size_XXL = 68; // larger than this -> use "LL" constexpr int Pinwheel_Steps_LL = 592; // 128x64 no holes, 28fps constexpr float Int_to_Rad_Small = (DEG_TO_RAD * 360) / Pinwheel_Steps_Small; // conversion: from 0...72 to Radians constexpr float Int_to_Rad_Med = (DEG_TO_RAD * 360) / Pinwheel_Steps_Medium; // conversion: from 0...192 to Radians constexpr float Int_to_Rad_Big = (DEG_TO_RAD * 360) / Pinwheel_Steps_Big; // conversion: from 0...304 to Radians constexpr float Int_to_Rad_XL = (DEG_TO_RAD * 360) / Pinwheel_Steps_XL; // conversion: from 0...368 to Radians constexpr float Int_to_Rad_XXL = (DEG_TO_RAD * 360) / Pinwheel_Steps_XXL; // conversion: from 0...456 to Radians constexpr float Int_to_Rad_LL = (DEG_TO_RAD * 360) / Pinwheel_Steps_LL; // conversion: from 0...592 to Radians constexpr int Fixed_Scale = 32768; // fixpoint scaling factor (15bit for fraction) // Pinwheel helper function: pixel index to radians static float getPinwheelAngle(int i, int vW, int vH) { int maxXY = max(vW, vH); if (maxXY <= Pinwheel_Size_Small) return float(i) * Int_to_Rad_Small; if (maxXY <= Pinwheel_Size_Medium) return float(i) * Int_to_Rad_Med; if (maxXY <= Pinwheel_Size_Big) return float(i) * Int_to_Rad_Big; if (maxXY <= Pinwheel_Size_XL) return float(i) * Int_to_Rad_XL; if (maxXY <= Pinwheel_Size_XXL) return float(i) * Int_to_Rad_XXL; // else return float(i) * Int_to_Rad_LL; } // Pinwheel helper function: matrix dimensions to number of rays static int getPinwheelLength(int vW, int vH) { int maxXY = max(vW, vH); if (maxXY <= Pinwheel_Size_Small) return Pinwheel_Steps_Small; if (maxXY <= Pinwheel_Size_Medium) return Pinwheel_Steps_Medium; if (maxXY <= Pinwheel_Size_Big) return Pinwheel_Steps_Big; if (maxXY <= Pinwheel_Size_XL) return Pinwheel_Steps_XL; if (maxXY <= Pinwheel_Size_XXL) return Pinwheel_Steps_XXL; // else return Pinwheel_Steps_LL; } #endif // 1D strip uint16_t Segment::calc_virtualLength() const { #ifndef WLED_DISABLE_2D if (is2D()) { uint16_t vW = calc_virtualWidth(); uint16_t vH = calc_virtualHeight(); uint16_t vLen = vW * vH; // use all pixels from segment switch (map1D2D) { case M12_pBar: vLen = vH; break; case M12_pCorner: vLen = max(vW,vH); // get the longest dimension break; case M12_pArc: { unsigned vLen2 = vW * vW + vH * vH; // length ^2 if (vLen2 < UINT16_MAX) vLen = sqrt32_bw(vLen2); // use faster function for 16bit values else vLen = sqrtf(vLen2); // fall-back to float if bigger if (vW != vH) vLen++; // round up } break; case M12_jMap: //WLEDMM jMap if (jMap) vLen = ((JMapC *)jMap)->length(); break; case M12_sCircle: //WLEDMM vLen = max(vW,vH); // get the longest dimension // vLen = (virtualWidth() + virtualHeight()) * 3; break; case M12_sBlock: //WLEDMM if (nrOfVStrips()>1) vLen = max(vW,vH) * 4;//0.5; // get the longest dimension else vLen = max(vW,vH) * 0.5f; // get the longest dimension break; case M12_sPinwheel: vLen = getPinwheelLength(vW, vH); break; } return vLen; } #endif uint16_t groupLen = groupLength(); uint16_t virtLength = (length() + groupLen - 1) / groupLen; if (mirror && width() > 1) virtLength = (virtLength + 1) /2; // divide by 2 if mirror, leave at least a single LED // WLEDMM bugfix for pseudo 2d strips return virtLength; } //WLEDMM used for M12_sBlock static void xyFromBlock(uint16_t &x,uint16_t &y, uint16_t i, uint16_t vW, uint16_t vH, uint16_t vStrip) { float i2; if (i<=SEGLEN*0.25f) { //top, left to right i2 = i/(SEGLEN*0.25f); x = vW / 2 - vStrip - 1 + i2 * vStrip * 2; y = vH / 2 - vStrip - 1; } else if (i <= SEGLEN * 0.5f) { //right, top to bottom i2 = (i-SEGLEN*0.25f)/(SEGLEN*0.25f); x = vW / 2 + vStrip; y = vH / 2 - vStrip - 1 + i2 * vStrip * 2; } else if (i <= SEGLEN * 0.75f) { //bottom, right to left i2 = (i-SEGLEN*0.5f)/(SEGLEN*0.25f); x = vW / 2 + vStrip - i2 * vStrip * 2; y = vH / 2 + vStrip; } else if (i <= SEGLEN) { //left, bottom to top i2 = (i-SEGLEN*0.75f)/(SEGLEN*0.25f); x = vW / 2 - vStrip - 1; y = vH / 2 + vStrip - i2 * vStrip * 2; } // softhack007 not sure if clamping is necessary //x = min(x, uint16_t(vW-1)); // clamp x at vW-1 //y = min(y, uint16_t(vH-1)); // clamp y at vH-1 } void IRAM_ATTR_YN WLED_O2_ATTR __attribute__((hot)) Segment::setPixelColor(int i, uint32_t col) //WLEDMM: IRAM_ATTR conditionally { if (!isActive()) return; // not active #ifndef WLED_DISABLE_2D int vStrip = i>>16; // hack to allow running on virtual strips (2D segment columns/rows) #endif i &= 0xFFFF; if (unsigned(i) >= virtualLength()) return; // if pixel would fall out of segment just exit //WLEDMM unsigned(i)>SEGLEN also catches "i<0" #ifndef WLED_DISABLE_2D if (is2D()) { uint16_t vH = virtualHeight(); // segment height in logical pixels uint16_t vW = virtualWidth(); switch (map1D2D) { case M12_Pixels: // use all available pixels as a long strip setPixelColorXY(i % vW, i / vW, col); break; case M12_pBar: // expand 1D effect vertically or have it play on virtual strips if (vStrip>0) setPixelColorXY(vStrip - 1, vH - i - 1, col); else drawLine(0,vH-i-1, vW-1,vH-i-1, col, false); // WLEDMM draw line instead of plotting each pixel break; case M12_pArc: // expand in circular fashion from center if (i==0) setPixelColorXY(0, 0, col); else { if (i == virtualLength() - 1) setPixelColorXY(vW-1, vH-1, col); // Last i always fill corner if (!_isSuperSimpleSegment) { // WLEDMM: drawArc() is faster if it's NOT "super simple" as the regular M12_pArc // can do "useSymmetry" to speed things along, but a more complicated segment likey // uses mirroring which generates a symmetry speed-up, or other things which mean // less pixels are calculated. drawArc(0, 0, i, col); } else { //WLEDMM: some optimizations for the drawing loop // pre-calculate loop limits, exploit symmetry at 45deg float radius = float(i); // float step = HALF_PI / (2.85f * radius); // upstream uses this float step = HALF_PI / (M_PI * radius); // WLEDMM we use the correct circumference bool useSymmetry = (max(vH, vW) > 20); // for segments wider than 20 pixels, we exploit symmetry unsigned numSteps; if (useSymmetry) numSteps = 1 + ((HALF_PI/2.0f + step/2.0f) / step); // with symmetry else numSteps = 1 + ((HALF_PI + step/2.0f) / step); // without symmetry float rad = 0.0f; for (unsigned count = 0; count < numSteps; count++) { // may want to try float version as well (with or without antialiasing) // int x = max(0, min(vW-1, (int)roundf(sinf(rad) * radius))); // int y = max(0, min(vH-1, (int)roundf(cosf(rad) * radius))); int x = roundf(sinf(rad) * radius); int y = roundf(cosf(rad) * radius); setPixelColorXY(x, y, col); if(useSymmetry) setPixelColorXY(y, x, col);// WLEDMM rad += step; } // // Bresenham’s Algorithm (may not fill every pixel) // int d = 3 - (2*i); // int y = i, x = 0; // while (y >= x) { // setPixelColorXY(x, y, col); // setPixelColorXY(y, x, col); // x++; // if (d > 0) { // y--; // d += 4 * (x - y) + 10; // } else { // d += 4 * x + 6; // } // } } } break; case M12_pCorner: { int x = min(i, vW-1); int y = min(i, vH-1); if (i <= y) drawLine(0,y, x,y, col, false); // botton line (if visible) if (i <= x) drawLine(x,0, x,y, col, false); // right line (if visible) } break; case M12_jMap: //WLEDMM jMap if (jMap) ((JMapC *)jMap)->setPixelColor(i, col); break; case M12_sCircle: //WLEDMM if (vStrip > 0) { int x = roundf(sinf(360*i/SEGLEN*DEG_TO_RAD) * vW * (vStrip+1)/nrOfVStrips()); int y = roundf(cosf(360*i/SEGLEN*DEG_TO_RAD) * vW * (vStrip+1)/nrOfVStrips()); setPixelColorXY(x + vW/2, y + vH/2, col); } else // pArc -> circle drawArc(vW/2, vH/2, i/2, col); break; case M12_sBlock: //WLEDMM if (vStrip > 0) { //vStrip+1 is distance from centre, i is how much of the square is filled uint16_t x=0,y=0; xyFromBlock(x,y, i, vW, vH, (vStrip+1)*2); setPixelColorXY(x, y, col); } else { // pCorner -> block int centerX = (vW+1)/2 - 1; int centerY = (vH+1)/2 - 1; int xLeft = max(centerX-i, 0); int yTop = max(centerY-i, 0); int xRight = min(centerX+i+1, vW-1); int yBottom = min(centerY+i+1, vH-1); if (yTop == centerY-i) drawLine(xLeft,yTop, xRight, yTop, col); // top and bottom horizontal lines, if visible if (yBottom == centerY+i+1) drawLine(xLeft,yBottom, xRight, yBottom, col); if (xLeft == centerX-i) drawLine(xLeft,yTop, xLeft, yBottom, col); // left and right vertical lines, if visible if (xRight == centerX+i+1) drawLine(xRight,yTop, xRight, yBottom, col); } break; case M12_sPinwheel: { // WLEDMM shortcut when no grouping/spacing used bool simpleSegment = (grouping == 1) && (spacing == 0); uint32_t scaled_col = col; if (simpleSegment) { // segment brightness must be pre-calculated for the "fast" setPixelColorXY variant! uint8_t _bri_t = currentBri(on ? opacity : 0); if (!_bri_t && !transitional) return; if (_bri_t < 255) scaled_col = color_fade(col, _bri_t); } // i = angle --> 0 - 296 (Big), 0 - 192 (Medium), 0 - 72 (Small) float centerX = roundf((vW-1) / 2.0f); float centerY = roundf((vH-1) / 2.0f); float angleRad = getPinwheelAngle(i, vW, vH); // angle in radians float cosVal = cosf(angleRad); float sinVal = sinf(angleRad); // avoid re-painting the same pixel int lastX = INT_MIN; // impossible position int lastY = INT_MIN; // impossible position // draw line at angle, starting at center and ending at the segment edge // we use fixed point math for better speed. Starting distance is 0.5 for better rounding // int_fast16_t and int_fast32_t types changed to int, minimum bits commented int posx = (centerX + 0.5f * cosVal) * Fixed_Scale; // X starting position in fixed point 18 bit int posy = (centerY + 0.5f * sinVal) * Fixed_Scale; // Y starting position in fixed point 18 bit int inc_x = cosVal * Fixed_Scale; // X increment per step (fixed point) 10 bit int inc_y = sinVal * Fixed_Scale; // Y increment per step (fixed point) 10 bit int32_t maxX = vW * Fixed_Scale; // X edge in fixedpoint int32_t maxY = vH * Fixed_Scale; // Y edge in fixedpoint // Odd rays start further from center if prevRay started at center. static int prevRay = INT_MIN; // previous ray number if ((i % 2 == 1) && (i - 1 == prevRay || i + 1 == prevRay)) { int jump = min(vW/3, vH/3); // can add 2 if using medium pinwheel posx += inc_x * jump; posy += inc_y * jump; } prevRay = i; // draw ray until we hit any edge while ((posx >= 0) && (posy >= 0) && (posx < maxX) && (posy < maxY)) { // scale down to integer (compiler will replace division with appropriate bitshift) int x = posx / Fixed_Scale; int y = posy / Fixed_Scale; // set pixel if (x != lastX || y != lastY) { // only paint if pixel position is different if (simpleSegment) setPixelColorXY_fast(x, y, col, scaled_col, vW, vH); else setPixelColorXY_slow(x, y, col); } lastX = x; lastY = y; // advance to next position posx += inc_x; posy += inc_y; } break; } } return; } else if (Segment::maxHeight!=1 && (width()==1 || height()==1)) { if (start < Segment::maxWidth*Segment::maxHeight) { // we have a vertical or horizontal 1D segment (WARNING: virtual...() may be transposed) int x = 0, y = 0; if (virtualHeight()>1) y = i; else if (virtualWidth() >1) x = i; setPixelColorXY(x, y, col); return; } } #endif if (ledsrgb) ledsrgb[i] = col; uint16_t len = length(); uint8_t _bri_t = currentBri(on ? opacity : 0); if (!_bri_t && !transitional && fadeTransition) return; // if _bri_t == 0 && segment is not transitioning && transitions are enabled then save a few CPU cycles if (_bri_t < 255) { col = color_fade(col, _bri_t); } // expand pixel (taking into account start, grouping, spacing [and offset]) i = i * groupLength(); if (reverse) { // is segment reversed? if (mirror) { // is segment mirrored? i = (len - 1) / 2 - i; //only need to index half the pixels } else { i = (len - 1) - i; } } i += start; // starting pixel in a group #if 0 // needs more testing // WLEDMM shortcut when no grouping/spacing used bool simpleSegment = !mirror && (grouping == 1) && (spacing == 0); if (simpleSegment) { int indexSet = i + offset; // offset/phase if (indexSet >= stop) indexSet -= len; // wrap strip.setPixelColor(indexSet, col); return; } #endif // set all the pixels in the group for (int j = 0; j < grouping; j++) { uint16_t indexSet = i + ((reverse) ? -j : j); if (indexSet >= start && indexSet < stop) { if (mirror) { //set the corresponding mirrored pixel uint16_t indexMir = stop - indexSet + start - 1; indexMir += offset; // offset/phase if (indexMir >= stop) indexMir -= len; // wrap strip.setPixelColor(indexMir, col); } indexSet += offset; // offset/phase if (indexSet >= stop) indexSet -= len; // wrap strip.setPixelColor(indexSet, col); } } } // anti-aliased normalized version of setPixelColor() void Segment::setPixelColor(float i, uint32_t col, bool aa) { if (!isActive()) return; // not active int vStrip = int(i/10.0f); // hack to allow running on virtual strips (2D segment columns/rows) i -= int(i); if (i<0.0f || i>1.0f) return; // not normalized float fC = i * (virtualLength()-1); if (aa) { uint16_t iL = roundf(fC-0.49f); uint16_t iR = roundf(fC+0.49f); float dL = (fC - iL)*(fC - iL); float dR = (iR - fC)*(iR - fC); uint32_t cIL = getPixelColor(iL | (vStrip<<16)); uint32_t cIR = getPixelColor(iR | (vStrip<<16)); if (iR!=iL) { // blend L pixel cIL = color_blend(col, cIL, uint8_t(dL*255.0f)); setPixelColor(iL | (vStrip<<16), cIL); // blend R pixel cIR = color_blend(col, cIR, uint8_t(dR*255.0f)); setPixelColor(iR | (vStrip<<16), cIR); } else { // exact match (x & y land on a pixel) setPixelColor(iL | (vStrip<<16), col); } } else { setPixelColor(uint16_t(roundf(fC)) | (vStrip<<16), col); } } uint32_t WLED_O2_ATTR __attribute__((hot)) Segment::getPixelColor(int i) const { if (!isActive()) return 0; // not active #ifndef WLED_DISABLE_2D int vStrip = i>>16; #endif i &= 0xFFFF; #ifndef WLED_DISABLE_2D if (is2D()) { uint16_t vH = virtualHeight(); // segment height in logical pixels uint16_t vW = virtualWidth(); switch (map1D2D) { case M12_Pixels: return getPixelColorXY(i % vW, i / vW); break; case M12_pBar: if (vStrip>0) return getPixelColorXY(vStrip - 1, vH - i -1); else return getPixelColorXY(0, vH - i -1); break; case M12_pCorner: case M12_pArc: { if (i < max(vW, vH)) { return vW>vH ? getPixelColorXY(i, 0) : getPixelColorXY(0, i); // Corner and Arc break; } float minradius = float(i) - 0.1f; const int minradius2 = roundf(minradius * minradius); int startX, startY; if (vW >= vH) {startX = vW - 1; startY = 1;} // Last Column else {startX = 1; startY = vH - 1;} // Last Row // Loop through only last row/column depending on orientation for (int x = startX; x < vW; x++) { int newX2 = x * x; for (int y = startY; y < vH; y++) { int newY2 = y * y; if (newX2 + newY2 >= minradius2) return getPixelColorXY(x, y); } } return getPixelColorXY(vW-1, vH-1); // Last pixel break; } case M12_jMap: //WLEDMM jMap if (jMap) return ((JMapC *)jMap)->getPixelColor(i); break; case M12_sCircle: //WLEDMM if (vStrip > 0) { int x = roundf(sinf(360*i/SEGLEN*DEG_TO_RAD) * vW * (vStrip+1)/nrOfVStrips()); int y = roundf(cosf(360*i/SEGLEN*DEG_TO_RAD) * vW * (vStrip+1)/nrOfVStrips()); return getPixelColorXY(x + vW/2, y + vH/2); } else return vW>vH ? getPixelColorXY(i, 0) : getPixelColorXY(0, i); break; case M12_sBlock: //WLEDMM if (vStrip > 0) { uint16_t x=0,y=0; xyFromBlock(x,y, i, vW, vH, (vStrip+1)*2); return getPixelColorXY(x, y); } else return getPixelColorXY(vW / 2, vH / 2 - i - 1); break; case M12_sPinwheel: // not 100% accurate, returns pixel at outer edge // i = angle --> 0 - 296 (Big), 0 - 192 (Medium), 0 - 72 (Small) float centerX = roundf((vW-1) / 2.0f); float centerY = roundf((vH-1) / 2.0f); float angleRad = getPinwheelAngle(i, vW, vH); // angle in radians float cosVal = cosf(angleRad); float sinVal = sinf(angleRad); int posx = (centerX + 0.5f * cosVal) * Fixed_Scale; // X starting position in fixed point 18 bit int posy = (centerY + 0.5f * sinVal) * Fixed_Scale; // Y starting position in fixed point 18 bit int inc_x = cosVal * Fixed_Scale; // X increment per step (fixed point) 10 bit int inc_y = sinVal * Fixed_Scale; // Y increment per step (fixed point) 10 bit int32_t maxX = vW * Fixed_Scale; // X edge in fixedpoint int32_t maxY = vH * Fixed_Scale; // Y edge in fixedpoint // trace ray from center until we hit any edge - to avoid rounding problems, we use the same method as in setPixelColor int x = INT_MIN; int y = INT_MIN; while ((posx >= 0) && (posy >= 0) && (posx < maxX) && (posy < maxY)) { // scale down to integer (compiler will replace division with appropriate bitshift) x = posx / Fixed_Scale; y = posy / Fixed_Scale; // advance to next position posx += inc_x; posy += inc_y; } return getPixelColorXY(x, y); break; } return 0; } #endif if (ledsrgb) return RGBW32(ledsrgb[i].r, ledsrgb[i].g, ledsrgb[i].b, 0); if (reverse) i = virtualLength() - i - 1; i *= groupLength(); i += start; /* offset/phase */ if (offset < INT16_MAX) i += offset; // WLEDMM if ((i >= stop) && (stop>0)) i -= length(); // WLEDMM avoid negative index (stop = 0 is a possible value) if (i<0) i=0; // WLEDMM just to be 100% sure return strip.getPixelColorRestored(i); } uint8_t Segment::differs(Segment& b) const { uint8_t d = 0; if (start != b.start) d |= SEG_DIFFERS_BOUNDS; if (stop != b.stop) d |= SEG_DIFFERS_BOUNDS; if (offset != b.offset) d |= SEG_DIFFERS_GSO; if (grouping != b.grouping) d |= SEG_DIFFERS_GSO; if (spacing != b.spacing) d |= SEG_DIFFERS_GSO; if (opacity != b.opacity) d |= SEG_DIFFERS_BRI; if (mode != b.mode) d |= SEG_DIFFERS_FX; if (speed != b.speed) d |= SEG_DIFFERS_FX; if (intensity != b.intensity) d |= SEG_DIFFERS_FX; if (palette != b.palette) d |= SEG_DIFFERS_FX; if (custom1 != b.custom1) d |= SEG_DIFFERS_FX; if (custom2 != b.custom2) d |= SEG_DIFFERS_FX; if (custom3 != b.custom3) d |= SEG_DIFFERS_FX; if (check1 != b.check1) d |= SEG_DIFFERS_FX; if (check2 != b.check2) d |= SEG_DIFFERS_FX; if (check3 != b.check3) d |= SEG_DIFFERS_FX; if (startY != b.startY) d |= SEG_DIFFERS_BOUNDS; if (stopY != b.stopY) d |= SEG_DIFFERS_BOUNDS; //bit pattern: (msb first) set:2, sound:1, mapping:3, transposed, mirrorY, reverseY, [transitional, reset,] paused, mirrored, on, reverse, [selected] if ((options & 0b1111111110011110U) != (b.options & 0b1111111110011110U)) d |= SEG_DIFFERS_OPT; if ((options & 0x0001U) != (b.options & 0x0001U)) d |= SEG_DIFFERS_SEL; for (uint8_t i = 0; i < NUM_COLORS; i++) if (colors[i] != b.colors[i]) d |= SEG_DIFFERS_COL; return d; } void Segment::refreshLightCapabilities() { uint8_t capabilities = 0; uint16_t segStartIdx = 0xFFFFU; uint16_t segStopIdx = 0; if (!isActive()) { _capabilities = 0; return; } if (start < Segment::maxWidth * Segment::maxHeight) { // we are withing 2D matrix (includes 1D segments) for (int y = startY; y < stopY; y++) for (int x = start; x < stop; x++) { uint16_t index = x + Segment::maxWidth * y; if (index < strip.customMappingSize) index = strip.customMappingTable[index]; // convert logical address to physical if (index < 0xFFFFU) { if (segStartIdx > index) segStartIdx = index; if (segStopIdx < index) segStopIdx = index; } if (segStartIdx == segStopIdx) segStopIdx++; // we only have 1 pixel segment } } else { // we are on the strip located after the matrix segStartIdx = start; segStopIdx = stop; } for (uint8_t b = 0; b < busses.getNumBusses(); b++) { Bus *bus = busses.getBus(b); if (bus == nullptr || bus->getLength()==0) break; if (!bus->isOk()) continue; if (bus->getStart() >= segStopIdx) continue; if (bus->getStart() + bus->getLength() <= segStartIdx) continue; //uint8_t type = bus->getType(); if (bus->hasRGB() || (cctFromRgb && bus->hasCCT())) capabilities |= SEG_CAPABILITY_RGB; if (!cctFromRgb && bus->hasCCT()) capabilities |= SEG_CAPABILITY_CCT; if (correctWB && (bus->hasRGB() || bus->hasCCT())) capabilities |= SEG_CAPABILITY_CCT; //white balance correction (CCT slider) if (bus->hasWhite()) { uint8_t aWM = Bus::getGlobalAWMode() == AW_GLOBAL_DISABLED ? bus->getAutoWhiteMode() : Bus::getGlobalAWMode(); bool whiteSlider = (aWM == RGBW_MODE_DUAL || aWM == RGBW_MODE_MANUAL_ONLY); // white slider allowed // if auto white calculation from RGB is active (Accurate/Brighter), force RGB controls even if there are no RGB busses if (!whiteSlider) capabilities |= SEG_CAPABILITY_RGB; // if auto white calculation from RGB is disabled/optional (None/Dual), allow white channel adjustments if ( whiteSlider) capabilities |= SEG_CAPABILITY_W; } } _capabilities = capabilities; } /* * Fills segment with color - WLEDMM using faster sPC if possible */ void __attribute__((hot)) Segment::fill(uint32_t c) { if (!isActive()) return; // not active // WLEDMM use "calc_" functions because fill() is also called from json.cpp without previous seg.startFrame const uint_fast16_t cols = is2D() ? calc_virtualWidth() : calc_virtualLength(); // WLEDMM use fast int types const uint_fast16_t rows = calc_virtualHeight(); // will be 1 for 1D if (is2D()) { // pre-calculate scaled color uint32_t scaled_col = c; bool simpleSegment = (grouping == 1) && (spacing == 0); if (simpleSegment) { uint8_t _bri_t = currentBri(on ? opacity : 0); if (!_bri_t && !transitional) return; if (_bri_t < 255) scaled_col = color_fade(c, _bri_t); } // fill 2D segment for(unsigned y = 0; y < rows; y++) for (unsigned x = 0; x < cols; x++) { if (simpleSegment) setPixelColorXY_fast(x, y, c, scaled_col, cols, rows); else setPixelColorXY_slow(x, y, c); } } else { // fill 1D strip for (unsigned x = 0; x < cols; x++) setPixelColor(int(x), c); } } // Blends the specified color with the existing pixel color. void Segment::blendPixelColor(int n, uint32_t color, uint8_t blend) { if (blend == UINT8_MAX) setPixelColor(n, color); else setPixelColor(n, color_blend(getPixelColor(n), color, blend)); } // Adds the specified color with the existing pixel color perserving color balance. void Segment::addPixelColor(int n, uint32_t color, bool fast) { if (!isActive()) return; // not active uint32_t col = getPixelColor(n); uint8_t r = R(col); uint8_t g = G(col); uint8_t b = B(col); uint8_t w = W(col); if (fast) { r = qadd8(r, R(color)); g = qadd8(g, G(color)); b = qadd8(b, B(color)); w = qadd8(w, W(color)); col = RGBW32(r,g,b,w); } else { col = color_add(col, color); } setPixelColor(n, col); } void Segment::fadePixelColor(uint16_t n, uint8_t fade) { if (!isActive()) return; // not active CRGB pix = CRGB(getPixelColor(n)).nscale8_video(fade); setPixelColor(n, pix); } /* * fade out function, higher rate = quicker fade */ void __attribute__((hot)) Segment::fade_out(uint8_t rate) { if (!isActive()) return; // not active const uint_fast16_t cols = is2D() ? virtualWidth() : virtualLength(); // WLEDMM use fast int types const uint_fast16_t rows = virtualHeight(); // will be 1 for 1D uint_fast8_t fadeRate = (255-rate) >> 1; float mappedRate_r = 1.0f / (float(fadeRate) +1.1f); // WLEDMM use reciprocal 1/mappedRate -> faster on non-FPU chips uint32_t color2 = colors[1]; // SEGCOLOR(1); // target color // WLEDMM minor optimization int w2 = W(color2); int r2 = R(color2); int g2 = G(color2); int b2 = B(color2); for (unsigned y = 0; y < rows; y++) for (unsigned x = 0; x < cols; x++) { uint32_t color = is2D() ? getPixelColorXY(int(x), int(y)) : getPixelColor(int(x)); if (color == color2) continue; // WLEDMM speedup - pixel color = target color, so nothing to do int w1 = W(color); int r1 = R(color); int g1 = G(color); int b1 = B(color); int wdelta = mappedRate_r * (w2 - w1); // WLEDMM use reciprocal - its faster int rdelta = mappedRate_r * (r2 - r1); int gdelta = mappedRate_r * (g2 - g1); int bdelta = mappedRate_r * (b2 - b1); // if fade isn't complete, make sure delta is at least 1 (fixes rounding issues) wdelta += (w2 == w1) ? 0 : (w2 > w1) ? 1 : -1; rdelta += (r2 == r1) ? 0 : (r2 > r1) ? 1 : -1; gdelta += (g2 == g1) ? 0 : (g2 > g1) ? 1 : -1; bdelta += (b2 == b1) ? 0 : (b2 > b1) ? 1 : -1; uint32_t colorNew = RGBW32(r1 + rdelta, g1 + gdelta, b1 + bdelta, w1 + wdelta); // WLEDMM if (colorNew != color) { // WLEDMM speedup - do not repaint the same color if (is2D()) setPixelColorXY(int(x), int(y), colorNew); else setPixelColor(int(x), colorNew); } } } // fades all pixels to black using nscale8() void __attribute__((hot)) Segment::fadeToBlackBy(uint8_t fadeBy) { if (!isActive() || fadeBy == 0) return; // optimization - no scaling to apply const uint_fast16_t cols = is2D() ? virtualWidth() : virtualLength(); // WLEDMM use fast int types const uint_fast16_t rows = virtualHeight(); // will be 1 for 1D const uint_fast8_t scaledown = 255-fadeBy; // WLEDMM faster to pre-compute this // WLEDMM minor optimization if(is2D()) { for (unsigned y = 0; y < rows; y++) for (unsigned x = 0; x < cols; x++) { uint32_t cc = getPixelColorXY(int(x),int(y)); // WLEDMM avoid RGBW32 -> CRGB -> RGBW32 conversion uint32_t cc2 = color_fade(cc, scaledown); // fade #ifdef WLEDMM_FASTPATH if (cc2 != cc) // WLEDMM only re-paint if faded color is different - normally disabled - causes problem with text overlay #endif setPixelColorXY(int(x), int(y), cc2); } } else { for (uint_fast16_t x = 0; x < cols; x++) { setPixelColor((uint16_t)x, CRGB(getPixelColor((uint16_t)x)).nscale8(scaledown)); } } } /* * blurs segment content, source: FastLED colorutils.cpp */ void __attribute__((hot)) Segment::blur(uint8_t blur_amount, bool smear) { if (!isActive() || blur_amount == 0) return; // optimization: 0 means "don't blur" #ifndef WLED_DISABLE_2D if (is2D()) { // compatibility with 2D const uint_fast32_t cols = virtualWidth(); const uint_fast32_t rows = virtualHeight(); for (uint_fast32_t i = 0; i < rows; i++) blurRow(i, blur_amount, smear); // blur all rows for (uint_fast32_t k = 0; k < cols; k++) blurCol(k, blur_amount, smear); // blur all columns return; } #endif uint8_t keep = smear ? 255 : 255 - blur_amount; uint8_t seep = blur_amount >> 1; unsigned virtlength = virtualLength(); uint32_t carryover = BLACK; uint32_t lastnew; uint32_t last; uint32_t curnew = 0; for (unsigned i = 0; i < virtlength; i++) { uint32_t cur = getPixelColor(i); uint32_t part = color_fade(cur, seep); curnew = color_fade(cur, keep); if (i > 0) { if (carryover) curnew = color_add(curnew, carryover, !smear); // WLEDMM uint32_t prev = color_add(lastnew, part, !smear); // WLEDMM if (last != prev) // optimization: only set pixel if color has changed setPixelColor(int(i - 1), prev); } else // first pixel setPixelColor(int(i), curnew); lastnew = curnew; last = cur; // save original value for comparison on next iteration carryover = part; } setPixelColor(int(virtlength - 1), curnew); } /* * Put a value 0 to 255 in to get a color value. * The colours are a transition r -> g -> b -> back to r * Inspired by the Adafruit examples. */ // WLEDMM: Segment::color_wheel(uint8_t pos) moved to FX.h for better optimization by the compiler /* * Returns a new, random wheel index with a minimum distance of 42 from pos. */ uint8_t Segment::get_random_wheel_index(uint8_t pos) const { // WLEDMM use fast int types, use native min/max uint_fast8_t r = 0, x = 0, y = 0, d = 0; while(d < 42) { r = random8(); x = abs(int(pos - r)); y = 255 - x; d = min(x, y); } return r; } /* * Gets a single color from the currently selected palette. * @param i Palette Index (if mapping is true, the full palette will be _virtualSegmentLength long, if false, 255). Will wrap around automatically. * @param mapping if true, LED position in segment is considered for color * @param wrap FastLED palettes will usually wrap back to the start smoothly. Set false to get a hard edge * @param mcol If the default palette 0 is selected, return the standard color 0, 1 or 2 instead. If >2, Party palette is used instead * @param pbri Value to scale the brightness of the returned color by. Default is 255. (no scaling) * @returns Single color from palette */ // WLEDMM: Segment::color_from_palette() moved to FX.h for better optimization by the compiler //WLEDMM netmindz ar palette uint8_t * Segment::getAudioPalette(int pal) const { // https://forum.makerforums.info/t/hi-is-it-possible-to-define-a-gradient-palette-at-runtime-the-define-gradient-palette-uses-the/63339 um_data_t *um_data; if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { um_data = simulateSound(SEGMENT.soundSim); } uint8_t *fftResult = (uint8_t*)um_data->u_data[2]; static uint8_t xyz[16]; // Needs to be 4 times however many colors are being used. // 3 colors = 12, 4 colors = 16, etc. xyz[0] = 0; // anchor of first color - must be zero xyz[1] = 0; xyz[2] = 0; xyz[3] = 0; CRGB rgb = getCRGBForBand(1, fftResult, pal); xyz[4] = 1; // anchor of first color xyz[5] = rgb.r; xyz[6] = rgb.g; xyz[7] = rgb.b; rgb = getCRGBForBand(128, fftResult, pal); xyz[8] = 128; xyz[9] = rgb.r; xyz[10] = rgb.g; xyz[11] = rgb.b; rgb = getCRGBForBand(255, fftResult, pal); xyz[12] = 255; // anchor of last color - must be 255 xyz[13] = rgb.r; xyz[14] = rgb.g; xyz[15] = rgb.b; return xyz; } /////////////////////////////////////////////////////////////////////////////// // WS2812FX class implementation /////////////////////////////////////////////////////////////////////////////// //WLEDMM from util.cpp // enumerate all ledmapX.json files on FS and extract ledmap names if existing void WS2812FX::enumerateLedmaps() { ledmapMaxSize = 0; ledMaps = 1; for (int i=1; i<10; i++) { char fileName[33] = {'\0'}; // WLEDMM ensure termination snprintf_P(fileName, sizeof(fileName), PSTR("/ledmap%d.json"), i); bool isFile = WLED_FS.exists(fileName); #ifndef ESP8266 if (ledmapNames[i-1]) { //clear old name delete[] ledmapNames[i-1]; ledmapNames[i-1] = nullptr; } #endif if (isFile) { ledMaps |= 1 << i; #ifndef ESP8266 if (requestJSONBufferLock(21)) { //WLEDMM: upstream code loops over all ledmap files, read them all, every byte (!!!!) and only get the name of the file!!! File f; f = WLED_FS.open(fileName, "r"); if (f) { f.find("\"n\":"); char name[WLED_MAX_SEGNAME_LEN+2] = { '\0' }; // ensure string termination f.readBytesUntil('\n', name, sizeof(name)-1); size_t len = strlen(name); if (len > 0 && len < (sizeof(name)-1)) { (void) cleanUpName(name); len = strlen(name); ledmapNames[i-1] = new(std::nothrow) char[len+1]; // +1 to include terminating \0 if (ledmapNames[i-1]) strlcpy(ledmapNames[i-1], name, len+1); } if (!ledmapNames[i-1]) { char tmp[33]; snprintf_P(tmp, 32, PSTR("ledmap%d.json"), i); size_t tmplen = strlen(tmp); ledmapNames[i-1] = new(std::nothrow) char[tmplen+1]; if (ledmapNames[i-1]) strlcpy(ledmapNames[i-1], tmp, tmplen+1); } USER_PRINTF("enumerateLedmaps %s \"%s\"", fileName, name); if (isMatrix) { //WLEDMM calc ledmapMaxSize (TroyHacks) char dim[34] = { '\0' }; f.find("\"width\":"); f.readBytesUntil('\n', dim, sizeof(dim)-1); //hack: use fileName as we have this allocated already uint16_t maxWidth = atoi(cleanUpName(dim)); f.find("\"height\":"); memset(dim, 0, sizeof(dim)); // clear buffer before reading f.readBytesUntil('\n', dim, sizeof(dim)-1); uint16_t maxHeight = atoi(cleanUpName(dim)); ledmapMaxSize = MAX(ledmapMaxSize, maxWidth * maxHeight); if (maxWidth*maxHeight>0) { USER_PRINTF(" (%dx%d -> %d)\n", maxWidth, maxHeight, ledmapMaxSize); } else { USER_PRINTLN(); } } else USER_PRINTLN(); } f.close(); USER_FLUSH(); releaseJSONBufferLock(); } #endif } } //WLEDMM add segment names to be used as ledmap names uint8_t segment_index = 0; for (segment &seg : _segments) { if (seg.name != nullptr && strlen(seg.name) > 0) { char fileName[WLED_MAX_SEGNAME_LEN+12] = { '\0' }; // segment name is 32 chars max, so we need 43 chars in worst case snprintf_P(fileName, sizeof(fileName)-1, PSTR("/lm%s.json"), seg.name); bool isFile = WLED_FS.exists(fileName); if (isFile) ledMaps |= 1 << (10+segment_index); } segment_index++; } } //do not call this method from system context (network callback) void WS2812FX::finalizeInit(void) { //reset segment runtimes suspendStripService = true; // WLEDMM avoid running effects on an incomplete strip for (segment &seg : _segments) { seg.markForReset(); seg.resetIfRequired(); } // for the lack of better place enumerate ledmaps here // if we do it in json.cpp (serializeInfo()) we are getting flashes on LEDs // unfortunately this means we do not get updates after uploads enumerateLedmaps(); _hasWhiteChannel = _isOffRefreshRequired = false; //if busses failed to load, add default (fresh install, FS issue, ...) if (busses.getNumBusses() == 0) { DEBUG_PRINTLN(F("No busses, init default")); const uint8_t defDataPins[] = {DATA_PINS}; const uint16_t defCounts[] = {PIXEL_COUNTS}; const uint8_t defNumBusses = ((sizeof defDataPins) / (sizeof defDataPins[0])); const uint8_t defNumCounts = ((sizeof defCounts) / (sizeof defCounts[0])); uint16_t prevLen = 0; for (uint8_t i = 0; i < defNumBusses && i < WLED_MAX_BUSSES+WLED_MIN_VIRTUAL_BUSSES; i++) { uint8_t defPin[] = {defDataPins[i]}; uint16_t start = prevLen; 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; } } _length = 0; for (uint8_t i=0; igetStart() + bus->getLength() > MAX_LEDS) { USER_PRINT("\nError: too many LEDs, max number is "); USER_PRINTLN(MAX_LEDS); break; } //RGBW mode is enabled if at least one of the strips is RGBW _hasWhiteChannel |= bus->hasWhite(); //refresh is required to remain off if at least one of the strips requires the refresh. _isOffRefreshRequired |= bus->isOffRefreshRequired(); uint16_t busEnd = bus->getStart() + bus->getLength(); if (busEnd > _length) _length = busEnd; #ifdef ESP8266 if ((!IS_DIGITAL(bus->getType()) || IS_2PIN(bus->getType()))) continue; uint8_t pins[5]; if (!bus->getPins(pins)) continue; BusDigital* bd = static_cast(bus); if (pins[0] == 3) bd->reinit(); #endif } if (isMatrix) setUpMatrix(); else { Segment::maxWidth = _length; Segment::maxHeight = 1; } //initialize leds array. TBD: realloc if nr of leds change if (Segment::_globalLeds) { // DONG - Valkyrie is about to die [Gauntlet, 1985] // this is a critical section that will be removed with PR #278 which removes _globalLeds // problem: suspendStripService provides interlocking, but there’s a window before service() observes it, // and ESP32 is dual-core. A critical section closes that window so the pointer swap is atomic across cores. CRGB* oldGLeds = Segment::_globalLeds; portENTER_CRITICAL(&ledsrgb_mux); Segment::_globalLeds = nullptr; portEXIT_CRITICAL(&ledsrgb_mux); free(oldGLeds); purgeSegments(true); // WLEDMM moved here, because it seems to improve stability. } if (useLedsArray && getLengthTotal()>0) { // WLEDMM avoid malloc(0) size_t arrSize = sizeof(CRGB) * getLengthTotal(); // softhack007 disabled; putting leds into psram leads to horrible slowdown on WROVER boards (see setUpLeds()) //#if defined(ARDUINO_ARCH_ESP32) && defined(BOARD_HAS_PSRAM) && defined(WLED_USE_PSRAM) //if (psramFound()) // Segment::_globalLeds = (CRGB*) ps_malloc(arrSize); //else //#endif if (arrSize > 0) Segment::_globalLeds = (CRGB*) malloc(arrSize); // WLEDMM avoid malloc(0) if ((Segment::_globalLeds != nullptr) && (arrSize > 0)) memset(Segment::_globalLeds, 0, arrSize); // WLEDMM avoid dereferencing nullptr if ((Segment::_globalLeds == nullptr) && (arrSize > 0)) errorFlag = ERR_NORAM_PX; // WLEDMM raise errorflag } //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 _isServicing = false; // WLEDMM suspendStripService = false; // WLEDMM ready, run ! } // WLEDMM wait until strip is idle (=not servicing). // on 8266 this function does nothing, because we can only do "busy waiting" on ESP32 //#define MAX_IDLE_WAIT_MS 50 // seems to work in most cases #define MAX_IDLE_WAIT_MS 120 // better safe than sorry - similar to the timeout used by upstream WLED void WS2812FX::waitUntilIdle(unsigned timeout) { #if defined(ARDUINO_ARCH_ESP32) && defined(WLEDMM_PROTECT_SERVICE) if (timeout < MAX_IDLE_WAIT_MS) timeout = MAX_IDLE_WAIT_MS; if (isServicing()) { unsigned long waitStarted = millis(); do { delay(2); // Suspending for 1 tick (or more) gives other tasks a chance to run. //yield(); // seems to be a no-op on esp32 } while (isServicing() && (millis() - waitStarted < timeout)); DEBUG_PRINTF("strip.waitUntilIdle(): strip %sidle after %d ms. (task %s with prio=%d)\n", isServicing()?"not ":"", int(millis() - waitStarted), pcTaskGetTaskName(NULL), uxTaskPriorityGet(NULL)); if (isServicing()) USER_PRINTF("strip.waitUntilIdle(): strip NOT idle after %d ms - overriding access. (task %s with prio=%d)\n", int(millis() - waitStarted), pcTaskGetTaskName(NULL), uxTaskPriorityGet(NULL)); } return; #else return; #endif } void WS2812FX::service() { unsigned long nowUp = millis(); // Be aware, millis() rolls over every 49 days // WLEDMM avoid losing precision if (OTAisRunning) return; // WLEDMM avoid flickering during OTA //#ifdef ARDUINO_ARCH_ESP32 //if ((_isServicing == true) && (strncmp(pcTaskGetTaskName(NULL), "loopTask", 8) != 0)) return; // WLEDMM experimental: not in looptask context - avoid self-blocking (DDP over webSockets) //#endif now = nowUp + timebase; unsigned long elapsed = nowUp - _lastServiceShow; #if defined(ARDUINO_ARCH_ESP32) && defined(WLEDMM_FASTPATH) // WLEDMM go faster on ESP32 //if (_suspend) return; if (elapsed < 2) return; // keep wifi alive if ( !_triggered && (_targetFps != FPS_UNLIMITED) && (_targetFps != FPS_UNLIMITED_AC)) { if ((elapsed+1) < _frametime) return; // code from upstream - stricter on FPS } #else // legacy if (elapsed < _frametime) return; #endif bool doShow = false; unsigned speedLimit = (_targetFps != FPS_UNLIMITED) && (_targetFps != FPS_UNLIMITED_AC) ? (0.85f * FRAMETIME) : 1; // WLEDMM minimum for effect frametime _isServicing = true; _segment_index = 0; if (esp32SemTake(segmentMux, 250) == pdTRUE) { // WLEDMM prevent changes to segments while servicing for (segment &seg : _segments) { #ifdef WLEDMM_FASTPATH _currentSeg = &seg; #endif // reset the segment runtime data if needed seg.resetIfRequired(); if (!seg.isActive()) continue; if (!seg.on && !seg.transitional) continue; // WLEDMM skip disabled segments, unless a crossfade is ongoing // last condition ensures all solid segments are updated at the same time if(nowUp >= seg.next_time || _triggered || (doShow && seg.mode == FX_MODE_STATIC)) // WLEDMM ">=" instead of ">" { if (seg.grouping == 0) seg.grouping = 1; //sanity check if ((!seg.freeze) || _triggered) doShow = true; // WLEDMM "triggered" overrules "freeze" uint16_t frameDelay = FRAMETIME; // WLEDMM avoid name clash with "delay" function if (!seg.freeze) { //only run effect function if not frozen _virtualSegmentLength = seg.calc_virtualLength(); _colors_t[0] = seg.currentColor(0, seg.colors[0]); _colors_t[1] = seg.currentColor(1, seg.colors[1]); _colors_t[2] = seg.currentColor(2, seg.colors[2]); seg.setCurrentPalette(); // load actual palette if (!cctFromRgb || correctWB) busses.setSegmentCCT(seg.currentBri(seg.cct, true), correctWB); for (uint8_t c = 0; c < NUM_COLORS; c++) _colors_t[c] = gamma32(_colors_t[c]); seg.startFrame(); // WLEDMM if (!_triggered && (seg.currentBri(seg.opacity) == 0) && (seg.lastBri == 0)) continue; // WLEDMM skip totally black segments // effect blending (execute previous effect) // actual code may be a bit more involved as effects have runtime data including allocated memory //if (seg.transitional && seg._modeP) (*_mode[seg._modeP])(progress()); // WLEDMM protect against parallel drawing if (esp32SemTake(busDrawMux, 200) != pdTRUE) { delay(1); continue;} // WLEDMM first acquire draw mutex frameDelay = (*_mode[seg.currentMode(seg.mode)])(); if (frameDelay < speedLimit) frameDelay = FRAMETIME; // WLEDMM limit effects that want to go faster than target FPS if (seg.mode != FX_MODE_HALLOWEEN_EYES) seg.call++; if (seg.transitional && frameDelay > max(int(FRAMETIME), int(FRAMETIME_FIXED))) frameDelay = max(int(FRAMETIME), int(FRAMETIME_FIXED)); // force faster updates during transition // WLEDMM only if effect requested very slow updates seg.lastBri = seg.currentBri(seg.on ? seg.opacity:0); // WLEDMM remember for next time seg.handleTransition(); esp32SemGive(busDrawMux); // WLEDMM unlock mutex } seg.next_time = nowUp + frameDelay; } _segment_index++; } esp32SemGive(segmentMux); } // end of critical section #ifdef WLEDMM_FASTPATH _currentSeg = & strip.getMainSegment(); // WLEDMM safe default #endif _virtualSegmentLength = 0; busses.setSegmentCCT(-1); if(doShow || _triggered) { yield(); show(); _lastServiceShow = nowUp; // WLEDMM use correct timestamp } _triggered = false; _isServicing = false; } // WLEDMM: WS2812FX::setPixelColor() moved FX.h for speed (inlining) // WLEDMM: WS2812FX::getPixelColor() moved FX.h for speed (inlining) // WLEDMM: WS2812FX::getPixelColorRestored() moved FX.h for speed (inlining) //DISCLAIMER //The following function attemps to calculate the current LED power usage, //and will limit the brightness to stay below a set amperage threshold. //It is NOT a measurement and NOT guaranteed to stay within the ablMilliampsMax margin. //Stay safe with high amperage and have a reasonable safety margin! //I am NOT to be held liable for burned down garages! //fine tune power estimation constants for your setup #define MA_FOR_ESP 100 //how much mA does the ESP use (Wemos D1 about 80mA, ESP32 about 120mA) //you can set it to 0 if the ESP is powered by USB and the LEDs by external void WS2812FX::estimateCurrentAndLimitBri() { //power limit calculation //each LED can draw up 195075 "power units" (approx. 53mA) //one PU is the power it takes to have 1 channel 1 step brighter per brightness step //so A=2,R=255,G=0,B=0 would use 510 PU per LED (1mA is about 3700 PU) bool useWackyWS2815PowerModel = false; byte actualMilliampsPerLed = milliampsPerLed; if(milliampsPerLed == 255) { useWackyWS2815PowerModel = true; actualMilliampsPerLed = 12; // from testing an actual strip } if (ablMilliampsMax < 150 || actualMilliampsPerLed == 0) { //0 mA per LED and too low numbers turn off calculation currentMilliamps = 0; busses.setBrightness(_brightness); return; } uint16_t pLen = getLengthPhysical(); uint32_t puPerMilliamp = 195075 / actualMilliampsPerLed; uint32_t powerBudget = (ablMilliampsMax - MA_FOR_ESP) * puPerMilliamp; //100mA for ESP power if (powerBudget > puPerMilliamp * pLen) { //each LED uses about 1mA in standby, exclude that from power budget powerBudget -= puPerMilliamp * pLen; } else { powerBudget = 0; } uint32_t powerSum = 0; for (uint_fast8_t bNum = 0; bNum < busses.getNumBusses(); bNum++) { Bus *bus = busses.getBus(bNum); if (!bus || !bus->isOk()) continue; // WLEDMM skip busses that are not initialized yet auto btype = bus->getType(); if (EXCLUDE_FROM_ABL(btype)) continue; // WLEDMM exclude non-ABL and network busses uint16_t len = bus->getLength(); uint32_t busPowerSum = 0; for (uint_fast16_t i = 0; i < len; i++) { //sum up the usage of each LED uint32_t c = bus->getPixelColor(i); byte r = R(c), g = G(c), b = B(c), w = W(c); if(useWackyWS2815PowerModel) { //ignore white component on WS2815 power calculation busPowerSum += (max(max(r,g),b)) * 3; // WLEDMM use native min/max } else { busPowerSum += (r + g + b + w); } } if (bus->hasWhite()) { //RGBW led total output with white LEDs enabled is still 50mA, so each channel uses less busPowerSum *= 3; busPowerSum = busPowerSum >> 2; //same as /= 4 } powerSum += busPowerSum; } uint32_t powerSum0 = powerSum; //powerSum *= _brightness; // for NPBrightnessBus powerSum *= 255; // no need to scale down powerSum - NPB-LG getPixelColor returns colors scaled down by brightness if (powerSum > powerBudget) //scale brightness down to stay in current limit { float scale = (float)powerBudget / (float)powerSum; uint16_t scaleI = scale * 255; uint8_t scaleB = (scaleI > 255) ? 255 : scaleI; uint8_t newBri = scale8(_brightness, scaleB); // to keep brightness uniform, sets virtual busses too - softhack007: apply reductions immediately if (scaleB < 255) busses.setBrightness(scaleB, true); // NPB-LG has already applied brightness, so its sufficient to post-apply scaling ==> use scaleB instead of newBri busses.setBrightness(newBri, false); // set new brightness for next frame //currentMilliamps = (powerSum0 * newBri) / puPerMilliamp; // for NPBrightnessBus currentMilliamps = (powerSum0 * scaleB) / puPerMilliamp; // for NPBus-LG } else { currentMilliamps = powerSum / puPerMilliamp; busses.setBrightness(_brightness, false); // set new brightness for next frame } currentMilliamps += MA_FOR_ESP; //add power of ESP back to estimate currentMilliamps += pLen; //add standby power back to estimate } void WS2812FX::show(void) { if (OTAisRunning) return; // WLEDMM avoid flickering during OTA // avoid race condition, capture _callback value show_callback callback = _callback; if (callback) callback(); estimateCurrentAndLimitBri(); unsigned long showNow = millis(); // include time needed for busses.show() #ifdef ARDUINO_ARCH_ESP32 // WLEDMM more accurate FPS measurement for ESP32 uint64_t now500 = esp_timer_get_time() / 2; // native timer; micros /2 -> millis * 500 #endif // 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 // WLEDMM protect against parallel access if (esp32SemTake(busDrawMux, 200) != pdTRUE) { delay(1); return;} // WLEDMM first acquire drawing permission (mutex), wait max 200ms busses.show(); esp32SemGive(busDrawMux); // WLEDMM return permissions unsigned long diff = showNow - _lastShow; uint16_t fpsCurr = 200; if (diff > 0) fpsCurr = 1000 / diff; _cumulativeFps = (3 * _cumulativeFps + fpsCurr +2) >> 2; // "+2" for proper rounding (2/4 = 0.5) _lastShow = showNow; _lastServiceShow = showNow; #ifdef ARDUINO_ARCH_ESP32 // WLEDMM more accurate FPS measurement for ESP32 int64_t diff500 = now500 - _lastShow500; if ((diff500 > 1) && (diff500 < 800000)) { // exclude stupid values (timer rollover, major hickups) float fpcCurr500 = 500000.0f / float(diff500); if (fpcCurr500 > 2) _cumulativeFps500 = (3 * _cumulativeFps500 + (500.0 * fpcCurr500)) / 4; // average for some smoothing } _lastShow500 = now500; #endif } /** * Returns a true value if any of the strips are still being updated. * On some hardware (ESP32), strip updates are done asynchronously. */ bool WS2812FX::isUpdating() const { return !busses.canAllShow(); } /** * Returns the refresh rate of the LED strip. Useful for finding out whether a given setup is fast enough. * Only updates on show() or is set to 0 fps if last show is more than 2 secs ago, so accuracy varies */ uint16_t WS2812FX::getFps() const { if (millis() - _lastShow > 2000) return 0; #ifdef ARDUINO_ARCH_ESP32 return ((_cumulativeFps500 + 250) / 500); // +250 for proper rounding #else return _cumulativeFps +1; #endif } void WS2812FX::setTargetFps(uint8_t fps) { if (fps <= 251) _targetFps = fps; // WLEDMM allow higher framerates //if (fps > 0) _frametime = ((2000 / _targetFps) +1) /2; // with rounding if (fps > 0) _frametime = 1000 / _targetFps; else _frametime = 2; // AC WLED compatibility if (fps >= FPS_UNLIMITED) _frametime = 2; // WLEDMM unlimited mode } void WS2812FX::setMode(uint8_t segid, uint8_t m) { if (segid >= _segments.size()) return; if (m >= getModeCount()) m = getModeCount() - 1; if (_segments[segid].mode != m) { _segments[segid].startTransition(_transitionDur); // set effect transitions //_segments[segid].markForReset(); _segments[segid].mode = m; } } //applies to all active and selected segments void WS2812FX::setColor(uint8_t slot, uint32_t c) { if (slot >= NUM_COLORS) return; for (segment &seg : _segments) { if (seg.isActive() && seg.isSelected()) { seg.setColor(slot, c); } } } void WS2812FX::setCCT(uint16_t k) { for (segment &seg : _segments) { if (seg.isActive() && seg.isSelected()) { seg.setCCT(k); } } } void WS2812FX::setBrightness(uint8_t b, bool direct) { if (gammaCorrectBri) b = gamma8(b); if (_brightness == b) return; _brightness = b; if (_brightness == 0) { //unfreeze all segments on power off for (segment &seg : _segments) { seg.freeze = false; } } if (direct) { // would be dangerous if applied immediately (could exceed ABL), but will not output until the next show() busses.setBrightness(b); } else { unsigned long t = millis(); if (_segments[0].next_time > t + 22 && t - _lastShow > MIN_SHOW_DELAY) show(); //apply brightness change immediately if no refresh soon } } uint8_t WS2812FX::getActiveSegsLightCapabilities(bool selectedOnly) { uint8_t totalLC = 0; for (segment &seg : _segments) { if (seg.isActive() && (!selectedOnly || seg.isSelected())) totalLC |= seg.getLightCapabilities(); } return totalLC; } uint8_t WS2812FX::getFirstSelectedSegId(void) { size_t i = 0; for (segment &seg : _segments) { if (seg.isActive() && seg.isSelected()) return i; i++; } // if none selected, use the main segment return getMainSegmentId(); } void WS2812FX::setMainSegmentId(uint8_t n) { _mainSegment = 0; if (n < _segments.size()) { _mainSegment = n; } return; } uint8_t WS2812FX::getLastActiveSegmentId(void) const { for (size_t i = _segments.size() -1; i > 0; i--) { if (_segments[i].isActive()) return i; } return 0; } uint8_t WS2812FX::getActiveSegmentsNum(void) const { uint8_t c = 0; for (size_t i = 0; i < _segments.size(); i++) { if (_segments[i].isActive()) c++; } return c; } uint16_t WS2812FX::getLengthTotal(void) const { // WLEDMM fast int types uint_fast16_t len = Segment::maxWidth * Segment::maxHeight; // will be _length for 1D (see finalizeInit()) but should cover whole matrix for 2D if (isMatrix && _length > len) len = _length; // for 2D with trailing strip return len; } uint16_t WS2812FX::getLengthPhysical(void) const { // WLEDMM fast int types uint_fast16_t len = 0; for (unsigned b = 0; b < busses.getNumBusses(); b++) { // WLEDMM use native (fast) types Bus *bus = busses.getBus(b); auto btype = bus->getType(); if (EXCLUDE_FROM_ABL(btype)) continue; //exclude HUB75, and non-physical network busses len += bus->getLength(); } return len; } //WLEDMM - getLengthPhysical plus plysical busses not supporting ABL (i.e. HUB75) uint16_t WS2812FX::getLengthPhysical2(void) const { uint_fast16_t len = 0; for (unsigned b = 0; b < busses.getNumBusses(); b++) { Bus *bus = busses.getBus(b); auto btype = bus->getType(); if (IS_VIRTUAL(btype)) continue; len += bus->getLength(); } return len; } //used for JSON API info.leds.rgbw. Little practical use, deprecate with info.leds.rgbw. //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) const { for (unsigned b = 0; b < busses.getNumBusses(); b++) { // WLEDMM use native (fast) types Bus *bus = busses.getBus(b); if (bus == nullptr || bus->getLength()==0) break; if (bus->hasRGB() && bus->hasWhite()) return true; } return false; } bool WS2812FX::hasCCTBus(void) const { if (cctFromRgb && !correctWB) return false; for (unsigned b = 0; b < busses.getNumBusses(); b++) { // WLEDMM use native (fast) types Bus *bus = busses.getBus(b); if (bus == nullptr || bus->getLength()==0) break; switch (bus->getType()) { case TYPE_ANALOG_5CH: case TYPE_ANALOG_2CH: return true; } } return false; } void WS2812FX::purgeSegments(bool force) { // remove all inactive segments (from the back) int deleted = 0; if (_segments.size() <= 1) return; // WLEDMM protect against parallel access while drawing if (esp32SemTake(segmentMux, 300) != pdTRUE) return; for (size_t i = _segments.size()-1; i > 0; i--) { if (_segments[i].stop == 0 || force) { deleted++; _segments.erase(_segments.begin() + i); } } if (deleted) { _segments.shrink_to_fit(); /*if (_mainSegment >= _segments.size())*/ setMainSegmentId(0); } esp32SemGive(segmentMux); } Segment& WS2812FX::getSegment(uint8_t id) { uint8_t mainSegID = getMainSegmentId(); if (mainSegID >= _segments.size()) mainSegID=0; // WLEDMM fallback in case that getMainSegmentId() is invalid return _segments[id >= _segments.size() ? mainSegID : id]; // vectors } void WS2812FX::setSegment(uint8_t n, uint16_t i1, uint16_t i2, uint8_t grouping, uint8_t spacing, uint16_t offset, uint16_t startY, uint16_t stopY) { if (n >= _segments.size()) return; _segments[n].setUp(i1, i2, grouping, spacing, offset, startY, stopY); } void WS2812FX::restartRuntime(bool doReset) { for (segment &seg : _segments) { if (doReset) { // WLEDMM we prefer not to perform a complete restart of all effects seg.markForReset(); // seg.resetIfRequired(); // WLEDMM calling this function from webserver context will cause troubles } else { seg.next_time = 0; seg.step = 0; seg.markForBlank(); } } } void WS2812FX::resetSegments(bool boundsOnly) { //WLEDMM add boundsonly DEBUG_PRINTF("resetSegments %d %dx%d\n", boundsOnly, Segment::maxWidth, Segment::maxHeight); if (!boundsOnly) { // WLEDMM protect against parallel access while drawing if (esp32SemTake(segmentMux, 2100) != pdTRUE) return; // wait long, but don't wait forever _segments.clear(); // destructs all Segment as part of clearing #ifndef WLED_DISABLE_2D segment seg = isMatrix ? Segment(0, Segment::maxWidth, 0, Segment::maxHeight) : Segment(0, _length); #else segment seg = Segment(0, _length); #endif _segments.push_back(seg); _mainSegment = 0; esp32SemGive(segmentMux); } else { //WLEDMM boundsonly // WLEDMM protect against parallel access while drawing if (esp32SemTake(segmentMux, 2100) != pdTRUE) return; // wait long, but don't wait forever for (segment &seg : _segments) { #ifndef WLED_DISABLE_2D seg.start = 0; seg.stop = Segment::maxWidth; seg.startY = 0; seg.stopY = Segment::maxHeight; #else seg.start = 0; seg.stop = _length; #endif seg.allocLeds(); } esp32SemGive(segmentMux); } } void WS2812FX::makeAutoSegments(bool forceReset) { if (autoSegments) { //make one segment per bus // WLEDMM protect against parallel access while drawing if (esp32SemTake(segmentMux, 2100) != pdTRUE) return; // wait long, but don't wait forever uint16_t segStarts[MAX_NUM_SEGMENTS] = {0}; uint16_t segStops [MAX_NUM_SEGMENTS] = {0}; size_t s = 0; #ifndef WLED_DISABLE_2D // 2D segment is the 1st one using entire matrix if (isMatrix) { segStarts[0] = 0; segStops[0] = Segment::maxWidth*Segment::maxHeight; s++; } #endif for (size_t i = s; i < busses.getNumBusses(); i++) { Bus* b = busses.getBus(i); segStarts[s] = b->getStart(); segStops[s] = segStarts[s] + b->getLength(); #ifndef WLED_DISABLE_2D if (isMatrix && segStops[s] <= Segment::maxWidth*Segment::maxHeight) continue; // ignore buses comprising matrix if (isMatrix && segStarts[s] < Segment::maxWidth*Segment::maxHeight) segStarts[s] = Segment::maxWidth*Segment::maxHeight; #endif //check for overlap with previous segments for (size_t j = 0; j < s; j++) { if (segStops[j] > segStarts[s] && segStarts[j] < segStops[s]) { //segments overlap, merge segStarts[j] = min(segStarts[s],segStarts[j]); segStops [j] = max(segStops [s],segStops [j]); segStops[s] = 0; s--; } } s++; } _segments.clear(); _segments.reserve(s); // prevent reallocations // there is always at least one segment (but we need to differentiate between 1D and 2D) #ifndef WLED_DISABLE_2D if (isMatrix) _segments.push_back(Segment(0, Segment::maxWidth, 0, Segment::maxHeight)); else #endif _segments.push_back(Segment(segStarts[0], segStops[0])); for (size_t i = 1; i < s; i++) { _segments.push_back(Segment(segStarts[i], segStops[i])); } esp32SemGive(segmentMux); } else { if (forceReset || getSegmentsNum() == 0) resetSegments(); //expand the main seg to the entire length, but only if there are no other segments, or reset is forced else if (getActiveSegmentsNum() == 1) { size_t i = getLastActiveSegmentId(); #ifndef WLED_DISABLE_2D _segments[i].start = 0; _segments[i].stop = Segment::maxWidth; _segments[i].startY = 0; _segments[i].stopY = Segment::maxHeight; _segments[i].grouping = 1; _segments[i].spacing = 0; #else _segments[i].start = 0; _segments[i].stop = _length; #endif } } _mainSegment = 0; fixInvalidSegments(); } void WS2812FX::fixInvalidSegments() { // WLEDMM protect against parallel access while drawing if (esp32SemTake(segmentMux, 2100) != pdTRUE) return; // wait long, but don't wait forever //make sure no segment is longer than total (sanity check) for (size_t i = getSegmentsNum()-1; i > 0; i--) { if (isMatrix) { #ifndef WLED_DISABLE_2D if (_segments[i].start >= Segment::maxWidth * Segment::maxHeight) { // 1D segment at the end of matrix if (_segments[i].start >= _length || _segments[i].startY > 0 || _segments[i].stopY > 1) { _segments.erase(_segments.begin()+i); continue; } if (_segments[i].stop > _length) _segments[i].stop = _length; continue; } if (_segments[i].start >= Segment::maxWidth || _segments[i].startY >= Segment::maxHeight) { _segments.erase(_segments.begin()+i); continue; } if (_segments[i].stop > Segment::maxWidth) _segments[i].stop = Segment::maxWidth; if (_segments[i].stopY > Segment::maxHeight) _segments[i].stopY = Segment::maxHeight; #endif } else { if (_segments[i].start >= _length) { _segments.erase(_segments.begin()+i); continue; } if (_segments[i].stop > _length) _segments[i].stop = _length; } } esp32SemGive(segmentMux); // give back the lock now, so purgeSegments can acquire it again // if any segments were deleted free memory purgeSegments(); // this is always called as the last step after finalizeInit(), update covered bus types for (segment &seg : _segments) seg.refreshLightCapabilities(); } //true if all segments align with a bus, or if a segment covers the total length //irrelevant in 2D set-up bool WS2812FX::checkSegmentAlignment() { bool aligned = false; for (segment &seg : _segments) { for (uint8_t b = 0; bgetStart() && seg.stop == bus->getStart() + bus->getLength()) aligned = true; } if (seg.start == 0 && seg.stop == _length) aligned = true; if (!aligned) return false; } return true; } #if 0 // WLEDMM dead code //After this function is called, setPixelColor() will use that segment (offsets, grouping, ... will apply) //Note: If called in an interrupt (e.g. JSON API), original segment must be restored, //otherwise it can lead to a crash on ESP32 because _segment_index is modified while in use by the main thread uint8_t WS2812FX::setPixelSegment(uint8_t n) { uint8_t prevSegId = _segment_index; if (n < _segments.size()) { _segment_index = n; _virtualSegmentLength = _segments[_segment_index].calc_virtualLength(); } return prevSegId; } #endif void WS2812FX::setRange(uint16_t i, uint16_t i2, uint32_t col) { if (i2 >= i) { for (uint_fast16_t x = i; x <= i2; x++) setPixelColor((uint16_t)x, col); // WLEDMM use fast int types } else { for (uint_fast16_t x = i2; x <= i; x++) setPixelColor((uint16_t)x, col); } } void WS2812FX::setTransitionMode(bool t) { for (segment &seg : _segments) if (!seg.transitional) seg.startTransition(t ? _transitionDur : 0); } #ifdef WLED_DEBUG void WS2812FX::printSize() { size_t size = 0; for (const Segment &seg : _segments) size += seg.getSize(); DEBUG_PRINTF("Segments: %d -> %uB\n", _segments.size(), size); DEBUG_PRINTF("Modes: %d*%d=%uB\n", sizeof(mode_ptr), _mode.size(), (_mode.capacity()*sizeof(mode_ptr))); DEBUG_PRINTF("Data: %d*%d=%uB\n", sizeof(const char *), _modeData.size(), (_modeData.capacity()*sizeof(const char *))); DEBUG_PRINTF("Map: %d*%d=%uB\n", sizeof(uint16_t), (int)customMappingSize, customMappingSize*sizeof(uint16_t)); size = getLengthTotal(); if (useLedsArray) DEBUG_PRINTF("Buffer: %d*%u=%uB\n", sizeof(CRGB), size, size*sizeof(CRGB)); } #endif void WS2812FX::loadCustomPalettes() { byte tcp[72]; //support gradient palettes with up to 18 entries CRGBPalette16 targetPalette; customPalettes.clear(); // start fresh for (int index = 0; index<10; index++) { char fileName[32]; sprintf_P(fileName, PSTR("/palette%d.json"), index); StaticJsonDocument<1536> pDoc; // barely enough to fit 72 numbers if (WLED_FS.exists(fileName)) { DEBUG_PRINT(F("Reading palette from ")); DEBUG_PRINTLN(fileName); if (readObjectFromFile(fileName, nullptr, &pDoc)) { JsonArray pal = pDoc[F("palette")]; if (!pal.isNull() && pal.size()>3) { // not an empty palette (at least 2 entries) if (pal[0].is() && pal[1].is()) { // we have an array of index & hex strings size_t palSize = min(pal.size(), (size_t)36); // WLEDMM use native min/max palSize -= palSize % 2; // make sure size is multiple of 2 for (unsigned i=0, j=0; i()<256; i+=2, j+=4) { uint8_t rgbw[] = {0,0,0,0}; tcp[ j ] = (uint8_t) pal[ i ].as(); // index colorFromHexString(rgbw, pal[i+1].as()); // will catch non-string entires for (unsigned c=0; c<3; c++) tcp[j+1+c] = gamma8(rgbw[c]); // only use RGB component DEBUG_PRINTF("%d(%d) : %d %d %d\n", i, int(tcp[j]), int(tcp[j+1]), int(tcp[j+2]), int(tcp[j+3])); } } else { size_t palSize = min(pal.size(), (size_t)72); // WLEDMM use native min/max palSize -= palSize % 4; // make sure size is multiple of 4 for (size_t i=0; i()<256; i+=4) { tcp[ i ] = (uint8_t) pal[ i ].as(); // index tcp[i+1] = gamma8((uint8_t) pal[i+1].as()); // R tcp[i+2] = gamma8((uint8_t) pal[i+2].as()); // G tcp[i+3] = gamma8((uint8_t) pal[i+3].as()); // B DEBUG_PRINTF("%d(%d) : %d %d %d\n", i, int(tcp[i]), int(tcp[i+1]), int(tcp[i+2]), int(tcp[i+3])); } } customPalettes.push_back(targetPalette.loadDynamicGradientPalette(tcp)); } else { DEBUG_PRINTLN(F("Wrong palette format.")); } } } else { break; } } } //load custom mapping table from JSON file (called from finalizeInit() or deserializeState()) bool WS2812FX::deserializeMap(uint8_t n) { // 2D support creates its own ledmap (on the fly) if a ledmap.json exists it will overwrite built one. char fileName[WLED_MAX_SEGNAME_LEN+10] = {'\0'}; // WLEDMM we need at least 32 + 7 bytes //WLEDMM: als support segment name ledmaps bool isFile = false;; if (n<10) { strcpy_P(fileName, PSTR("/ledmap")); if (n) sprintf(fileName +7, "%d", n); //WLEDMM: trick to not include 0 in ledmap.json strcat(fileName, ".json"); isFile = WLED_FS.exists(fileName); } else { //WLEDMM add segment name as ledmap.name uint8_t segment_index = 0; for (segment &seg : _segments) { if (n == 10 + segment_index && !isFile && seg.name != nullptr) { snprintf_P(fileName, sizeof(fileName) -1, PSTR("/%s.json"), seg.name); fileName[sizeof(fileName) -1] = '\0'; isFile = WLED_FS.exists(fileName); } if (isFile) break; segment_index++; } } if (!isFile) { // erase custom mapping if selecting nonexistent ledmap.json (n==0) //WLEDMM: doubt this is necessary as return false causes setupMatrix to deal with this !!!! if (!isMatrix && !n) { customMappingSize = 0; loadedLedmap = 0; //WLEDMM } return false; } if (!requestJSONBufferLock(7)) return false; // WLEDMM: before changing maps, make sure our strip is _not_ servicing effects in parallel if (strip.isServicing()) { USER_PRINTLN(F("deserializeMap(): strip is still drawing effects, waiting ...")); strip.waitUntilIdle(); } //WLEDMM: change upstream code: do not load complete ledmaps in json as this blows up memory, use file read instead //read the file File f; f = WLED_FS.open(fileName, "r"); if (!f) { releaseJSONBufferLock(); return false; //if file does not exist just exit } USER_PRINT(F("Reading LED map from ")); //WLEDMM use USER_PRINT USER_PRINTLN(fileName); if (isMatrix) { //WLEDMM: read width and height memset(fileName, 0, sizeof(fileName)); // clear old buffer - readBytesUntil() does not terminate strings !!! f.find("\"width\":"); f.readBytesUntil('\n', fileName, sizeof(fileName)-1); //hack: use fileName as we have this allocated already uint16_t maxWidth = atoi(cleanUpName(fileName)); //DEBUG_PRINTF(" (\"width\": %s) ", fileName) memset(fileName, 0, sizeof(fileName)); // clear old buffer f.seek(0); // rewind to start f.find("\"height\":"); f.readBytesUntil('\n', fileName, sizeof(fileName)-1); uint16_t maxHeight = atoi(cleanUpName(fileName)); //DEBUG_PRINTF(" (\"height\": %s) \n", fileName) #ifndef WLEDMM_NO_MAP_RESET //WLEDMM: support ledmap file properties width and height: if found change segment if (maxWidth * maxHeight > 0) { Segment::maxWidth = maxWidth; Segment::maxHeight = maxHeight; resetSegments(true); //WLEDMM not makeAutoSegments() as we only want to change bounds } else setUpMatrix(); //reset segment sizes to panels #endif } DEBUG_PRINTF("deserializeMap %d x %d\n", Segment::maxWidth, Segment::maxHeight); //WLEDMM recreate customMappingTable if more space needed if (Segment::maxWidth * Segment::maxHeight > customMappingTableSize) { size_t size = max(ledmapMaxSize, size_t(Segment::maxWidth * Segment::maxHeight)); // TroyHacks USER_PRINTF("deserializemap customMappingTable alloc %u from %u\n", size, customMappingTableSize); //if (customMappingTable != nullptr) delete[] customMappingTable; //customMappingTable = new(std::nothrow) uint16_t[size]; // don't use new / delete if ((size > 0) && (customMappingTable != nullptr)) { customMappingTable = (uint16_t*) reallocf(customMappingTable, sizeof(uint16_t) * size); // reallocf will free memory if it cannot resize } if ((size > 0) && (customMappingTable == nullptr)) { // second try DEBUG_PRINTLN("deserializeMap: trying to get fresh memory block."); customMappingTable = (uint16_t*) calloc(size, sizeof(uint16_t)); if (customMappingTable == nullptr) { DEBUG_PRINTLN("deserializeMap: alloc failed!"); errorFlag = ERR_LOW_MEM; // WLEDMM raise errorflag } } if ((errorFlag == ERR_LOW_MEM) && (size > 0) && (customMappingTable != nullptr)) errorFlag = ERR_NONE; // reset error flag if (customMappingTable != nullptr) customMappingTableSize = size; } if (customMappingTable != nullptr) { customMappingSize = Segment::maxWidth * Segment::maxHeight; // WLEDMM reset mapping table before loading //memset(customMappingTable, 0xFF, customMappingTableSize * sizeof(uint16_t)); // FFFF = no pixel for (unsigned i=0; i0?',':' ', mapi, i); if (i < customMappingSize) customMappingTable[i++] = (uint16_t) (mapi<0 ? 0xFFFFU : mapi); // WLEDMM do not write past array bounds endOfArray = entry.indexOf("]") >= 0; // if we hit "]", stop reading } while (f.available() && (i < customMappingSize) && !endOfArray); //DEBUG_PRINTLN(""); loadedLedmap = n; f.close(); if ((customMappingTable != nullptr) && (customMappingSize>0)) { USER_PRINTF(PSTR("Ledmap #%d read. Size=%d (%d x %d); %d items found.\n"), loadedLedmap, customMappingSize, Segment::maxWidth, Segment::maxHeight, i); } #ifdef WLED_DEBUG_MAPS for (uint16_t j=0; j