Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 63 additions & 1 deletion wled00/bus_manager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if (_valid) {
_panelWidth = fourScanPanel ? fourScanPanel->width() : display->width(); // cache width - it will never change
Expand Down
26 changes: 23 additions & 3 deletions wled00/bus_manager.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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);
Expand All @@ -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();

Expand All @@ -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).
Expand Down
29 changes: 29 additions & 0 deletions wled00/wled.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down