diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index f143811dbc..6a3f02dc82 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; +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; uint8_t BusHub75Matrix::instanceCount = 0; @@ -1096,7 +1100,85 @@ 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. + // 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. + // AI: below section was generated by an AI + default: + { + 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 + + // 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. + // 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; + + // 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. + 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->setRotation(0); + } + } + } + + // 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 + } 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..7a0b4f9e59 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 } @@ -440,9 +445,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,12 +466,18 @@ 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). // 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