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
105 changes: 105 additions & 0 deletions apollo/hub75-color-order-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// AI: below section was generated by an AI
// Host-side check of the HUB75 color order pin permutation in wled00/bus_manager.cpp (hub75RemapColorPins).
// Runs with `npm test` (node --test). It reads the lineSrc table, the normalisation range and the M-1 (MOONHUB_S3)
// pinout from the C++ source, simulates the remap, and asserts:
// - every order permutes the same six GPIOs (pin allocation is unaffected),
// - stored 0 (factory/default), 1, 7..15 and white-swap values leave the M-1 pins exactly as before (no swap),
// - order 4 (BGR) gives the same pins as the old r1<->b1 / r2<->b2 swap,
// - orders 1..5 put the same data on the panel's R/G/B lines as WLED's PolyBus wire order and WLED-MM PR #349,
// - HUB75-only order 6 is a real red/green swap.
'use strict';
const test = require('node:test');
const assert = require('node:assert');
const fs = require('fs');
const path = require('path');

const src = fs.readFileSync(path.join(__dirname, '..', 'wled00', 'bus_manager.cpp'), 'utf8');

// lineSrc rows: logical channel (0=R 1=G 2=B) sent on the panel's R, G, B input lines, per stored order
const tableText = src.match(/lineSrc\[[^\]]+\]\[3\]\s*=\s*\{([\s\S]*?)\};/);
assert.ok(tableText, 'lineSrc table not found in bus_manager.cpp');
const lineSrc = [...tableText[1].matchAll(/\{\s*(\d)\s*,\s*(\d)\s*,\s*(\d)\s*\}/g)].map(m => [+m[1], +m[2], +m[3]]);
const maxOrder = +src.match(/COL_ORDER_HUB75_GRB\s*=\s*(\d+)/)[1];

// M-1 board wiring (MOONHUB_S3_PINOUT): r1,g1,b1,r2,g2,b2 are the first six entries
const pinText = src.match(/defined\(MOONHUB_S3_PINOUT\)[\s\S]*?mxconfig\.gpio\s*=\s*\{([^}]*)\}/);
assert.ok(pinText, 'MOONHUB_S3_PINOUT pin list not found');
const p = pinText[1].split(',').map(s => +s.trim());
const M1 = { r1: p[0], g1: p[1], b1: p[2], r2: p[3], g2: p[4], b2: p[5] };
const lineOfPin = { [M1.r1]: 'R', [M1.g1]: 'G', [M1.b1]: 'B', [M1.r2]: 'R', [M1.g2]: 'G', [M1.b2]: 'B' };

