Merge pull request #179 from troyhacks/Art-Net-Improvements-v2

Art-Net Improvements
* AsyncUDP instead of WiFiUDP so it's much faster
* Support for professional Art-Net gear with many outputs
* Color-order remapping
* ESP32-P4 SIMD assembly optimization for brightness calc, a bit faster
* Full GUI support for all settings
* GUI guidance for setup
This commit is contained in:
Frank
2024-11-18 13:47:17 +01:00
committed by GitHub
10 changed files with 379 additions and 81 deletions

View File

@@ -448,7 +448,7 @@ uint8_t BusOnOff::getPins(uint8_t* pinArray) const {
}
BusNetwork::BusNetwork(BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWhite) {
BusNetwork::BusNetwork(BusConfig &bc, const ColorOrderMap &com) : Bus(bc.type, bc.start, bc.autoWhite), _colorOrderMap(com) {
_valid = false;
USER_PRINT("[");
switch (bc.type) {
@@ -457,6 +457,11 @@ BusNetwork::BusNetwork(BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWhite) {
_UDPtype = 2;
USER_PRINT("NET_ARTNET_RGB");
break;
case TYPE_NET_ARTNET_RGBW:
_rgbw = true;
_UDPtype = 2;
USER_PRINT("NET_ARTNET_RGBW");
break;
case TYPE_NET_E131_RGB:
_rgbw = false;
_UDPtype = 1;
@@ -469,37 +474,84 @@ BusNetwork::BusNetwork(BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWhite) {
break;
}
_UDPchannels = _rgbw ? 4 : 3;
_data = (byte *)malloc(bc.count * _UDPchannels);
#ifdef ESP32
_data = (byte*) heap_caps_calloc_prefer((bc.count * _UDPchannels)+15, sizeof(byte), 3, MALLOC_CAP_DEFAULT, MALLOC_CAP_SPIRAM);
#else
_data = (byte*) calloc((bc.count * _UDPchannels)+15, sizeof(byte));
#endif
if (_data == nullptr) return;
memset(_data, 0, bc.count * _UDPchannels);
_len = bc.count;
_colorOrder = bc.colorOrder;
_client = IPAddress(bc.pins[0],bc.pins[1],bc.pins[2],bc.pins[3]);
_broadcastLock = false;
_valid = true;
USER_PRINTF(" %u.%u.%u.%u] \n", bc.pins[0],bc.pins[1],bc.pins[2],bc.pins[3]);
_artnet_outputs = bc.artnet_outputs;
_artnet_leds_per_output = bc.artnet_leds_per_output;
_artnet_fps_limit = max(uint8_t(1), bc.artnet_fps_limit);
USER_PRINTF(" %u.%u.%u.%u]\n", bc.pins[0],bc.pins[1],bc.pins[2],bc.pins[3]);
}
void BusNetwork::setPixelColor(uint16_t pix, uint32_t c) {
if (!_valid || pix >= _len) return;
if (hasWhite()) c = autoWhiteCalc(c);
if (_cct >= 1900) c = colorBalanceFromKelvin(_cct, c); //color correction from CCT
uint16_t offset = pix * _UDPchannels;
_data[offset] = R(c);
_data[offset+1] = G(c);
_data[offset+2] = B(c);
if (_rgbw) _data[offset+3] = W(c);
void IRAM_ATTR_YN BusNetwork::setPixelColor(uint16_t pix, uint32_t c) {
if (!_valid || pix >= _len) return;
if (_rgbw) c = autoWhiteCalc(c);
if (_cct >= 1900) c = colorBalanceFromKelvin(_cct, c); // color correction from CCT
uint16_t offset = pix * _UDPchannels;
uint8_t co = _colorOrderMap.getPixelColorOrder(pix + _start, _colorOrder);
if (_colorOrder != co || _colorOrder != COL_ORDER_RGB) {
switch (co) {
case COL_ORDER_GRB:
_data[offset] = G(c); _data[offset+1] = R(c); _data[offset+2] = B(c);
break;
case COL_ORDER_RGB:
_data[offset] = R(c); _data[offset+1] = G(c); _data[offset+2] = B(c);
break;
case COL_ORDER_BRG:
_data[offset] = B(c); _data[offset+1] = R(c); _data[offset+2] = G(c);
break;
case COL_ORDER_RBG:
_data[offset] = R(c); _data[offset+1] = B(c); _data[offset+2] = G(c);
break;
case COL_ORDER_GBR:
_data[offset] = G(c); _data[offset+1] = B(c); _data[offset+2] = R(c);
break;
case COL_ORDER_BGR:
_data[offset] = B(c); _data[offset+1] = G(c); _data[offset+2] = R(c);
break;
}
if (_rgbw) _data[offset+3] = W(c);
} else {
_data[offset] = R(c); _data[offset+1] = G(c); _data[offset+2] = B(c);
if (_rgbw) _data[offset+3] = W(c);
}
}
uint32_t BusNetwork::getPixelColor(uint16_t pix) const {
if (!_valid || pix >= _len) return 0;
uint16_t offset = pix * _UDPchannels;
return RGBW32(_data[offset], _data[offset+1], _data[offset+2], _rgbw ? (_data[offset+3] << 24) : 0);
uint32_t IRAM_ATTR_YN BusNetwork::getPixelColor(uint16_t pix) const {
if (!_valid || pix >= _len) return 0;
uint16_t offset = pix * _UDPchannels;
uint8_t co = _colorOrderMap.getPixelColorOrder(pix + _start, _colorOrder);
uint8_t r = _data[offset + 0];
uint8_t g = _data[offset + 1];
uint8_t b = _data[offset + 2];
uint8_t w = _rgbw ? _data[offset + 3] : 0;
switch (co) {
case COL_ORDER_GRB: return RGBW32(g, r, b, w);
case COL_ORDER_RGB: return RGBW32(r, g, b, w);
case COL_ORDER_BRG: return RGBW32(b, r, g, w);
case COL_ORDER_RBG: return RGBW32(r, b, g, w);
case COL_ORDER_GBR: return RGBW32(g, b, r, w);
case COL_ORDER_BGR: return RGBW32(b, g, r, w);
default: return RGBW32(r, g, b, w); // default to RGB order
}
}
void BusNetwork::show() {
if (!_valid || !canShow()) return;
_broadcastLock = true;
realtimeBroadcast(_UDPtype, _client, _len, _data, _bri, _rgbw);
realtimeBroadcast(_UDPtype, _client, _len, _data, _bri, _rgbw, _artnet_outputs, _artnet_leds_per_output, _artnet_fps_limit);
_broadcastLock = false;
}
@@ -1186,7 +1238,7 @@ int BusManager::add(BusConfig &bc) {
if (getNumBusses() - getNumVirtualBusses() >= WLED_MAX_BUSSES) return -1;
DEBUG_PRINTF("BusManager::add(bc.type=%u)\n", bc.type);
if (bc.type >= TYPE_NET_DDP_RGB && bc.type < 96) {
busses[numBusses] = new BusNetwork(bc);
busses[numBusses] = new BusNetwork(bc, colorOrderMap);
} else if (bc.type >= TYPE_HUB75MATRIX && bc.type <= (TYPE_HUB75MATRIX + 10)) {
#ifdef WLED_ENABLE_HUB75MATRIX
DEBUG_PRINTLN("BusManager::add - Adding BusHub75Matrix");

View File

@@ -54,12 +54,16 @@ struct BusConfig {
uint8_t skipAmount;
bool refreshReq;
uint8_t autoWhite;
uint8_t artnet_outputs, artnet_fps_limit;
uint16_t artnet_leds_per_output;
uint8_t pins[5] = {LEDPIN, 255, 255, 255, 255}; // WLEDMM warning: this means that BusConfig cannot handle nore than 5 pins per bus!
uint16_t frequency;
BusConfig(uint8_t busType, uint8_t* ppins, uint16_t pstart, uint16_t len = 1, uint8_t pcolorOrder = COL_ORDER_GRB, bool rev = false, uint8_t skip = 0, byte aw=RGBW_MODE_MANUAL_ONLY, uint16_t clock_kHz=0U) {
BusConfig(uint8_t busType, uint8_t* ppins, uint16_t pstart, uint16_t len = 1, uint8_t pcolorOrder = COL_ORDER_GRB, bool rev = false, uint8_t skip = 0, byte aw=RGBW_MODE_MANUAL_ONLY, uint16_t clock_kHz=0U, uint8_t art_o=1, uint16_t art_l=1, uint8_t art_f=30) {
refreshReq = (bool) GET_BIT(busType,7);
type = busType & 0x7F; // bit 7 may be/is hacked to include refresh info (1=refresh in off state, 0=no refresh)
count = len; start = pstart; colorOrder = pcolorOrder; reversed = rev; skipAmount = skip; autoWhite = aw; frequency = clock_kHz;
artnet_outputs = art_o; artnet_leds_per_output = art_l; artnet_fps_limit = art_f;
uint8_t nPins = 1; // default = only one pin (clockless LEDs like WS281x)
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
@@ -144,6 +148,9 @@ class Bus {
virtual uint8_t getColorOrder() const { return COL_ORDER_RGB; }
virtual uint8_t skippedLeds() const { return 0; }
virtual uint16_t getFrequency() const { return 0U; }
virtual uint8_t get_artnet_fps_limit() const { return 0; }
virtual uint8_t get_artnet_outputs() const { return 0; }
virtual uint16_t get_artnet_leds_per_output() const { return 0; }
inline uint16_t getStart() const { return _start; }
inline void setStart(uint16_t start) { _start = start; }
inline uint8_t getType() const { return _type; }
@@ -330,7 +337,7 @@ class BusOnOff : public Bus {
class BusNetwork : public Bus {
public:
BusNetwork(BusConfig &bc);
BusNetwork(BusConfig &bc, const ColorOrderMap &com);
uint16_t getMaxPixels() const override { return 4096; };
bool hasRGB() const { return true; }
@@ -348,12 +355,30 @@ class BusNetwork : public Bus {
return !_broadcastLock;
}
uint8_t getPins(uint8_t* pinArray) const;
uint8_t getPins(uint8_t* pinArray) const override;
uint16_t getLength() const override {
uint16_t getLength() const override {
return _len;
}
uint8_t get_artnet_fps_limit() const override {
return _artnet_fps_limit;
}
uint8_t get_artnet_outputs() const override {
return _artnet_outputs;
}
uint16_t get_artnet_leds_per_output() const override {
return _artnet_leds_per_output;
}
void setColorOrder(uint8_t colorOrder);
uint8_t getColorOrder() const override {
return _colorOrder;
}
void cleanup();
~BusNetwork() {
@@ -361,12 +386,17 @@ class BusNetwork : public Bus {
}
private:
IPAddress _client;
uint8_t _UDPtype;
uint8_t _UDPchannels;
bool _rgbw;
bool _broadcastLock;
byte *_data;
IPAddress _client;
uint8_t _UDPtype;
uint8_t _UDPchannels;
bool _rgbw;
bool _broadcastLock;
byte *_data;
uint8_t _colorOrder = COL_ORDER_RGB;
uint8_t _artnet_fps_limit;
uint8_t _artnet_outputs;
uint16_t _artnet_leds_per_output;
const ColorOrderMap &_colorOrderMap;
};
#ifdef WLED_ENABLE_HUB75MATRIX
@@ -474,4 +504,4 @@ class BusManager {
return j;
}
};
#endif
#endif

View File

@@ -194,13 +194,16 @@ bool deserializeConfig(JsonObject doc, bool fromFS) {
uint16_t freqkHz = elm[F("freq")] | 0; // will be in kHz for DotStar and Hz for PWM (not yet implemented fully)
ledType |= refresh << 7; // hack bit 7 to indicate strip requires off refresh
uint8_t AWmode = elm[F("rgbwm")] | RGBW_MODE_MANUAL_ONLY;
uint8_t artnet_outputs = elm["artnet_outputs"] | 1; // sanity check
uint16_t artnet_leds_per_output = elm["artnet_leds_per_output"] | length; // sanity check
uint8_t artnet_fps_limit = elm["artnet_fps_limit"] | 24; // sanity check
if (fromFS) {
BusConfig bc = BusConfig(ledType, pins, start, length, colorOrder, reversed, skipFirst, AWmode, freqkHz);
BusConfig bc = BusConfig(ledType, pins, start, length, colorOrder, reversed, skipFirst, AWmode, freqkHz, artnet_outputs, artnet_leds_per_output, artnet_fps_limit);
mem += BusManager::memUsage(bc);
if (mem <= MAX_LED_MEMORY) if (busses.add(bc) == -1) break; // finalization will be done in WLED::beginStrip()
} else {
if (busConfigs[s] != nullptr) delete busConfigs[s];
busConfigs[s] = new BusConfig(ledType, pins, start, length, colorOrder, reversed, skipFirst, AWmode);
busConfigs[s] = new BusConfig(ledType, pins, start, length, colorOrder, reversed, skipFirst, AWmode, freqkHz, artnet_outputs, artnet_leds_per_output, artnet_fps_limit);
busesChanged = true;
}
s++;
@@ -829,6 +832,9 @@ void serializeConfig() {
ins["ref"] = bus->isOffRefreshRequired();
ins[F("rgbwm")] = bus->getAutoWhiteMode();
ins[F("freq")] = bus->getFrequency();
ins["artnet_outputs"] = bus->get_artnet_outputs();
ins["artnet_fps_limit"] = bus->get_artnet_fps_limit();
ins["artnet_leds_per_output"] = bus->get_artnet_leds_per_output();
}
JsonArray hw_com = hw.createNestedArray(F("com"));

View File

@@ -252,7 +252,8 @@
//Network types (master broadcast) (80-95)
#define TYPE_NET_DDP_RGB 80 //network DDP RGB bus (master broadcast bus)
#define TYPE_NET_E131_RGB 81 //network E131 RGB bus (master broadcast bus, unused)
#define TYPE_NET_ARTNET_RGB 82 //network ArtNet RGB bus (master broadcast bus, unused)
#define TYPE_NET_ARTNET_RGB 82 //network ArtNet RGB bus (master broadcast bus)
#define TYPE_NET_ARTNET_RGBW 83 //network ArtNet RGB bus (master broadcast bus)
#define TYPE_NET_DDP_RGBW 88 //network DDP RGBW bus (master broadcast bus)
#define IS_DIGITAL(t) (((t) & 0x10) || ((t)==TYPE_HUB75MATRIX)) //digital are 16-31 and 48-63 // WLEDMM added HUB75

View File

@@ -224,6 +224,34 @@
gRGBW |= isRGBW = ((t > 17 && t < 22) || (t > 28 && t < 32) || (t > 40 && t < 46 && t != 43) || t == 88); // RGBW checkbox, TYPE_xxxx values from const.h
gId("co"+n).style.display = ((t >= 80 && t < 96) || (t >= 40 && t < 48)||(t >= 100 && t < 110)) ? "none":"inline"; // hide color order for PWM
gId("dig"+n+"w").style.display = (t > 28 && t < 32) ? "inline":"none"; // show swap channels dropdown
gId("dig"+n+"O").style.display = (t == 82 || t == 83) ? "inline":"none"; // show Art-Net output number
gId("dig"+n+"L").style.display = (t == 82 || t == 83) ? "inline":"none"; // show Art-Net LEDs per output
gId("dig"+n+"F").style.display = (t == 82 || t == 83) ? "inline":"none"; // show Art-Net FPS limiter
gId("dig"+n+"W").style.display = (t == 82 || t == 83) ? "inline":"none"; // show Art-Net warnings/info box
d.getElementsByName("AO"+n)[0].min = (t == 82 || t == 83) ? 1 : -1; // make sure these fields do not block saving when hidden
d.getElementsByName("AL"+n)[0].min = (t == 82 || t == 83) ? 1 : -1;
d.getElementsByName("AF"+n)[0].min = (t == 82 || t == 83) ? 1 : -1;
if (gId("dig"+n+"F").style.display == "inline") {
total_leds = d.getElementsByName("LC"+n)[0].value;
outputs = d.getElementsByName("AO"+n)[0].value;
leds_per_output = d.getElementsByName("AL"+n)[0].value;
fps_limit = d.getElementsByName("AF"+n)[0].value;
last_octet = d.getElementsByName("L3"+n)[0].value;
if (outputs > 1) {
if (t == 82) gId("dig"+n+"W").innerHTML = "<br />Set your Art-Net Hardware to "+Math.ceil(leds_per_output/170)+" universes per output.";
if (t == 83) gId("dig"+n+"W").innerHTML = "<br />Set your Art-Net Hardware to "+Math.ceil(leds_per_output/128)+" universes per output.";
} else if (outputs == 1) {
gId("dig"+n+"W").innerHTML = "<br />WLED-style Art-Net output enabled.";
} else {
gId("dig"+n+"W").innerHTML = "<br />You need at least 1 output!";
}
if (outputs > 1 && fps_limit > 33333/leds_per_output) gId("dig"+n+"W").innerHTML += "<br />FPS limit may be too high for WS281x pixels.";
if (outputs*leds_per_output != total_leds) gId("dig"+n+"W").innerHTML += "<br />Total LEDs doesn't match outputs * LEDs per output.";
if (last_octet == 255) {
if (total_leds <= 1024) gId("dig"+n+"W").innerHTML += "<br />Art-Net is in broadcast mode.";
if (total_leds > 1024) gId("dig"+n+"W").innerHTML += "<br />You are sending a lot of broadcast data. Be cautious.";
}
}
if (!(t > 28 && t < 32)) d.getElementsByName("WO"+n)[0].value = 0; // reset swapping
gId("dig"+n+"c").style.display = ((t >= 40 && t < 48)||(t >= 100 && t < 110)) ? "none":"inline"; // hide count for analog and HUB75
gId("dig"+n+"r").style.display = (t >= 80) && (t < 100) ? "none":"inline"; // hide reversed for virtual, except for HUB75
@@ -318,7 +346,6 @@
// update total led count
gId("lc").textContent = sLC;
gId("pc").textContent = (sLC == sPC) ? "":"(" + sPC + " physical)";
// memory usage and warnings
gId('m0').innerHTML = memu;
bquot = memu / maxM * 100;
@@ -425,6 +452,10 @@ ${i+1}:
<span id="p2d${i}"></span><input type="number" name="L2${i}" class="s" onchange="UI()"/>
<span id="p3d${i}"></span><input type="number" name="L3${i}" class="s" onchange="UI()"/>
<span id="p4d${i}"></span><input type="number" name="L4${i}" class="s" onchange="UI()"/>
<div id="dig${i}O" style="display:inline"><br>Number of Outputs: <input type="number" name="AO${i}" min="1" max="255" value="1" oninput="UI()"></div>
<div id="dig${i}L" style="display:inline"><br>LEDs Per Output: <input type="number" name="AL${i}" min="1" max="65535" value="1" oninput="UI()"></div>
<div id="dig${i}F" style="display:inline"><br>FPS Limit: <input type="number" name="AF${i}" min="1" max="255" value="30" oninput="UI()"></div>
<div id="dig${i}W" style="display:inline"><br>Art-Net Warnings Go Here.</div>
<div id="dig${i}r" style="display:inline"><br><span id="rev${i}">Reversed</span>: <input type="checkbox" name="CV${i}"></div>
<div id="dig${i}s" style="display:inline"><br>Skip first LEDs: <input type="number" name="SL${i}" min="0" max="255" value="0" oninput="UI()"></div>
<div id="dig${i}f" style="display:inline"><br><span id="ref${i}">Off Refresh</span>: <input id="rf${i}" type="checkbox" name="RF${i}"></div>

View File

@@ -241,7 +241,7 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply=tru
//udp.cpp
void notify(byte callMode, bool followUp=false);
uint8_t realtimeBroadcast(uint8_t type, IPAddress client, uint16_t length, uint8_t *buffer, uint8_t bri=255, bool isRGBW=false);
uint8_t realtimeBroadcast(uint8_t type, IPAddress client, uint16_t length, uint8_t *buffer, uint8_t bri=255, bool isRGBW=false, uint8_t artnet_outouts=1, uint16_t artnet_leds_per_output=1, uint8_t artnet_fps_limit=1);
void realtimeLock(uint32_t timeoutMs, byte md = REALTIME_MODE_GENERIC);
void exitRealtime();
void handleNotifications();

31
wled00/p4_mul16x16.S Normal file
View File

@@ -0,0 +1,31 @@
#if defined(ARDUINO_ARCH_ESP32P4)
.text
.align 4
.global p4_mul16x16
.type p4_mul16x16,@function
# ESP32-P4 needs -march rv32imafc_zicsr_zifencei_xesppie -mabi ilp32f
# a0 = out_packet, a1 = brightness, a2 = num_loops, a3 = pixelbuffer
p4_mul16x16:
esp.movx.r.cfg t6 # Enable aligned data access
or t6, t6, 2 # Enable aligned data access
esp.movx.w.cfg t6 # Enable aligned data access
li t6, 8 # put 8 (eventually for vmul bitshift) in temp register 6
esp.movx.w.sar t6 # set the numbers of bits to right-shift from t6
li t5, 255 # load 255 into t5 for a comparison
esp.vldbc.8.ip q1, a1, 0 # load the "B" value into q1 from a1, broadcasting the same value to all 16 values of q1
li t1, 0 # start our loop_num counter t1 at 0
loop: # "loop" label
beq t1, a2, exit # branch to "exit" if loop_num == num_loops
esp.vld.128.ip q0, a3, 16 # load 16 "A" values into q0 from a3, then move the pointer by 16 to get a new batch
beq a1, t5, skip # If brightness (a1) == 255, jump to "skip"
esp.vmul.u8 q2, q0, q1 # C = A*B (q2 = q0 * q1) then >> by esp.movx.w.sar which we set to 8
esp.vst.128.ip q2, a0, 16 # store the 16 "C" values into a0, then move the pointer by 16
j end_skip # jump to "end_skip"
skip: # "skip" label
esp.vst.128.ip q0, a0, 16 # just store brightness (q0 from a3) to packet (a0)
end_skip: # "end_skip" label
addi t1, t1, 1 # increment loop_num counter t1
j loop # jump to "loop"
exit: # "exit" label
ret # return
#endif

View File

@@ -95,8 +95,8 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage)
}
}
uint8_t colorOrder, type, skip, awmode, channelSwap;
uint16_t length, start;
uint8_t colorOrder, type, skip, awmode, channelSwap, artnet_outputs, artnet_fps_limit;
uint16_t length, start, artnet_leds_per_output;
uint8_t pins[5] = {255, 255, 255, 255, 255};
autoSegments = request->hasArg(F("MS"));
@@ -110,6 +110,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage)
bool busesChanged = false;
for (uint8_t s = 0; s < WLED_MAX_BUSSES+WLED_MIN_VIRTUAL_BUSSES; s++) {
// "48+s" means the ASCII character "0", so 48+1 = ASCII for "1", etc - and "[3]=0" means null-terminate the string.
char lp[4] = "L0"; lp[2] = 48+s; lp[3] = 0; //ascii 0-9 //strip data pin
char lc[4] = "LC"; lc[2] = 48+s; lc[3] = 0; //strip length
char co[4] = "CO"; co[2] = 48+s; co[3] = 0; //strip color order
@@ -121,6 +122,9 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage)
char aw[4] = "AW"; aw[2] = 48+s; aw[3] = 0; //auto white mode
char wo[4] = "WO"; wo[2] = 48+s; wo[3] = 0; //channel swap
char sp[4] = "SP"; sp[2] = 48+s; sp[3] = 0; //bus clock speed (DotStar & PWM)
char ao[4] = "AO"; ao[2] = 48+s; ao[3] = 0; //Art-Net outputs
char al[4] = "AL"; al[2] = 48+s; al[3] = 0; //Art-Net LEDs per output
char af[4] = "AF"; af[2] = 48+s; af[3] = 0; //Art-Net FPS limit
if (!request->hasArg(lp)) {
DEBUG_PRINT(F("No data for "));
DEBUG_PRINTLN(s);
@@ -167,10 +171,13 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage)
}
channelSwap = Bus::hasWhite(type) ? request->arg(wo).toInt() : 0;
type |= request->hasArg(rf) << 7; // off refresh override
artnet_outputs = (request->hasArg(ao)) ? request->arg(ao).toInt() : 1;
artnet_leds_per_output = (request->hasArg(al)) ? request->arg(al).toInt() : length;
artnet_fps_limit = (request->hasArg(af)) ? request->arg(af).toInt() : 33333/length;
// actual finalization is done in WLED::loop() (removing old busses and adding new)
// this may happen even before this loop is finished so we do "doInitBusses" after the loop
if (busConfigs[s] != nullptr) delete busConfigs[s];
busConfigs[s] = new BusConfig(type, pins, start, length, colorOrder | (channelSwap<<4), request->hasArg(cv), skip, awmode, freqHz);
busConfigs[s] = new BusConfig(type, pins, start, length, colorOrder | (channelSwap<<4), request->hasArg(cv), skip, awmode, freqHz, artnet_outputs, artnet_leds_per_output, artnet_fps_limit);
busesChanged = true;
}
//doInitBusses = busesChanged; // we will do that below to ensure all input data is processed

View File

@@ -759,17 +759,44 @@ void sendSysInfoUDP()
// isRGBW - true if the buffer contains 4 components per pixel
static size_t sequenceNumber = 0; // this needs to be shared across all outputs
static const size_t ART_NET_HEADER_SIZE = 12;
static const byte ART_NET_HEADER[] PROGMEM = {0x41,0x72,0x74,0x2d,0x4e,0x65,0x74,0x00,0x00,0x50,0x00,0x0e};
static const byte ART_NET_HEADER[12] PROGMEM = {0x41,0x72,0x74,0x2d,0x4e,0x65,0x74,0x00,0x00,0x50,0x00,0x0e};
#if defined(ARDUINO_ARCH_ESP32P4)
extern "C" {
int p4_mul16x16(uint8_t* outpacket, uint8_t* brightness, uint16_t num_loops, uint8_t* pixelbuffer);
}
#endif
uint8_t IRAM_ATTR_YN realtimeBroadcast(uint8_t type, IPAddress client, uint16_t length, uint8_t *buffer, uint8_t bri, bool isRGBW, uint8_t outputs, uint16_t leds_per_output, uint8_t fps_limit) {
uint8_t realtimeBroadcast(uint8_t type, IPAddress client, uint16_t length, uint8_t *buffer, uint8_t bri, bool isRGBW) {
if (!(apActive || interfacesInited) || !client[0] || !length) return 1; // network not initialised or dummy/unset IP address 031522 ajn added check for ap
WiFiUDP ddpUdp;
// For some reason, this is faster outside of the case block...
//
#ifdef ESP32
static byte *packet_buffer = (byte *) heap_caps_calloc_prefer(530, sizeof(byte), 2, MALLOC_CAP_DEFAULT, MALLOC_CAP_SPIRAM);
#else
static byte *packet_buffer = (byte *) calloc(530, sizeof(byte));
#endif
if (packet_buffer[0] != 0x41) memcpy(packet_buffer, ART_NET_HEADER, 12); // copy in the Art-Net header if it isn't there already
// Volumetric test code
// static byte *buffer = (byte *) heap_caps_calloc_prefer(length*3*72, sizeof(byte), 3, MALLOC_CAP_IRAM_8BIT, MALLOC_CAP_SPIRAM, MALLOC_CAP_DEFAULT); // MALLOC_CAP_TCM seems to have alignment issues.
// memmove(buffer+(length*3),buffer,length*3*7);
// memcpy(buffer,buffer_in,length*3);
// framenumber++;
// if (framenumber >= 8) {
// framenumber = 0;
// } else {
// // return 0;
// }
// length *= 8;
switch (type) {
case 0: // DDP
{
WiFiUDP ddpUdp;
// calculate the number of UDP packets we need to send
size_t channelCount = length * (isRGBW? 4:3); // 1 channel for every R,G,B value
size_t packetCount = ((channelCount-1) / DDP_CHANNELS_PER_PACKET) +1;
@@ -835,61 +862,167 @@ uint8_t realtimeBroadcast(uint8_t type, IPAddress client, uint16_t length, uint8
case 1: //E1.31
{
} break;
case 2: //ArtNet
case 2: //Art-Net
{
// calculate the number of UDP packets we need to send
const size_t channelCount = length * (isRGBW?4:3); // 1 channel for every R,G,B,(W?) value
const size_t ARTNET_CHANNELS_PER_PACKET = isRGBW?512:510; // 512/4=128 RGBW LEDs, 510/3=170 RGB LEDs
const size_t packetCount = ((channelCount-1)/ARTNET_CHANNELS_PER_PACKET)+1;
static unsigned long artnetlimiter = micros()+(1000000/fps_limit);
while (artnetlimiter > micros()) {
delayMicroseconds(10); // Make WLED obey fps_limit and just delay here until we're ready to send a frame.
}
uint32_t channel = 0;
size_t bufferOffset = 0;
/*
WLED rendering Art-Net data considers itself to be 1 hardware output with many universes - but
many Art-Net controllers like the H807SA can be manually set to "X universes per output" or in
some cases "X channels per port" - which is the same thing, just expressed differently.
We need to know the LEDs per output so we can break the pixel data across physically attached universes.
The H807SA obeys the "510 channels for RGB" rule like WLED and xLights - some other controllers do not care,
but we're not supporting those here. If you run into one of these, override ARTNET_CHANNELS_PER_PACKET to 512.
*/
#ifdef ARTNET_TIMER
uint_fast16_t datatotal = 0;
uint_fast16_t packetstotal = 0;
#endif
unsigned long timer = micros();
AsyncUDP artnetudp;// AsyncUDP so we can just blast packets.
const uint_fast16_t ARTNET_CHANNELS_PER_PACKET = isRGBW?512:510; // 512/4=128 RGBW LEDs, 510/3=170 RGB LEDs
uint_fast16_t bufferOffset = 0;
uint_fast16_t hardware_output_universe = 0;
sequenceNumber++;
for (size_t currentPacket = 0; currentPacket < packetCount; currentPacket++) {
if (sequenceNumber == 0 || sequenceNumber > 255) sequenceNumber = 1;
if (sequenceNumber > 255) sequenceNumber = 0;
if (!ddpUdp.beginPacket(client, ARTNET_DEFAULT_PORT)) {
DEBUG_PRINTLN(F("Art-Net WiFiUDP.beginPacket returned an error"));
return 1; // borked
for (uint_fast16_t hardware_output = 0; hardware_output < outputs; hardware_output++) {
if (bufferOffset > length * (isRGBW?4:3)) {
// This stop is reached if we don't have enough pixels for the defined Art-Net output.
return 1; // stop when we hit end of LEDs
}
size_t packetSize = ARTNET_CHANNELS_PER_PACKET;
uint_fast16_t channels_remaining = leds_per_output * (isRGBW?4:3);
if (currentPacket == (packetCount - 1U)) {
// last packet
if (channelCount % ARTNET_CHANNELS_PER_PACKET) {
packetSize = channelCount % ARTNET_CHANNELS_PER_PACKET;
while (channels_remaining > 0) {
uint_fast16_t packetSize = ARTNET_CHANNELS_PER_PACKET;
if (channels_remaining < ARTNET_CHANNELS_PER_PACKET) {
packetSize = channels_remaining;
channels_remaining = 0;
} else {
channels_remaining -= packetSize;
}
#ifdef ARTNET_TIMER
packetstotal++;
datatotal += packetSize + 18;
#endif
// set the parts of the Art-Net packet header that change:
packet_buffer[12] = sequenceNumber;
// packet_buffer[13] = 0; // "The physical input port from which DMX512 data was input. This field is used by the receiving device to discriminate between packets with identical Port-Address that have been generated by different input ports and so need to be merged."
packet_buffer[14] = hardware_output_universe;
packet_buffer[15] = hardware_output_universe >> 8; // needed for universes > 255
packet_buffer[16] = packetSize >> 8;
packet_buffer[17] = packetSize;
#ifdef ARTNET_TESTING_ZEROS
bri = 0; // Set all brightness to 0 but keep all calculations the same and keep sending packets.
#endif
#if defined(ARDUINO_ARCH_ESP32P4)
p4_mul16x16(packet_buffer+18, &bri, (packetSize >> 4)+1, buffer+bufferOffset);
#else
if (bri == 255) { // speed hack - don't adjust brightness if full brightness
memcpy(packet_buffer+18, buffer+bufferOffset, packetSize);
} else {
for (uint_fast16_t i = 0; i < packetSize; i+=(isRGBW?4:3)) {
// set brightness values in the packet - seems slightly faster than scale8()?
// for some reason, doing 3 (or 4) at a time is 200 micros faster than 1 at a time.
packet_buffer[i+18] = (buffer[bufferOffset+i] * bri) >> 8;
packet_buffer[i+19] = (buffer[bufferOffset+i+1] * bri) >> 8;
packet_buffer[i+20] = (buffer[bufferOffset+i+2] * bri) >> 8;
if (isRGBW) packet_buffer[i+21] = (buffer[bufferOffset+i+3] * bri) >> 8;
}
}
#endif
bufferOffset += packetSize;
if (!artnetudp.writeTo(packet_buffer,packetSize+18, client, ARTNET_DEFAULT_PORT)) {
DEBUG_PRINTLN(F("Art-Net artnetudp.writeTo() returned an error"));
return 1; // borked
}
hardware_output_universe++;
}
}
byte header_buffer[ART_NET_HEADER_SIZE];
memcpy_P(header_buffer, ART_NET_HEADER, ART_NET_HEADER_SIZE);
ddpUdp.write(header_buffer, ART_NET_HEADER_SIZE); // This doesn't change. Hard coded ID, OpCode, and protocol version.
ddpUdp.write(sequenceNumber & 0xFF); // sequence number. 1..255
ddpUdp.write(0x00); // physical - more an FYI, not really used for anything. 0..3
ddpUdp.write((currentPacket) & 0xFF); // Universe LSB. 1 full packet == 1 full universe, so just use current packet number.
ddpUdp.write(0x00); // Universe MSB, unused.
ddpUdp.write(0xFF & (packetSize >> 8)); // 16-bit length of channel data, MSB
ddpUdp.write(0xFF & (packetSize )); // 16-bit length of channel data, LSB
// Send Art-Net sync. Just reuse the packet and adjust.
// This should get re-written on the next run.
// After the first sync packet, and assuming 1 sync packet every 4
// seconds at least, should keep Art-Net nodes in synchronous mode.
for (size_t i = 0; i < packetSize; i += (isRGBW?4:3)) {
ddpUdp.write(scale8(buffer[bufferOffset++], bri)); // R
ddpUdp.write(scale8(buffer[bufferOffset++], bri)); // G
ddpUdp.write(scale8(buffer[bufferOffset++], bri)); // B
if (isRGBW) ddpUdp.write(scale8(buffer[bufferOffset++], bri)); // W
}
// This is very much untested and generally not needed unless you
// have several Art-Net devices being broadcast to, and should only
// be called in that situation.
// Art-Net broadcast mode (setting Art-Net to 255.255.255.255) should ONLY
// be used if you know what you're doing, as that is a lot of pixels being
// sent to EVERYTHING on your network, including WiFi devices - and can
// overwhelm them if you have a lot of Art-Net data being broadcast.
if (!ddpUdp.endPacket()) {
DEBUG_PRINTLN(F("Art-Net WiFiUDP.endPacket returned an error"));
#ifdef ARTNET_SYNC_ENABLED
// This block sends Art-Net "ArtSync" packets. Can't do this with AsyncUDP because it doesn't support source port binding.
// Tested on Art-Net qualifier software but not on real hardware with known support for ArtSync.
// Doesn't seem to do anything on my gear, so it's disabled.
// packet_buffer[8] = 0x00; // ArtSync opcode low byte (low byte is same as ArtDmx, 0x00)
packet_buffer[9] = 0x52; // ArtSync opcode high byte
packet_buffer[12] = 0x00; // Aux1 - Transmit as 0. This is normally the sequence number in ArtDMX packets.
// packet_buffer[13] = 0x00; // Aux2 - Transmit as 0 - this should be 0 anyway in the packet already
#ifdef ARTNET_SYNC_STRICT
WiFiUDP artnetsync;
artnetsync.begin(ETH.localIP(), ARTNET_DEFAULT_PORT);
artnetsync.beginPacket(IPADDR_BROADCAST,ARTNET_DEFAULT_PORT);
artnetsync.write(packet_buffer,14);
if (!artnetsync.endPacket()) {
DEBUG_PRINTLN(F("Art-Net Sync Broadcast Strict returned an error"));
return 1; // borked
}
channel += packetSize;
}
} break;
#else
if (!artnetudp.broadcastTo(packet_buffer,14,ARTNET_DEFAULT_PORT)) {
DEBUG_PRINTLN(F("Art-Net Sync Broadcast returned an error"));
return 1; // borked
}
#endif
packet_buffer[9] = ART_NET_HEADER[9]; // reset ArtSync opcode high byte
#ifdef ARTNET_TIMER
packetstotal++;
datatotal += 14;
#endif
#endif
artnetlimiter = timer + (1000000/fps_limit);
// This is the proper stop if pixels = Art-Net output.
#ifdef ARTNET_TIMER
float mbps = (datatotal*8)/((micros()-timer)*0.95367431640625f);
// the "micros()" calc is just to limit the print to a more random debug output so it doesn't overwhelm the terminal
if (micros() % 100 < 3) USER_PRINTF("UDP for %u pixels took %lu micros. %u data in %u total packets. %2.2f mbit/sec at %u FPS.\n",length, micros()-timer, datatotal, packetstotal, mbps, strip.getFps());
#endif
break;
}
}
return 0;
}

View File

@@ -424,6 +424,7 @@ void getSettingsJS(AsyncWebServerRequest* request, byte subPage, char* dest) //W
for (uint8_t s=0; s < busses.getNumBusses(); s++) {
Bus* bus = busses.getBus(s);
if (bus == nullptr) continue;
// "48+s" means the ASCII character "0", so 48+1 = ASCII for "1", etc - and "[3]=0" means null-terminate the string.
char lp[4] = "L0"; lp[2] = 48+s; lp[3] = 0; //ascii 0-9 //strip data pin
char lc[4] = "LC"; lc[2] = 48+s; lc[3] = 0; //strip length
char co[4] = "CO"; co[2] = 48+s; co[3] = 0; //strip color order
@@ -435,6 +436,9 @@ void getSettingsJS(AsyncWebServerRequest* request, byte subPage, char* dest) //W
char aw[4] = "AW"; aw[2] = 48+s; aw[3] = 0; //auto white mode
char wo[4] = "WO"; wo[2] = 48+s; wo[3] = 0; //swap channels
char sp[4] = "SP"; sp[2] = 48+s; sp[3] = 0; //bus clock speed
char ao[4] = "AO"; ao[2] = 48+s; ao[3] = 0; //Art-Net outputs
char al[4] = "AL"; al[2] = 48+s; al[3] = 0; //Art-Net LEDs per output
char af[4] = "AF"; af[2] = 48+s; af[3] = 0; //Art-Net FPS limit
oappend(SET_F("addLEDs(1);"));
uint8_t pins[5];
uint8_t nPins = bus->getPins(pins);
@@ -451,6 +455,9 @@ void getSettingsJS(AsyncWebServerRequest* request, byte subPage, char* dest) //W
sappend('c',rf,bus->isOffRefreshRequired());
sappend('v',aw,bus->getAutoWhiteMode());
sappend('v',wo,bus->getColorOrder() >> 4);
sappend('v',ao,bus->get_artnet_outputs());
sappend('v',al,bus->get_artnet_leds_per_output());
sappend('v',af,bus->get_artnet_fps_limit());
uint16_t speed = bus->getFrequency();
if (bus->getType() > TYPE_ONOFF && bus->getType() < 48) {
switch (speed) {