diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 9ef6463140..d921deb62f 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -1178,9 +1178,9 @@ void WS2812FX::finalizeInit() { // create buses/outputs unsigned mem = 0; unsigned maxI2S = 0; - for (const auto &bus : busConfigs) { - unsigned memB = bus.memUsage(Bus::isDigital(bus.type) && !Bus::is2Pin(bus.type) ? digitalCount++ : 0); // does not include DMA/RMT buffer - mem += memB; + for (auto bus : busConfigs) { + bool use_placeholder = false; + unsigned busMemUsage = bus.memUsage(Bus::isDigital(bus.type) && !Bus::is2Pin(bus.type) ? digitalCount++ : 0); // does not include DMA/RMT buffer // estimate maximum I2S memory usage (only relevant for digital non-2pin busses) #if !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(ESP8266) #if defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S3) @@ -1200,13 +1200,14 @@ void WS2812FX::finalizeInit() { if (i2sCommonSize > maxI2S) maxI2S = i2sCommonSize; } #endif - if (mem + maxI2S <= MAX_LED_MEMORY) { - BusManager::add(bus); - DEBUG_PRINTF_P(PSTR("Bus memory: %uB\n"), memB); - } else { - errorFlag = ERR_NORAM_PX; // alert UI - DEBUG_PRINTF_P(PSTR("Out of LED memory! Bus %d (%d) #%u not created."), (int)bus.type, (int)bus.count, digitalCount); - break; + if (mem + busMemUsage + maxI2S > MAX_LED_MEMORY) { + DEBUG_PRINTF_P(PSTR("Bus %d with %d LEDS memory usage exceeds limit\n"), (int)bus.type, bus.count); + errorFlag = ERR_NORAM; // alert UI TODO: make this a distinct error: not enough memory for bus + use_placeholder = true; + } + if (BusManager::add(bus, use_placeholder) != -1) { + mem += BusManager::busses.back()->getBusSize(); + if (Bus::isDigital(bus.type) && !Bus::is2Pin(bus.type) && BusManager::busses.back()->isPlaceholder()) digitalCount--; // remove placeholder from digital count } } DEBUG_PRINTF_P(PSTR("LED buffer size: %uB/%uB\n"), mem + maxI2S, BusManager::memUsage()); @@ -1804,6 +1805,10 @@ Segment& WS2812FX::getSegment(unsigned id) { void WS2812FX::resetSegments() { _segments.clear(); // destructs all Segment as part of clearing _segments.emplace_back(0, isMatrix ? Segment::maxWidth : _length, 0, isMatrix ? Segment::maxHeight : 1); + if(_segments.size() == 0) { + _segments.emplace_back(); // if out of heap, create a default segment + errorFlag = ERR_NORAM_PX; + } _segments.shrink_to_fit(); // just in case ... _mainSegment = 0; } @@ -1825,7 +1830,7 @@ void WS2812FX::makeAutoSegments(bool forceReset) { for (size_t i = s; i < BusManager::getNumBusses(); i++) { const Bus *bus = BusManager::getBus(i); - if (!bus || !bus->isOk()) break; + if (!bus) break; segStarts[s] = bus->getStart(); segStops[s] = segStarts[s] + bus->getLength(); diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index 4fa5c40a57..311c9b0435 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -1121,6 +1121,26 @@ size_t BusHub75Matrix::getPins(uint8_t* pinArray) const { #endif // *************************************************************************** +BusPlaceholder::BusPlaceholder(const BusConfig &bc) +: Bus(bc.type, bc.start, bc.autoWhite, bc.count, bc.reversed, bc.refreshReq) +, _colorOrder(bc.colorOrder) +, _skipAmount(bc.skipAmount) +, _frequency(bc.frequency) +, _milliAmpsPerLed(bc.milliAmpsPerLed) +, _milliAmpsMax(bc.milliAmpsMax) +, _text(bc.text) +{ + memcpy(_pins, bc.pins, sizeof(_pins)); +} + +size_t BusPlaceholder::getPins(uint8_t* pinArray) const { + size_t nPins = Bus::getNumberOfPins(_type); + if (pinArray) { + for (size_t i = 0; i < nPins; i++) pinArray[i] = _pins[i]; + } + return nPins; +} + //utility to get the approx. memory usage of a given BusConfig size_t BusConfig::memUsage(unsigned nr) const { if (Bus::isVirtual(type)) { @@ -1164,7 +1184,7 @@ size_t BusManager::memUsage() { return size + maxI2S; } -int BusManager::add(const BusConfig &bc) { +int BusManager::add(const BusConfig &bc, bool placeholder) { DEBUGBUS_PRINTF_P(PSTR("Bus: Adding bus (p:%d v:%d)\n"), getNumBusses(), getNumVirtualBusses()); unsigned digital = 0; unsigned analog = 0; @@ -1174,8 +1194,10 @@ int BusManager::add(const BusConfig &bc) { if (bus->isDigital() && !bus->is2Pin()) digital++; if (bus->is2Pin()) twoPin++; } - if (digital > WLED_MAX_DIGITAL_CHANNELS || analog > WLED_MAX_ANALOG_CHANNELS) return -1; - if (Bus::isVirtual(bc.type)) { + if (digital > WLED_MAX_DIGITAL_CHANNELS || analog > WLED_MAX_ANALOG_CHANNELS) placeholder = true; // TODO: add errorFlag here + if (placeholder) { + busses.push_back(make_unique(bc)); + } else if (Bus::isVirtual(bc.type)) { busses.push_back(make_unique(bc)); #ifdef WLED_ENABLE_HUB75MATRIX } else if (Bus::isHub75(bc.type)) { @@ -1282,7 +1304,7 @@ void BusManager::on() { if (PinManager::getPinOwner(LED_BUILTIN) == PinOwner::BusDigital) { for (auto &bus : busses) { uint8_t pins[2] = {255,255}; - if (bus->isDigital() && bus->getPins(pins)) { + if (bus->isDigital() && bus->getPins(pins) && bus->isOk()) { if (pins[0] == LED_BUILTIN || pins[1] == LED_BUILTIN) { BusDigital &b = static_cast(*bus); b.begin(); @@ -1377,7 +1399,7 @@ void BusManager::initializeABL() { _useABL = true; // at least one bus has ABL set uint32_t ESPshare = MA_FOR_ESP / numABLbuses; // share of ESP current per ABL bus for (auto &bus : busses) { - if (bus->isDigital()) { + if (bus->isDigital() && bus->isOk()) { BusDigital &busd = static_cast(*bus); uint32_t busLength = busd.getLength(); uint32_t busDemand = busLength * busd.getLEDCurrent(); diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index 95772a443f..625707049e 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -133,7 +133,7 @@ class Bus { virtual void setColorOrder(uint8_t co) {} virtual uint32_t getPixelColor(unsigned pix) const { return 0; } virtual size_t getPins(uint8_t* pinArray = nullptr) const { return 0; } - virtual uint16_t getLength() const { return isOk() ? _len : 0; } + virtual uint16_t getLength() const { return _len; } virtual uint8_t getColorOrder() const { return COL_ORDER_RGB; } virtual unsigned skippedLeds() const { return 0; } virtual uint16_t getFrequency() const { return 0U; } @@ -152,6 +152,7 @@ class Bus { inline bool isPWM() const { return isPWM(_type); } inline bool isVirtual() const { return isVirtual(_type); } inline bool is16bit() const { return is16bit(_type); } + virtual bool isPlaceholder() const { return false; } inline bool mustRefresh() const { return mustRefresh(_type); } inline void setReversed(bool reversed) { _reversed = reversed; } inline void setStart(uint16_t start) { _start = start; } @@ -372,6 +373,39 @@ class BusNetwork : public Bus { #endif }; +// Placeholder for buses that we can't construct due to resource limitations +// This preserves the configuration so it can be read back to the settings pages +// Function calls "mimic" the replaced bus, isPlaceholder() can be used to identify a placeholder +class BusPlaceholder : public Bus { + public: + BusPlaceholder(const BusConfig &bc); + + // Actual calls are stubbed out + void setPixelColor(unsigned pix, uint32_t c) override {}; + void show() override {}; + + // Accessors + uint8_t getColorOrder() const override { return _colorOrder; } + size_t getPins(uint8_t* pinArray) const override; + unsigned skippedLeds() const override { return _skipAmount; } + uint16_t getFrequency() const override { return _frequency; } + uint16_t getLEDCurrent() const override { return _milliAmpsPerLed; } + uint16_t getMaxCurrent() const override { return _milliAmpsMax; } + const String getCustomText() const override { return _text; } + bool isPlaceholder() const override { return true; } + + size_t getBusSize() const override { return sizeof(BusPlaceholder); } + + private: + uint8_t _colorOrder; + uint8_t _skipAmount; + uint8_t _pins[5]; + uint16_t _frequency; + uint8_t _milliAmpsPerLed; + uint16_t _milliAmpsMax; + String _text; +}; + #ifdef WLED_ENABLE_HUB75MATRIX class BusHub75Matrix : public Bus { public: @@ -504,7 +538,7 @@ namespace BusManager { //do not call this method from system context (network callback) void removeAll(); - int add(const BusConfig &bc); + int add(const BusConfig &bc, bool placeholder); void on(); void off(); diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index 840afb51ba..916a1542cf 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -964,7 +964,7 @@ void serializeConfig(JsonObject root) { for (size_t s = 0; s < BusManager::getNumBusses(); s++) { DEBUG_PRINTF_P(PSTR("Cfg: Saving bus #%u\n"), s); const Bus *bus = BusManager::getBus(s); - if (!bus || !bus->isOk()) break; + if (!bus) break; // Memory corruption, iterator invalid DEBUG_PRINTF_P(PSTR(" (%d-%d, type:%d, CO:%d, rev:%d, skip:%d, AW:%d kHz:%d, mA:%d/%d)\n"), (int)bus->getStart(), (int)(bus->getStart()+bus->getLength()), (int)(bus->getType() & 0x7F), diff --git a/wled00/data/index.js b/wled00/data/index.js index 2514f03fb1..3ff6376a22 100644 --- a/wled00/data/index.js +++ b/wled00/data/index.js @@ -668,9 +668,20 @@ function parseInfo(i) { if (loc) name = "(L) " + name; d.title = name; simplifiedUI = i.simplifiedui; - ledCount = i.leds.count; + // safety checks for LED count data to prevent UI crashes + if (i.leds && typeof i.leds.count !== 'undefined') { + ledCount = i.leds.count; + } else { + console.warn('No LED count'); + ledCount = 30; // fallback value + } //syncTglRecv = i.str; - maxSeg = i.leds.maxseg; + if (i.leds && typeof i.leds.maxseg !== 'undefined') { + maxSeg = i.leds.maxseg; + } else { + console.warn('No max segment data'); + maxSeg = 16; // Reasonable fallback for max segments + } pmt = i.fs.pmt; if (pcMode && !i.wifi.ap) gId('edit').classList.remove("hide"); else gId('edit').classList.add("hide"); gId('buttonNodes').style.display = lastinfo.ndc > 0 ? null:"none"; @@ -912,12 +923,24 @@ function populateSegments(s) gId(`segr${i}`).classList.add("hide"); } if (segCount < 2) { - gId(`segd${lSeg}`).classList.add("hide"); // hide delete if only one segment - if (parseInt(gId("seg0bri").value)==255) gId(`segp0`).classList.add("hide"); + // safety check for segment elements to prevent UI crashes + const segdElement = gId(`segd${lSeg}`); + if (segdElement) segdElement.classList.add("hide"); // hide delete if only one segment + const seg0briElement = gId("seg0bri"); + const segp0Element = gId(`segp0`); + if (seg0briElement && segp0Element && parseInt(seg0briElement.value)==255) segp0Element.classList.add("hide"); // hide segment controls if there is only one segment in simplified UI if (simplifiedUI) gId("segcont").classList.add("hide"); } - if (!isM && !noNewSegs && (cfg.comp.seglen?parseInt(gId(`seg${lSeg}s`).value):0)+parseInt(gId(`seg${lSeg}e`).value) 1) ? "block":"none"; // rsbtn parent if (Array.isArray(li.maps) && li.maps.length>1) { @@ -2251,7 +2274,9 @@ function rptSeg(s) var rev = gId(`seg${s}rev`).checked; var mi = gId(`seg${s}mi`).checked; var sel = gId(`seg${s}sel`).checked; - var pwr = gId(`seg${s}pwr`).classList.contains('act'); + // safety check for segment power element to prevent UI crashes + const segPwrElement = gId(`seg${s}pwr`); + var pwr = segPwrElement ? segPwrElement.classList.contains('act') : false; var obj = {"seg": {"id": s, "n": name, "start": start, "stop": (cfg.comp.seglen?start:0)+stop, "rev": rev, "mi": mi, "on": pwr, "bri": parseInt(gId(`seg${s}bri`).value), "sel": sel}}; if (gId(`seg${s}grp`)) { var grp = parseInt(gId(`seg${s}grp`).value); @@ -2378,7 +2403,13 @@ function setGrp(s, g) function setSegPwr(s) { - var pwr = gId(`seg${s}pwr`).classList.contains('act'); + // safety check for segment power element to prevent UI crashes + const segPwrElement = gId(`seg${s}pwr`); + if (!segPwrElement) { + console.warn('No power elemen'); + return; + } + var pwr = segPwrElement.classList.contains('act'); var obj = {"seg": {"id": s, "on": !pwr}}; requestJson(obj); } diff --git a/wled00/wled.cpp b/wled00/wled.cpp index 923688106d..94cf087f0b 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -39,8 +39,8 @@ void WLED::reset() void WLED::loop() { - static uint32_t lastHeap = UINT32_MAX; - static unsigned long heapTime = 0; + static uint16_t heapTime = 0; // timestamp for heap check + static uint8_t heapDanger = 0; // counter for consecutive low-heap readings #ifdef WLED_DEBUG static unsigned long lastRun = 0; unsigned long loopMillis = millis(); @@ -169,19 +169,41 @@ void WLED::loop() createEditHandler(false); } - // reconnect WiFi to clear stale allocations if heap gets too low - if (millis() - heapTime > 15000) { - uint32_t heap = getFreeHeapSize(); - if (heap < MIN_HEAP_SIZE && lastHeap < MIN_HEAP_SIZE) { - DEBUG_PRINTF_P(PSTR("Heap too low! %u\n"), heap); - forceReconnect = true; - strip.resetSegments(); // remove all but one segments from memory - } else if (heap < MIN_HEAP_SIZE) { - DEBUG_PRINTLN(F("Heap low, purging segments.")); - strip.purgeSegments(); + // free memory and reconnect WiFi to clear stale allocations if heap is too low for too long, check once per second + if ((uint16_t)(millis() - heapTime) > 1000) { + #ifdef ESP8266 + uint32_t heap = getFreeHeapSize(); // ESP8266 needs ~8k of free heap for UI to work properly + #else + uint32_t heap = getContiguousFreeHeap(); // ESP32 family needs ~10k of contiguous free heap for UI to work properly + #endif + if (heap < MIN_HEAP_SIZE) heapDanger++; + else heapDanger = 0; + switch (heapDanger) { + case 15: // 15 consecutive seconds + DEBUG_PRINTLN(F("Heap low, purging segments")); + strip.purgeSegments(); + strip.setTransition(0); // disable transitions + for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { + strip.getSegments()[i].setMode(FX_MODE_STATIC); // set static mode to free effect memory + } + errorFlag = ERR_NORAM; // alert UI TODO: make this a distinct error: segment reset + break; + case 30: // 30 consecutive seconds + DEBUG_PRINTLN(F("Heap low, reset segments")); + strip.resetSegments(); // remove all but one segments from memory + errorFlag = ERR_NORAM; // alert UI TODO: make this a distinct error: segment reset + break; + case 45: // 45 consecutive seconds + DEBUG_PRINTF_P(PSTR("Heap panic! Reset strip, reset connection\n")); + strip.~WS2812FX(); // deallocate strip and all its memory + new(&strip) WS2812FX(); // re-create strip object, respecting current memory limits + forceReconnect = true; // in case wifi is broken, make sure UI comes back, set disableForceReconnect = true to avert + errorFlag = ERR_NORAM; // alert UI TODO: make this a distinct error: strip reset + break; + default: + break; } - lastHeap = heap; - heapTime = millis(); + heapTime = (uint16_t)millis(); } //LED settings have been saved, re-init busses diff --git a/wled00/xml.cpp b/wled00/xml.cpp index 95479594ed..b47802f810 100644 --- a/wled00/xml.cpp +++ b/wled00/xml.cpp @@ -314,7 +314,7 @@ void getSettingsJS(byte subPage, Print& settingsScript) unsigned sumMa = 0; for (size_t s = 0; s < BusManager::getNumBusses(); s++) { const Bus *bus = BusManager::getBus(s); - if (!bus || !bus->isOk()) break; // should not happen but for safety + if (!bus) break; // should not happen but for safety int offset = s < 10 ? '0' : 'A' - 10; char lp[4] = "L0"; lp[2] = offset+s; lp[3] = 0; //ascii 0-9 //strip data pin char lc[4] = "LC"; lc[2] = offset+s; lc[3] = 0; //strip length