// hub75ColorOrder(): read its accepted range and fallback from the C++ so a change there is tested too
const normText = src.match(/static uint8_t hub75ColorOrder\(uint8_t co\)\s*\{\s*co &= 0x0F;\s*return \(co >= (\w+) && co <= (\w+)\) \? co : (\w+);/);
assert.ok(normText, 'hub75ColorOrder() body not in the expected form');
const COL = { COL_ORDER_GRB: 0, COL_ORDER_RGB: 1, COL_ORDER_HUB75_GRB: maxOrder };
const [normLo, normHi, normDefault] = normText.slice(1, 4).map(s => (s in COL) ? COL[s] : +s);
const norm = co => { co &= 0x0F; return (co >= normLo && co <= normHi) ? co : normDefault; };
// mirrors hub75RemapColorPins()
function remap(g, co) {
const in1 = [g.r1, g.g1, g.b1], in2 = [g.r2, g.g2, g.b2], k1 = ['r1', 'g1', 'b1'], k2 = ['r2', 'g2', 'b2'];
const out = { ...g }, s = lineSrc[norm(co)];
for (let line = 0; line < 3; line++) { out[k1[s[line]]] = in1[line]; out[k2[s[line]]] = in2[line]; }
return out;
}
// data that reaches the panel's physical R, G, B lines (upper half) for logical colour r,g,b
function lines(co, r, g, b) {
const q = remap(M1, co), L = {};
L[lineOfPin[q.r1]] = r; L[lineOfPin[q.g1]] = g; L[lineOfPin[q.b1]] = b;
return [L.R, L.G, L.B];
}
// same for the lower half (r2,g2,b2)
function lines2(co, r, g, b) {
const q = remap(M1, co), L = {};
L[lineOfPin[q.r2]] = r; L[lineOfPin[q.g2]] = g; L[lineOfPin[q.b2]] = b;
return [L.R, L.G, L.B];
}
// WLED-MM PR #349 per-pixel switch (data on R,G,B lines)
const mm = (o, r, g, b) => ({ 0: [g, r, b], 1: [r, g, b], 2: [b, r, g], 3: [r, b, g], 4: [b, g, r], 5: [g, b, r] })[o];
// WLED PolyBus wire order: first byte -> panel R line, second -> G, third -> B
const poly = (o, r, g, b) => ({ 1: [r, g, b], 2: [b, r, g], 3: [r, b, g], 4: [b, g, r], 5: [g, b, r] })[o];
const sorted = g => Object.values(g).sort((a, b) => a - b).join();

test('table shape', () => {
assert.strictEqual(maxOrder, 6);
assert.strictEqual(lineSrc.length, maxOrder + 1);
for (const row of lineSrc) assert.deepStrictEqual([...row].sort(), [0, 1, 2]);
});

test('stored 0 (factory and default value) can never select a swap', () => {
assert.deepStrictEqual([normLo, normHi, normDefault], [1, maxOrder, 1]); // 0 falls back to RGB
assert.deepStrictEqual(lineSrc[0], [0, 1, 2]); // and its row is the identity anyway
});

test('every stored value permutes the same six GPIOs', () => {
for (let co = 0; co <= 0xFF; co++) assert.strictEqual(sorted(remap(M1, co)), sorted(M1), `order ${co}`);
});

test('values shipped units hold keep the original pins (no swap)', () => {
for (const co of [0, 1, 7, 8, 15, 0x10, 0x11, 0xF0]) assert.deepStrictEqual(remap(M1, co), M1, `stored ${co}`);
});

test('BGR (4) matches the former r<->b pin swap', () => {
const bgr = { ...M1 };
[bgr.r1, bgr.b1] = [bgr.b1, bgr.r1];
[bgr.r2, bgr.b2] = [bgr.b2, bgr.r2];
assert.deepStrictEqual(remap(M1, 4), bgr);
});

test('orders 1..5 match PolyBus and WLED-MM #349, both panel halves', () => {
for (const o of [1, 2, 3, 4, 5]) for (const [r, g, b] of [[255, 0, 0], [0, 255, 0], [0, 0, 255], [10, 20, 30]]) {
assert.deepStrictEqual(lines(o, r, g, b), poly(o, r, g, b), `order ${o} vs PolyBus`);
assert.deepStrictEqual(lines(o, r, g, b), mm(o, r, g, b), `order ${o} vs WLED-MM`);
assert.deepStrictEqual(lines2(o, r, g, b), lines(o, r, g, b), `order ${o} lower half`);
}
});

test('RBG (3) swaps green and blue: the customer panel', () => {
assert.deepStrictEqual(lines(3, 0, 255, 0), [0, 0, 255]); // green data leaves on the panel's B input
assert.deepStrictEqual(lines(3, 0, 0, 255), [0, 255, 0]); // blue data leaves on the panel's G input
assert.deepStrictEqual(lines(3, 255, 0, 0), [255, 0, 0]);
});

test('HUB75-only GRB (6) is a real red/green swap', () => {
assert.deepStrictEqual(lines(6, 10, 20, 30), mm(0, 10, 20, 30));
});
// AI: end
50 changes: 35 additions & 15 deletions wled00/bus_manager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -795,9 +795,42 @@ void BusNetwork::cleanup() {
#error ESP8266 does not support HUB75
#endif

// AI: below section was generated by an AI
// HUB75 color order (Apollo M-1). Order "XYZ" means the panel's R, G and B inputs light X, Y and Z LEDs, so the X data
// must leave on the pin wired to the panel's R input. Same meaning as the PolyBus WS281x orders and as WLED-MM PR #349
// (troyhacks). Implemented by permuting the driver's GPIO roles once before the driver is created, like the former
// BGR-only case: no per-pixel cost, and it covers both the buffered and the direct-draw paths. The pin-swap approach
// follows softhack007's review of WLED-MM #349; the persistence gap was analysed by jsenecal in wled/WLED#5723.
// A stored 0 (WLED's GRB default) is what shipped M-1 units hold and has always meant "no swap" on HUB75, so it stays
// RGB; a real red/green swap uses the HUB75-only value 6.
static constexpr uint8_t COL_ORDER_HUB75_GRB = 6; // COL_ORDER_MAX stays 5, so Color Order Override still rejects it

// returns 1..6; 0, 7..15 and the white-swap nibble (meaningless without a white channel) map to RGB
static uint8_t hub75ColorOrder(uint8_t co) {
co &= 0x0F;
return (co >= COL_ORDER_RGB && co <= COL_ORDER_HUB75_GRB) ? co : COL_ORDER_RGB;
}

// permutes {r1,g1,b1} and {r2,g2,b2} among themselves, so the set of allocated GPIOs does not change
static void hub75RemapColorPins(HUB75_I2S_CFG::i2s_pins &gpio, uint8_t co) {
// for each order: the logical channel (0=R 1=G 2=B) to send on the panel's R, G and B input lines
static const uint8_t lineSrc[COL_ORDER_HUB75_GRB + 1][3] = {
{0,1,2}, {0,1,2}, {2,0,1}, {0,2,1}, {2,1,0}, {1,2,0}, {1,0,2} // (0 = legacy RGB), RGB, BRG, RBG, BGR, GBR, GRB
};
const int8_t in1[3] = {gpio.r1, gpio.g1, gpio.b1}, in2[3] = {gpio.r2, gpio.g2, gpio.b2}; // board wiring R,G,B
int8_t *role1[3] = {&gpio.r1, &gpio.g1, &gpio.b1}, *role2[3] = {&gpio.r2, &gpio.g2, &gpio.b2}; // driver: red data -> r pin
const uint8_t *src = lineSrc[hub75ColorOrder(co)];
for (unsigned line = 0; line < 3; line++) {
*role1[src[line]] = in1[line];
*role2[src[line]] = in2[line];
}
}
// AI: end

BusHub75Matrix::BusHub75Matrix(const BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWhite) {
size_t lastHeap = ESP.getFreeHeap();
_valid = false;
_colorOrder = hub75ColorOrder(bc.colorOrder); // set before any early return so a config save keeps the choice
_hasRgb = true;
_hasWhite = false;
virtualDisp = nullptr; // todo: this should be solved properly, can cause memory leak (if omitted here, nothing seems to work)
Expand Down Expand Up @@ -984,21 +1017,8 @@ BusHub75Matrix::BusHub75Matrix(const BusConfig &bc) : Bus(bc.type, bc.start, bc.
return;
}

if (bc.colorOrder == COL_ORDER_RGB) {
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA = Default color order (RGB)");
} else if (bc.colorOrder == COL_ORDER_BGR) {
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA = color order BGR");
int8_t tmpPin;
tmpPin = mxconfig.gpio.r1;
mxconfig.gpio.r1 = mxconfig.gpio.b1;
mxconfig.gpio.b1 = tmpPin;
tmpPin = mxconfig.gpio.r2;
mxconfig.gpio.r2 = mxconfig.gpio.b2;
mxconfig.gpio.b2 = tmpPin;
}
else {
DEBUGBUS_PRINTF("MatrixPanel_I2S_DMA = unsupported color order %u\n", bc.colorOrder);
}
hub75RemapColorPins(mxconfig.gpio, _colorOrder); // after pin allocation (same pin set), before the driver is created
DEBUGBUS_PRINTF_P(PSTR("MatrixPanel_I2S_DMA color order %u (cfg %u)\n"), _colorOrder, bc.colorOrder);

DEBUGBUS_PRINTF("MatrixPanel_I2S_DMA config - %ux%u length: %u\n", mxconfig.mx_width, mxconfig.mx_height, mxconfig.chain_length);
DEBUGBUS_PRINTF("R1_PIN=%u, G1_PIN=%u, B1_PIN=%u, R2_PIN=%u, G2_PIN=%u, B2_PIN=%u, A_PIN=%u, B_PIN=%u, C_PIN=%u, D_PIN=%u, E_PIN=%u, LAT_PIN=%u, OE_PIN=%u, CLK_PIN=%u\n",
Expand Down
2 changes: 2 additions & 0 deletions wled00/bus_manager.h
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,7 @@ class BusHub75Matrix : public Bus {
void show() override;
void setBrightness(uint8_t b) override;
size_t getPins(uint8_t* pinArray = nullptr) const override;
uint8_t getColorOrder() const override { return _colorOrder; } // so cfg.json and the settings page keep the HUB75 order
void deallocatePins();
void cleanup();

Expand All @@ -440,6 +441,7 @@ class BusHub75Matrix : public Bus {
unsigned _panelWidth = 0;
uint8_t _rows = 1; // panels per row
uint8_t _cols = 1; // panels per column
uint8_t _colorOrder = COL_ORDER_RGB; // normalised HUB75 color order: 1..5 as WLED, 6 = GRB (see bus_manager.cpp)
bool _isVirtual = false; // note: this is not strictly needed but there are padding bytes here anyway
bool _isQuadScan = false;
CRGB *_ledBuffer = nullptr; // note: using uint32_t buffer is only 2% faster and not worth the extra RAM
Expand Down
19 changes: 18 additions & 1 deletion wled00/data/settings_leds.htm
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,23 @@
}
gId("rf"+n).onclick = mustR(t) ? (()=>{return false}) : (()=>{}); // prevent change change of "Refresh" checkmark when mandatory
gRGBW |= hasW(t); // RGBW checkbox
gId("co"+n).style.display = (isVir(t) || isAna(t) || isHub75(t)) ? "none":"inline"; // hide color order for PWM
gId("co"+n).style.display = (isVir(t) || isAna(t)) ? "none":"inline"; // hide color order for PWM
// AI: below section was generated by an AI
// HUB75 (Apollo M-1): a stored 0 (GRB default) is a legacy alias of RGB; real GRB is 6 (hub75ColorOrder() in bus_manager.cpp).
// dataset.prev remembers the value a conversion replaced, so switching a bus type and back restores it.
// A manual choice in the select clears it; loadCfg() rebuilds the rows, so a loaded config starts fresh.
let co = d.Sf["CO"+n], hub = isHub75(t), prev = co.dataset.prev;
if (hub && co.value == 0) { // entering HUB75 (or legacy config): no swap, as before
if (prev === "6") { co.value = 6; delete co.dataset.prev; } else { co.value = 1; co.dataset.prev = "0"; }
} else if (!hub && co.value == 6) { // leaving HUB75 with GRB: GRB keeps its meaning
co.value = 0; co.dataset.prev = "6";
} else if (!hub && co.value == 1 && prev === "0") { // back to a strip that was GRB before
co.value = 0; delete co.dataset.prev;
}
co.onchange = () => { delete co.dataset.prev; }; // a manual choice wins over any restore
co.options[0].hidden = co.options[0].disabled = hub; // value 0 = GRB for strips
co.options[6].hidden = co.options[6].disabled = !hub; // value 6 = GRB for HUB75
// AI: end
gId("dig"+n+"w").style.display = (isDig(t) && hasW(t)) ? "inline":"none"; // show swap channels dropdown
gId("dig"+n+"w").querySelector("[data-opt=CCT]").disabled = !hasCCT(t); // disable WW/CW swapping
if (!(isDig(t) && hasW(t))) d.Sf["WO"+n].value = 0; // reset swapping
Expand Down Expand Up @@ -566,6 +582,7 @@
<option value="3">RBG</option>
<option value="4">BGR</option>
<option value="5">GBR</option>
<option value="6" hidden disabled>GRB</option>
</select></div>
<div id="dig${s}w" style="display:none">Swap: <select name="WO${s}"><option value="0">None</option><option value="1">W & B</option><option value="2">W & G</option><option value="3">W & R</option><option data-opt="CCT" value="4">WW & CW</option></select></div>
<div id="dig${s}l" style="display:none">Clock: <select name="SP${s}"><option value="0">Slowest</option><option value="1">Slow</option><option value="2">Normal</option><option value="3">Fast</option><option value="4">Fastest</option></select></div>
Expand Down
Loading