From bbfe6b5dddb6fd42f980188e00ce52ba89640976 Mon Sep 17 00:00:00 2001 From: Atlan Date: Fri, 31 Jul 2026 17:09:07 +0200 Subject: [PATCH 1/5] HUB75: allow non-horizontal panel arrangements via VirtualMatrixPanel A HUB75 chain is electrically always horizontal, so N panels always form an area of (panel width * N) x panel height. Declaring a different 2D layout in the panel configuration only changes the logical area - BusHub75Matrix::show() still iterates over display->width(), so the image gets wrapped onto the physical chain width. Measured on device: a logical 64x128 area came out as 128x64, with the upper half red on BOTH panels instead of left red/right blue. VirtualMatrixPanel from the DMA library already does the logical-to-physical mapping, but it was only ever created for four-scan panels, and there hard-coded as (1, chain_length) - always a single row. This adds a default branch to the panel type switch that creates a VirtualMatrixPanel for normal panels as well, as soon as rows or columns exceed 1. The arrangement is carried in the existing bus "pin" field: [0] chain length [1] rows [2] columns [3] PANEL_CHAIN_TYPE nPins for HUB75 goes from 1 to 4 and getPins() reports the arrangement back, otherwise only the chain length survives a config save. Unset values are normalised to 1, so an existing pin: [2] becomes [2, 1, 1, 0] and the arrangement stays dormant - behaviour is unchanged for existing setups. rows * columns must equal the chain length, otherwise the arrangement is ignored and a warning is printed. Also adds a safety fuse: a bad arrangement could in theory stall during panel initialisation, leaving a device without USB access unreachable. wled.cpp writes /vpanel_try.txt before beginStrip() and removes it once the main loop has run for 20 s. If the marker is still there at boot, the arrangement is skipped so the device comes up normally; deleting the file from /edit arms it again. Parts of this change were drafted with AI assistance; they are marked as such in the code and were reviewed and tested on hardware by the author. --- wled00/bus_manager.cpp | 49 +++++++++++++++++++++++++++++++++++++++++- wled00/bus_manager.h | 26 +++++++++++++++++++--- wled00/wled.cpp | 29 +++++++++++++++++++++++++ 3 files changed, 100 insertions(+), 4 deletions(-) diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index f143811dbc..b232f87248 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -615,6 +615,10 @@ void BusNetwork::cleanup() { // BusHub75Matrix "global" variables (static members) MatrixPanel_I2S_DMA* BusHub75Matrix::activeDisplay = nullptr; VirtualMatrixPanel* BusHub75Matrix::activeFourScanPanel = nullptr; + +// WLEDMM+: see comment in bus_manager.h +bool hub75ArrangementArmed = true; +const char hub75TryFile[] = "/vpanel_try.txt"; HUB75_I2S_CFG BusHub75Matrix::activeMXconfig = HUB75_I2S_CFG(); uint8_t BusHub75Matrix::activeType = 0; uint8_t BusHub75Matrix::instanceCount = 0; @@ -1096,7 +1100,50 @@ BusHub75Matrix::BusHub75Matrix(BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWh fourScanPanel->setPhysicalPanelScanRate(FOUR_SCAN_64PX_HIGH); fourScanPanel->setRotation(0); break; - } + + // WLEDMM+: non-horizontal panel arrangement for NORMAL (non four-scan) panels. + // (This block was drafted with AI assistance and reviewed and tested on hardware by the author.) + // A HUB75 chain is electrically always horizontal: N panels form an area of + // (panel width * N) x panel height. Stacking the panels physically therefore needs a mapping + // from logical to physical coordinates. VirtualMatrixPanel does exactly that, but so far it was + // only created for four-scan panels, and there hard-coded as (1, chain_length) - always a single + // row. Without this branch show() iterates over display->width() and wraps the image onto the + // physical chain width. Measured on device: a logical 64x128 area came out as 128x64, with the + // upper half red on BOTH panels instead of left red / right blue. + // + // The arrangement is carried in the bus "pin" field: [0] = chain length, [1] = rows, + // [2] = columns, [3] = PANEL_CHAIN_TYPE (0 = CHAIN_NONE, 1 = TOP_LEFT_DOWN, 2 = TOP_RIGHT_DOWN, + // 3 = BOTTOM_LEFT_UP, 4 = BOTTOM_RIGHT_UP, 5..8 = the ZigZag variants). Which chain type is + // correct depends on how the panels are wired and mounted. Example for 4 panels of 64x32 + // stacked vertically and chained bottom-left up in a zigzag: pin: [4, 4, 1, 8]. + // Unset values are normalised to 1, so an existing pin: [2] becomes [2, 1, 1, 0] and the + // arrangement stays dormant - behaviour is unchanged for every existing configuration. + default: + if (!fourScanPanel) { + unsigned vRows = (bc.pins[1] == 255 || bc.pins[1] == 0) ? 1 : bc.pins[1]; + unsigned vCols = (bc.pins[2] == 255 || bc.pins[2] == 0) ? 1 : bc.pins[2]; + unsigned vType = (bc.pins[3] == 255) ? 0 : bc.pins[3]; + if (vType > CHAIN_BOTTOM_LEFT_UP_ZZ) vType = 0; // ignore out-of-range values + if (((vRows > 1) || (vCols > 1)) && !hub75ArrangementArmed) { + USER_PRINTLN("MatrixPanel_I2S_DMA: virtual arrangement SKIPPED (previous boot did not complete)."); + } + else if ((vRows > 1) || (vCols > 1)) { + if (vRows * vCols != mxconfig.chain_length) { + USER_PRINTF("MatrixPanel_I2S_DMA WARNING: %ux%u panels != chain length %u - arrangement ignored.\n", + vRows, vCols, mxconfig.chain_length); + } else { + USER_PRINTF("MatrixPanel_I2S_DMA virtual arrangement: %u rows x %u cols, chain type %u.\n", + vRows, vCols, vType); + fourScanPanel = new VirtualMatrixPanel((*display), vRows, vCols, + mxconfig.mx_width, mxconfig.mx_height, + (PANEL_CHAIN_TYPE)vType); + fourScanPanel->setRotation(0); + _vRows = vRows; _vCols = vCols; _vChainType = vType; + } + } + } + break; + } if (_valid) { _panelWidth = fourScanPanel ? fourScanPanel->width() : display->width(); // cache width - it will never change diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index 55bf20dc35..1e7aa6f6f1 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -90,7 +90,12 @@ struct BusConfig { if ((type >= TYPE_NET_DDP_RGB) && (type < (TYPE_NET_DDP_RGB + 16))) nPins = 4; // virtual network bus. 4 "pins" store IP address else if ((type > 47) && (type < 63)) nPins = 2; // (data + clock / SPI) busses - two pins else if (IS_PWM(type)) nPins = NUM_PWM_PINS(type); // PWM needs 1..5 pins - else if (type >= TYPE_HUB75MATRIX && type <= (TYPE_HUB75MATRIX + 10)) nPins = 1; // HUB75 does not use LED pins, but we need to preserve the "chain length" parameter + // HUB75 does not use LED pins. The "pin" array carries panel arrangement instead: + // [0] chain length, [1] virtual rows, [2] virtual cols, [3] PANEL_CHAIN_TYPE + // Rows/cols > 1 describe a non-horizontal arrangement (e.g. panels stacked vertically), + // which is handled by VirtualMatrixPanel. Was 1 before - then only the chain length survived + // a config save, and any arrangement was silently lost. + else if (type >= TYPE_HUB75MATRIX && type <= (TYPE_HUB75MATRIX + 10)) nPins = 4; for (uint8_t i = 0; i < min(unsigned(nPins), sizeof(pins)/sizeof(pins[0])); i++) pins[i] = ppins[i]; //softhack007 fix for potential array out-of-bounds access } @@ -422,6 +427,14 @@ class BusNetwork : public Bus { }; #ifdef WLED_ENABLE_HUB75MATRIX +// WLEDMM+: safety fuse for the virtual HUB75 arrangement. +// wled.cpp writes a marker file before the strip is initialised and removes it once the main loop +// has been running for a while. If the marker is still present at boot, the previous attempt never +// got that far -> hub75ArrangementArmed = false and no VirtualMatrixPanel is created. That way a +// bad arrangement cannot leave the device permanently unreachable. +extern bool hub75ArrangementArmed; +extern const char hub75TryFile[]; + class BusHub75Matrix : public Bus { public: BusHub75Matrix(BusConfig &bc); @@ -440,9 +453,13 @@ class BusHub75Matrix : public Bus { void setBrightness(uint8_t b, bool immediate) override; uint8_t getPins(uint8_t* pinArray) const override { + // No real LED pins - we report back the panel arrangement so it survives a config save. pinArray[0] = activeMXconfig.chain_length; - return 1; - } // Fake value due to keep finaliseInit happy + pinArray[1] = _vRows; + pinArray[2] = _vCols; + pinArray[3] = _vChainType; + return 4; + } // Fake values due to keep finaliseInit happy void deallocatePins(); @@ -457,6 +474,9 @@ class BusHub75Matrix : public Bus { private: unsigned _panelWidth = 0; uint8_t _colorOrder = COL_ORDER_RGB; + uint8_t _vRows = 1; // virtual panel rows (1 = classic horizontal chain) + uint8_t _vCols = 1; // virtual panel columns + uint8_t _vChainType = 0; // PANEL_CHAIN_TYPE, 0 = CHAIN_NONE CRGB *_ledBuffer = nullptr; byte *_ledsDirty = nullptr; // C++ dirty trick: private static variables are actually _not_ part of the class (however only visibile to class instances). diff --git a/wled00/wled.cpp b/wled00/wled.cpp index f8c4b2d780..8586a6cdb3 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -130,6 +130,17 @@ void WLED::loop() static uint16_t avgStripMillis = 0; #endif + #ifdef WLED_ENABLE_HUB75MATRIX + // WLEDMM+: the boot counts as successful once the main loop has been running for a while - then + // remove the marker for the virtual HUB75 arrangement. 20 s is ample: panel initialisation is + // long done by then and the web server is already serving requests. + static bool hub75TryCleared = false; + if (!hub75TryCleared && (millis() > 20000)) { + hub75TryCleared = true; + if (WLED_FS.exists(hub75TryFile)) WLED_FS.remove(hub75TryFile); + } + #endif + handleTime(); #ifndef WLED_DISABLE_INFRARED handleIR(); // 2nd call to function needed for ESP32 to return valid results -- should be good for ESP8266, too @@ -891,6 +902,24 @@ void WLED::setup() } #endif +#ifdef WLED_ENABLE_HUB75MATRIX + // WLEDMM+: safety fuse for the virtual HUB75 panel arrangement. + // (This block was drafted with AI assistance and reviewed and tested on hardware by the author.) + // A bad arrangement could in theory stall during panel initialisation - the web server would + // then never come up and the device would be unreachable without USB access. So: write a marker + // BEFORE creating it and remove it after a successful boot (see WLED::loop). If the marker is + // still present at boot, the previous attempt did not get through -> skip the arrangement so the + // device boots normally. To arm it again, delete the file from the /edit page. + if (WLED_FS.exists(hub75TryFile)) { + hub75ArrangementArmed = false; + USER_PRINTLN(F("HUB75: previous boot with virtual arrangement did not complete - arrangement DISABLED.")); + USER_PRINTLN(F("HUB75: delete /vpanel_try.txt to arm it again.")); + } else { + File f = WLED_FS.open(hub75TryFile, "w"); + if (f) { f.print(1); f.close(); } + } +#endif + DEBUG_PRINTLN(F("Initializing strip")); beginStrip(); // wait for strip to finish updating, to prevent glitches From b686411e80edc9d0f71328dbff6dd72da23fdfa6 Mon Sep 17 00:00:00 2001 From: Atlan Date: Fri, 31 Jul 2026 23:27:50 +0200 Subject: [PATCH 2/5] HUB75: keep arrangement metadata in sync when the display object is re-used MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses the review on #372. `_vRows` / `_vCols` / `_vChainType` are per-instance members defaulting to (1, 1, 0), and they were assigned only inside the `if (!fourScanPanel)` creation guard. When a bus is re-created while the display object is re-used, the fresh BusHub75Matrix already has `fourScanPanel = activeFourScanPanel` before the switch, so that block is skipped and the members keep their defaults. Since `getPins()` reports exactly those members — and does so specifically to make the arrangement survive a config save — the next config save after the first re-creation silently rewrote a working arrangement to "none". The boot fuse cannot help at that point, because the data it protects is already gone; for a panel in a closed enclosure that is the one failure mode this feature was supposed to avoid. The resync now happens on every pass, before the creation guard. Validation and panel creation are unchanged. Deliberately, the metadata is also assigned when the arrangement is skipped (fuse not armed) or rejected (rows * cols != chain_length): reporting (1, 1, 0) there would erase what the user configured. The warning already repeats on every boot — better to keep the configuration and keep complaining about it than to discard it quietly. Also marks the block with the `// AI: below section was generated by an AI` / `// AI: end` comments required by AGENTS.md and docs/cpp.instructions.md, replacing the informal note. Build checked: adafruit_matrixportal_esp32s3_tinyUF2 SUCCESS. --- wled00/bus_manager.cpp | 49 +++++++++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index b232f87248..b0a9e279a9 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -1102,7 +1102,6 @@ BusHub75Matrix::BusHub75Matrix(BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWh break; // WLEDMM+: non-horizontal panel arrangement for NORMAL (non four-scan) panels. - // (This block was drafted with AI assistance and reviewed and tested on hardware by the author.) // A HUB75 chain is electrically always horizontal: N panels form an area of // (panel width * N) x panel height. Stacking the panels physically therefore needs a mapping // from logical to physical coordinates. VirtualMatrixPanel does exactly that, but so far it was @@ -1118,31 +1117,47 @@ BusHub75Matrix::BusHub75Matrix(BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWh // stacked vertically and chained bottom-left up in a zigzag: pin: [4, 4, 1, 8]. // Unset values are normalised to 1, so an existing pin: [2] becomes [2, 1, 1, 0] and the // arrangement stays dormant - behaviour is unchanged for every existing configuration. + // AI: below section was generated by an AI default: - if (!fourScanPanel) { + { unsigned vRows = (bc.pins[1] == 255 || bc.pins[1] == 0) ? 1 : bc.pins[1]; unsigned vCols = (bc.pins[2] == 255 || bc.pins[2] == 0) ? 1 : bc.pins[2]; unsigned vType = (bc.pins[3] == 255) ? 0 : bc.pins[3]; if (vType > CHAIN_BOTTOM_LEFT_UP_ZZ) vType = 0; // ignore out-of-range values - if (((vRows > 1) || (vCols > 1)) && !hub75ArrangementArmed) { - USER_PRINTLN("MatrixPanel_I2S_DMA: virtual arrangement SKIPPED (previous boot did not complete)."); - } - else if ((vRows > 1) || (vCols > 1)) { - if (vRows * vCols != mxconfig.chain_length) { - USER_PRINTF("MatrixPanel_I2S_DMA WARNING: %ux%u panels != chain length %u - arrangement ignored.\n", - vRows, vCols, mxconfig.chain_length); - } else { - USER_PRINTF("MatrixPanel_I2S_DMA virtual arrangement: %u rows x %u cols, chain type %u.\n", - vRows, vCols, vType); - fourScanPanel = new VirtualMatrixPanel((*display), vRows, vCols, - mxconfig.mx_width, mxconfig.mx_height, - (PANEL_CHAIN_TYPE)vType); - fourScanPanel->setRotation(0); - _vRows = vRows; _vCols = vCols; _vChainType = vType; + + // Keep the arrangement metadata in sync with the configuration on EVERY pass, not only + // when the panel object is created. getPins() reports these members so the arrangement + // survives a config save - and when a bus is re-created while the display object is + // re-used (see "continue with existing matrix object" above), fourScanPanel is already + // set on this fresh instance, so the creation block below is skipped. Assigning only in + // there would leave the new instance at the (1,1,0) defaults, and the next config save + // would silently overwrite a working arrangement with "none" - unrecoverable without + // physical access, which is exactly what the boot fuse is meant to avoid. + // Deliberately also assigned when the arrangement is skipped or rejected below: what the + // user configured stays in the configuration, and the warning repeats on every boot. + _vRows = vRows; _vCols = vCols; _vChainType = vType; + + if (!fourScanPanel) { + if (((vRows > 1) || (vCols > 1)) && !hub75ArrangementArmed) { + USER_PRINTLN("MatrixPanel_I2S_DMA: virtual arrangement SKIPPED (previous boot did not complete)."); + } + else if ((vRows > 1) || (vCols > 1)) { + if (vRows * vCols != mxconfig.chain_length) { + USER_PRINTF("MatrixPanel_I2S_DMA WARNING: %ux%u panels != chain length %u - arrangement ignored.\n", + vRows, vCols, mxconfig.chain_length); + } else { + USER_PRINTF("MatrixPanel_I2S_DMA virtual arrangement: %u rows x %u cols, chain type %u.\n", + vRows, vCols, vType); + fourScanPanel = new VirtualMatrixPanel((*display), vRows, vCols, + mxconfig.mx_width, mxconfig.mx_height, + (PANEL_CHAIN_TYPE)vType); + fourScanPanel->setRotation(0); + } } } } break; + // AI: end } if (_valid) { From cad1dc7a58776961ea6dce3189b4d704e1da25f3 Mon Sep 17 00:00:00 2001 From: Atlan Date: Thu, 13 Aug 2026 20:44:23 +0200 Subject: [PATCH 3/5] HUB75: use the // WLEDMM: comment marker Review feedback: the project marks its own comments and additions with // WLEDMM: - the // WLEDMM+: variant used here made them harder to find. Comment text only, no functional change. Co-Authored-By: Claude Opus 5 (1M context) --- wled00/bus_manager.cpp | 4 ++-- wled00/bus_manager.h | 2 +- wled00/wled.cpp | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index b0a9e279a9..055ece1c5e 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -616,7 +616,7 @@ void BusNetwork::cleanup() { MatrixPanel_I2S_DMA* BusHub75Matrix::activeDisplay = nullptr; VirtualMatrixPanel* BusHub75Matrix::activeFourScanPanel = nullptr; -// WLEDMM+: see comment in bus_manager.h +// WLEDMM: see comment in bus_manager.h bool hub75ArrangementArmed = true; const char hub75TryFile[] = "/vpanel_try.txt"; HUB75_I2S_CFG BusHub75Matrix::activeMXconfig = HUB75_I2S_CFG(); @@ -1101,7 +1101,7 @@ BusHub75Matrix::BusHub75Matrix(BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWh fourScanPanel->setRotation(0); break; - // WLEDMM+: non-horizontal panel arrangement for NORMAL (non four-scan) panels. + // WLEDMM: non-horizontal panel arrangement for NORMAL (non four-scan) panels. // A HUB75 chain is electrically always horizontal: N panels form an area of // (panel width * N) x panel height. Stacking the panels physically therefore needs a mapping // from logical to physical coordinates. VirtualMatrixPanel does exactly that, but so far it was diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index 1e7aa6f6f1..06a963ee47 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -427,7 +427,7 @@ class BusNetwork : public Bus { }; #ifdef WLED_ENABLE_HUB75MATRIX -// WLEDMM+: safety fuse for the virtual HUB75 arrangement. +// WLEDMM: safety fuse for the virtual HUB75 arrangement. // wled.cpp writes a marker file before the strip is initialised and removes it once the main loop // has been running for a while. If the marker is still present at boot, the previous attempt never // got that far -> hub75ArrangementArmed = false and no VirtualMatrixPanel is created. That way a diff --git a/wled00/wled.cpp b/wled00/wled.cpp index 8586a6cdb3..7d30ab9217 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -131,7 +131,7 @@ void WLED::loop() #endif #ifdef WLED_ENABLE_HUB75MATRIX - // WLEDMM+: the boot counts as successful once the main loop has been running for a while - then + // WLEDMM: the boot counts as successful once the main loop has been running for a while - then // remove the marker for the virtual HUB75 arrangement. 20 s is ample: panel initialisation is // long done by then and the web server is already serving requests. static bool hub75TryCleared = false; @@ -903,7 +903,7 @@ void WLED::setup() #endif #ifdef WLED_ENABLE_HUB75MATRIX - // WLEDMM+: safety fuse for the virtual HUB75 panel arrangement. + // WLEDMM: safety fuse for the virtual HUB75 panel arrangement. // (This block was drafted with AI assistance and reviewed and tested on hardware by the author.) // A bad arrangement could in theory stall during panel initialisation - the web server would // then never come up and the device would be unreachable without USB access. So: write a marker From d62c805a0fec46941877fff87f7ce480b22f44c9 Mon Sep 17 00:00:00 2001 From: Atlan Date: Thu, 13 Aug 2026 21:07:22 +0200 Subject: [PATCH 4/5] HUB75: guard the arrangement allocation, drop the boot fuse The arrangement runs after display->begin() and after the LED buffer allocation, and VirtualMatrixPanel only remaps coordinates - it touches neither DMA nor pins. The one thing left that could keep the device from reaching the main loop was the unchecked allocation: on failure the very next line dereferenced the null pointer. Allocate with std::nothrow and check the result. Without a panel object the plain horizontal chain is used, which is the behaviour before this series, and the reason is logged. With that handled at the source, the marker file written before beginStrip() no longer has a purpose, so the wled.cpp part of this series is removed again along with hub75ArrangementArmed and /vpanel_try.txt. Range checking needs no extra code: chain_length is already capped to a sane value when it is read, and rows * cols must equal it, so both are bounded by the existing validation. Co-Authored-By: Claude Opus 5 (1M context) --- wled00/bus_manager.cpp | 35 ++++++++++++++++++----------------- wled00/bus_manager.h | 8 -------- wled00/wled.cpp | 29 ----------------------------- 3 files changed, 18 insertions(+), 54 deletions(-) diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index 055ece1c5e..c8f811f884 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -616,9 +616,6 @@ void BusNetwork::cleanup() { MatrixPanel_I2S_DMA* BusHub75Matrix::activeDisplay = nullptr; VirtualMatrixPanel* BusHub75Matrix::activeFourScanPanel = nullptr; -// WLEDMM: see comment in bus_manager.h -bool hub75ArrangementArmed = true; -const char hub75TryFile[] = "/vpanel_try.txt"; HUB75_I2S_CFG BusHub75Matrix::activeMXconfig = HUB75_I2S_CFG(); uint8_t BusHub75Matrix::activeType = 0; uint8_t BusHub75Matrix::instanceCount = 0; @@ -1132,25 +1129,29 @@ BusHub75Matrix::BusHub75Matrix(BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWh // set on this fresh instance, so the creation block below is skipped. Assigning only in // there would leave the new instance at the (1,1,0) defaults, and the next config save // would silently overwrite a working arrangement with "none" - unrecoverable without - // physical access, which is exactly what the boot fuse is meant to avoid. - // Deliberately also assigned when the arrangement is skipped or rejected below: what the - // user configured stays in the configuration, and the warning repeats on every boot. + // physical access. + // Deliberately also assigned when the arrangement is rejected below: what the user + // configured stays in the configuration, and the warning repeats on every boot. _vRows = vRows; _vCols = vCols; _vChainType = vType; - if (!fourScanPanel) { - if (((vRows > 1) || (vCols > 1)) && !hub75ArrangementArmed) { - USER_PRINTLN("MatrixPanel_I2S_DMA: virtual arrangement SKIPPED (previous boot did not complete)."); - } - else if ((vRows > 1) || (vCols > 1)) { - if (vRows * vCols != mxconfig.chain_length) { - USER_PRINTF("MatrixPanel_I2S_DMA WARNING: %ux%u panels != chain length %u - arrangement ignored.\n", - vRows, vCols, mxconfig.chain_length); + if (!fourScanPanel && ((vRows > 1) || (vCols > 1))) { + // The arrangement must describe exactly the panels that are chained. chain_length is + // already capped to a sane value above, so this also bounds vRows and vCols. + if (vRows * vCols != mxconfig.chain_length) { + USER_PRINTF("MatrixPanel_I2S_DMA WARNING: %ux%u panels != chain length %u - arrangement ignored.\n", + vRows, vCols, mxconfig.chain_length); + } else { + // The display is fully initialised at this point - VirtualMatrixPanel only remaps + // coordinates, so the allocation is the one thing left that can fail here. Without a + // panel object the plain horizontal chain is used, which is the previous behaviour. + fourScanPanel = new(std::nothrow) VirtualMatrixPanel((*display), vRows, vCols, + mxconfig.mx_width, mxconfig.mx_height, + (PANEL_CHAIN_TYPE)vType); + if (fourScanPanel == nullptr) { + USER_PRINTLN("MatrixPanel_I2S_DMA WARNING: not enough memory for the virtual arrangement - using the plain chain."); } else { USER_PRINTF("MatrixPanel_I2S_DMA virtual arrangement: %u rows x %u cols, chain type %u.\n", vRows, vCols, vType); - fourScanPanel = new VirtualMatrixPanel((*display), vRows, vCols, - mxconfig.mx_width, mxconfig.mx_height, - (PANEL_CHAIN_TYPE)vType); fourScanPanel->setRotation(0); } } diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index 06a963ee47..7d282aa7b7 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -427,14 +427,6 @@ class BusNetwork : public Bus { }; #ifdef WLED_ENABLE_HUB75MATRIX -// WLEDMM: safety fuse for the virtual HUB75 arrangement. -// wled.cpp writes a marker file before the strip is initialised and removes it once the main loop -// has been running for a while. If the marker is still present at boot, the previous attempt never -// got that far -> hub75ArrangementArmed = false and no VirtualMatrixPanel is created. That way a -// bad arrangement cannot leave the device permanently unreachable. -extern bool hub75ArrangementArmed; -extern const char hub75TryFile[]; - class BusHub75Matrix : public Bus { public: BusHub75Matrix(BusConfig &bc); diff --git a/wled00/wled.cpp b/wled00/wled.cpp index 7d30ab9217..f8c4b2d780 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -130,17 +130,6 @@ void WLED::loop() static uint16_t avgStripMillis = 0; #endif - #ifdef WLED_ENABLE_HUB75MATRIX - // WLEDMM: the boot counts as successful once the main loop has been running for a while - then - // remove the marker for the virtual HUB75 arrangement. 20 s is ample: panel initialisation is - // long done by then and the web server is already serving requests. - static bool hub75TryCleared = false; - if (!hub75TryCleared && (millis() > 20000)) { - hub75TryCleared = true; - if (WLED_FS.exists(hub75TryFile)) WLED_FS.remove(hub75TryFile); - } - #endif - handleTime(); #ifndef WLED_DISABLE_INFRARED handleIR(); // 2nd call to function needed for ESP32 to return valid results -- should be good for ESP8266, too @@ -902,24 +891,6 @@ void WLED::setup() } #endif -#ifdef WLED_ENABLE_HUB75MATRIX - // WLEDMM: safety fuse for the virtual HUB75 panel arrangement. - // (This block was drafted with AI assistance and reviewed and tested on hardware by the author.) - // A bad arrangement could in theory stall during panel initialisation - the web server would - // then never come up and the device would be unreachable without USB access. So: write a marker - // BEFORE creating it and remove it after a successful boot (see WLED::loop). If the marker is - // still present at boot, the previous attempt did not get through -> skip the arrangement so the - // device boots normally. To arm it again, delete the file from the /edit page. - if (WLED_FS.exists(hub75TryFile)) { - hub75ArrangementArmed = false; - USER_PRINTLN(F("HUB75: previous boot with virtual arrangement did not complete - arrangement DISABLED.")); - USER_PRINTLN(F("HUB75: delete /vpanel_try.txt to arm it again.")); - } else { - File f = WLED_FS.open(hub75TryFile, "w"); - if (f) { f.print(1); f.close(); } - } -#endif - DEBUG_PRINTLN(F("Initializing strip")); beginStrip(); // wait for strip to finish updating, to prevent glitches From d5b9cbb7d9dfe2433fcd572588a9995185c4dd80 Mon Sep 17 00:00:00 2001 From: Atlan Date: Thu, 13 Aug 2026 21:34:09 +0200 Subject: [PATCH 5/5] HUB75: rebuild the virtual mapping when the arrangement changes The re-use check for the display only compares the physical configuration - pins, chain length, panel size, type, driver options. The arrangement lives in the bus pin field and is not part of it, so a re-used display kept its old VirtualMatrixPanel while the metadata was already updated: show() went on rendering through the previous mapping, a changed arrangement only took effect after a reboot, and switching the arrangement off did not fall back to the plain chain at all. Track what the active mapping was built for and drop it when the configured arrangement no longer matches, so the block below builds the right one - or none, which is the plain chain. The object itself is not deleted, in line with the disabled delete in cleanup(). Verified on hardware, MatrixPortal-S3 with four 64x32 panels stacked vertically, chain length 4, arrangement 4x1. Test picture: one colour per panel plus a black bar over the top left of each, so both the panel order and the orientation are visible. | firmware | action | bars | |----------|-------------------------------|--------------| | before | running with chain type 2 | top left | | before | live change to chain type 1 | top left | | after | reboot with chain type 1 | bottom right | | after | live change to chain type 2 | top left | Row 2 is the bug: the configuration said a different chaining and nothing changed. Row 3 shows the two types do render differently, row 4 shows the mapping is now rebuilt without a reboot. Co-Authored-By: Claude Opus 5 (1M context) --- wled00/bus_manager.cpp | 19 +++++++++++++++++++ wled00/bus_manager.h | 3 +++ 2 files changed, 22 insertions(+) diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index c8f811f884..6a3f02dc82 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -615,6 +615,9 @@ void BusNetwork::cleanup() { // BusHub75Matrix "global" variables (static members) MatrixPanel_I2S_DMA* BusHub75Matrix::activeDisplay = nullptr; VirtualMatrixPanel* BusHub75Matrix::activeFourScanPanel = nullptr; +uint8_t BusHub75Matrix::activeVRows = 1; +uint8_t BusHub75Matrix::activeVCols = 1; +uint8_t BusHub75Matrix::activeVChainType = 0; HUB75_I2S_CFG BusHub75Matrix::activeMXconfig = HUB75_I2S_CFG(); uint8_t BusHub75Matrix::activeType = 0; @@ -1134,6 +1137,17 @@ BusHub75Matrix::BusHub75Matrix(BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWh // configured stays in the configuration, and the warning repeats on every boot. _vRows = vRows; _vCols = vCols; _vChainType = vType; + // A re-used display brings its mapping along (see "continue with existing matrix object" + // above), and the re-use check only looks at the physical configuration - the arrangement + // is not part of it. Keep the mapping only while it still describes what is configured; + // otherwise drop it, so the block below builds the right one and switching the + // arrangement off falls back to the plain chain instead of rendering through the old map. + // Not deleted on purpose: see the note next to the disabled delete in cleanup(). + if (fourScanPanel + && !((activeVRows == vRows) && (activeVCols == vCols) && (activeVChainType == vType))) { + fourScanPanel = nullptr; + } + if (!fourScanPanel && ((vRows > 1) || (vCols > 1))) { // The arrangement must describe exactly the panels that are chained. chain_length is // already capped to a sane value above, so this also bounds vRows and vCols. @@ -1156,6 +1170,11 @@ BusHub75Matrix::BusHub75Matrix(BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWh } } } + + // Record what the mapping in use actually represents, so the check above can tell a + // re-used one apart from a reconfigured arrangement. + if (fourScanPanel) { activeVRows = vRows; activeVCols = vCols; activeVChainType = vType; } + else { activeVRows = 1; activeVCols = 1; activeVChainType = 0; } } break; // AI: end diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index 7d282aa7b7..7a0b4f9e59 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -475,6 +475,9 @@ class BusHub75Matrix : public Bus { // These variables persist when BusHub75Matrix gets deleted. static MatrixPanel_I2S_DMA *activeDisplay; // active display object static VirtualMatrixPanel *activeFourScanPanel; // active fourScan object + // WLEDMM: arrangement the active fourScan object was built for - the re-use check for the + // display only covers the physical configuration, so the arrangement is tracked separately. + static uint8_t activeVRows, activeVCols, activeVChainType; static HUB75_I2S_CFG activeMXconfig; // last used mxconfig static uint8_t activeType; // last used type static uint8_t instanceCount; // active instances - 0 or 1