diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index f143811dbc..b0a9e279a9 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,65 @@ 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, 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) { _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