diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index aea8051afa..053d595783 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -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; } @@ -1178,7 +1230,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"); diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index 9259bd1ff4..75c43cafc8 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -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 @@ -473,4 +503,4 @@ class BusManager { return j; } }; -#endif \ No newline at end of file +#endif diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index 4de49f99af..069b303aa0 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -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")); diff --git a/wled00/const.h b/wled00/const.h index 62fbfb624a..2c6ac1a71b 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -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 diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index b50e8ecba2..a39629b59e 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -219,6 +219,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 = "
Set your Art-Net Hardware to "+Math.ceil(leds_per_output/170)+" universes per output."; + if (t == 83) gId("dig"+n+"W").innerHTML = "
Set your Art-Net Hardware to "+Math.ceil(leds_per_output/128)+" universes per output."; + } else if (outputs == 1) { + gId("dig"+n+"W").innerHTML = "
WLED-style Art-Net output enabled."; + } else { + gId("dig"+n+"W").innerHTML = "
You need at least 1 output!"; + } + if (outputs > 1 && fps_limit > 33333/leds_per_output) gId("dig"+n+"W").innerHTML += "
FPS limit may be too high for WS281x pixels."; + if (outputs*leds_per_output != total_leds) gId("dig"+n+"W").innerHTML += "
Total LEDs doesn't match outputs * LEDs per output."; + if (last_octet == 255) { + if (total_leds <= 1024) gId("dig"+n+"W").innerHTML += "
Art-Net is in broadcast mode."; + if (total_leds > 1024) gId("dig"+n+"W").innerHTML += "
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 @@ -313,7 +341,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; @@ -420,6 +447,10 @@ +

Number of Outputs:
+

LEDs Per Output:
+

FPS Limit:
+

Art-Net Warnings Go Here.

Reversed:

Skip first LEDs:

Off Refresh:
diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 694e9c9f55..a2f1bf29a3 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -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(); diff --git a/wled00/p4_mul16x16.S b/wled00/p4_mul16x16.S new file mode 100644 index 0000000000..ea91165a11 --- /dev/null +++ b/wled00/p4_mul16x16.S @@ -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 diff --git a/wled00/set.cpp b/wled00/set.cpp index 6c9b57c273..449155e27a 100644 --- a/wled00/set.cpp +++ b/wled00/set.cpp @@ -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 diff --git a/wled00/udp.cpp b/wled00/udp.cpp index 0a2d53324b..6dd01a3bdf 100644 --- a/wled00/udp.cpp +++ b/wled00/udp.cpp @@ -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. - sequenceNumber++; + We need to know the LEDs per output so we can break the pixel data across physically attached universes. - for (size_t currentPacket = 0; currentPacket < packetCount; currentPacket++) { + 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(); - if (sequenceNumber > 255) sequenceNumber = 0; + AsyncUDP artnetudp;// AsyncUDP so we can just blast packets. - if (!ddpUdp.beginPacket(client, ARTNET_DEFAULT_PORT)) { - DEBUG_PRINTLN(F("Art-Net WiFiUDP.beginPacket returned an error")); - return 1; // borked + 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++; + + if (sequenceNumber == 0 || sequenceNumber > 255) sequenceNumber = 1; + + 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; } - } - 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 + #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 - 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 + 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++; } + } - if (!ddpUdp.endPacket()) { - DEBUG_PRINTLN(F("Art-Net WiFiUDP.endPacket returned an error")); + // 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. + + // 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. + + #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; } diff --git a/wled00/xml.cpp b/wled00/xml.cpp index b90b7bb71c..b7bd1005e9 100644 --- a/wled00/xml.cpp +++ b/wled00/xml.cpp @@ -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) {