diff --git a/wled00/e131.cpp b/wled00/e131.cpp index 6f7f193bdc..98e397f403 100644 --- a/wled00/e131.cpp +++ b/wled00/e131.cpp @@ -58,6 +58,10 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ if (protocol == P_ARTNET) { + if (p->art_opcode == ARTNET_OPCODE_OPPOLL) { + handleArtnetPollReply(clientIP); + return; + } uni = p->art_universe; dmxChannels = htons(p->art_length); e131_data = p->art_data; @@ -214,3 +218,197 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ e131NewData = true; } + +void handleArtnetPollReply(IPAddress ipAddress) { + ArtPollReply artnetPollReply; + prepareArtnetPollReply(&artnetPollReply); + + uint16_t startUniverse = e131Universe; + uint16_t endUniverse = e131Universe; + + switch (DMXMode) { + case DMX_MODE_DISABLED: + return; // nothing to do + break; + + case DMX_MODE_SINGLE_RGB: + case DMX_MODE_SINGLE_DRGB: + case DMX_MODE_EFFECT: + break; // 1 universe is enough + + case DMX_MODE_MULTIPLE_DRGB: + case DMX_MODE_MULTIPLE_RGB: + case DMX_MODE_MULTIPLE_RGBW: + { + bool is4Chan = (DMXMode == DMX_MODE_MULTIPLE_RGBW); + const uint16_t dmxChannelsPerLed = is4Chan ? 4 : 3; + const uint16_t dimmerOffset = (DMXMode == DMX_MODE_MULTIPLE_DRGB) ? 1 : 0; + const uint16_t dmxLenOffset = (DMXAddress == 0) ? 0 : 1; // For legacy DMX start address 0 + const uint16_t ledsInFirstUniverse = (((MAX_CHANNELS_PER_UNIVERSE - DMXAddress) + dmxLenOffset) - dimmerOffset) / dmxChannelsPerLed; + const uint16_t totalLen = strip.getLengthTotal(); + + if (totalLen > ledsInFirstUniverse) { + const uint16_t ledsPerUniverse = is4Chan ? MAX_4_CH_LEDS_PER_UNIVERSE : MAX_3_CH_LEDS_PER_UNIVERSE; + const uint16_t remainLED = totalLen - ledsInFirstUniverse; + + endUniverse += (remainLED / ledsPerUniverse); + + if ((remainLED % ledsPerUniverse) > 0) { + endUniverse++; + } + + if ((endUniverse - startUniverse) > E131_MAX_UNIVERSE_COUNT) { + endUniverse = startUniverse + E131_MAX_UNIVERSE_COUNT - 1; + } + } + break; + } + default: + DEBUG_PRINTLN(F("unknown E1.31 DMX mode")); + return; // nothing to do + break; + } + + for (uint16_t i = startUniverse; i <= endUniverse; ++i) { + sendArtnetPollReply(&artnetPollReply, ipAddress, i); + } +} + +void prepareArtnetPollReply(ArtPollReply *reply) { + // Art-Net + reply->reply_id[0] = 0x41; + reply->reply_id[1] = 0x72; + reply->reply_id[2] = 0x74; + reply->reply_id[3] = 0x2d; + reply->reply_id[4] = 0x4e; + reply->reply_id[5] = 0x65; + reply->reply_id[6] = 0x74; + reply->reply_id[7] = 0x00; + + reply->reply_opcode = ARTNET_OPCODE_OPPOLLREPLY; + + IPAddress localIP = Network.localIP(); + for (uint8_t i = 0; i < 4; i++) { + reply->reply_ip[i] = localIP[i]; + } + + reply->reply_port = ARTNET_DEFAULT_PORT; + + char * numberEnd = versionString; + reply->reply_version_h = (uint8_t)strtol(numberEnd, &numberEnd, 10); + numberEnd++; + reply->reply_version_l = (uint8_t)strtol(numberEnd, &numberEnd, 10); + + // Switch values depend on universe, set before sending + reply->reply_net_sw = 0x00; + reply->reply_sub_sw = 0x00; + + reply->reply_oem_h = 0x00; // TODO add assigned oem code + reply->reply_oem_l = 0x00; + + reply->reply_ubea_ver = 0x00; + + // Indicators in Normal Mode + // All or part of Port-Address programmed by network or Web browser + reply->reply_status_1 = 0xE0; + + reply->reply_esta_man = 0x0000; + + strlcpy((char *)(reply->reply_short_name), serverDescription, 18); + strlcpy((char *)(reply->reply_long_name), serverDescription, 64); + + reply->reply_node_report[0] = '\0'; + + reply->reply_num_ports_h = 0x00; + reply->reply_num_ports_l = 0x01; // One output port + + reply->reply_port_types[0] = 0x80; // Output DMX data + reply->reply_port_types[1] = 0x00; + reply->reply_port_types[2] = 0x00; + reply->reply_port_types[3] = 0x00; + + // No inputs + reply->reply_good_input[0] = 0x00; + reply->reply_good_input[1] = 0x00; + reply->reply_good_input[2] = 0x00; + reply->reply_good_input[3] = 0x00; + + // One output + reply->reply_good_output_a[0] = 0x80; // Data is being transmitted + reply->reply_good_output_a[1] = 0x00; + reply->reply_good_output_a[2] = 0x00; + reply->reply_good_output_a[3] = 0x00; + + // Values depend on universe, set before sending + reply->reply_sw_in[0] = 0x00; + reply->reply_sw_in[1] = 0x00; + reply->reply_sw_in[2] = 0x00; + reply->reply_sw_in[3] = 0x00; + + // Values depend on universe, set before sending + reply->reply_sw_out[0] = 0x00; + reply->reply_sw_out[1] = 0x00; + reply->reply_sw_out[2] = 0x00; + reply->reply_sw_out[3] = 0x00; + + reply->reply_sw_video = 0x00; + reply->reply_sw_macro = 0x00; + reply->reply_sw_remote = 0x00; + + reply->reply_spare[0] = 0x00; + reply->reply_spare[1] = 0x00; + reply->reply_spare[2] = 0x00; + + // A DMX to / from Art-Net device + reply->reply_style = 0x00; + + Network.localMAC(reply->reply_mac); + + for (uint8_t i = 0; i < 4; i++) { + reply->reply_bind_ip[i] = localIP[i]; + } + + reply->reply_bind_index = 1; + + // Product supports web browser configuration + // Node’s IP is DHCP or manually configured + // Node is DHCP capable + // Node supports 15 bit Port-Address (Art-Net 3 or 4) + // Node is able to switch between ArtNet and sACN + reply->reply_status_2 = (staticIP[0] == 0) ? 0x1F : 0x1D; + + // RDM is disabled + // Output style is continuous + reply->reply_good_output_b[0] = 0xC0; + reply->reply_good_output_b[1] = 0xC0; + reply->reply_good_output_b[2] = 0xC0; + reply->reply_good_output_b[3] = 0xC0; + + // Fail-over state: Hold last state + // Node does not support fail-over + reply->reply_status_3 = 0x00; + + for (uint8_t i = 0; i < 21; i++) { + reply->reply_filler[i] = 0x00; + } +} + +void sendArtnetPollReply(ArtPollReply *reply, IPAddress ipAddress, uint16_t portAddress) { + reply->reply_net_sw = (uint8_t)((portAddress >> 8) & 0x007F); + reply->reply_sub_sw = (uint8_t)((portAddress >> 4) & 0x000F); + reply->reply_sw_out[0] = (uint8_t)(portAddress & 0x000F); + + sprintf((char *)reply->reply_node_report, "#0001 [%04u] OK - WLED v" TOSTRING(WLED_VERSION), pollReplyCount); + + if (pollReplyCount < 9999) { + pollReplyCount++; + } else { + pollReplyCount = 0; + } + + notifierUdp.beginPacket(ipAddress, ARTNET_DEFAULT_PORT); + notifierUdp.write(reply->raw, sizeof(ArtPollReply)); + notifierUdp.endPacket(); + + reply->reply_bind_index++; +} \ No newline at end of file diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 5762708975..5417e7a48b 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -80,6 +80,9 @@ void handleDMX(); //e131.cpp void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol); +void handleArtnetPollReply(IPAddress ipAddress); +void prepareArtnetPollReply(ArtPollReply* reply); +void sendArtnetPollReply(ArtPollReply* reply, IPAddress ipAddress, uint16_t portAddress); //file.cpp bool handleFileRead(AsyncWebServerRequest*, String path); diff --git a/wled00/src/dependencies/e131/ESPAsyncE131.cpp b/wled00/src/dependencies/e131/ESPAsyncE131.cpp index b4065b06fc..75d6b8dc29 100644 --- a/wled00/src/dependencies/e131/ESPAsyncE131.cpp +++ b/wled00/src/dependencies/e131/ESPAsyncE131.cpp @@ -110,8 +110,8 @@ void ESPAsyncE131::parsePacket(AsyncUDPPacket _packet) { if (protocol == P_ARTNET) { if (memcmp(sbuff->art_id, ESPAsyncE131::ART_ID, sizeof(sbuff->art_id))) error = true; //not "Art-Net" - if (sbuff->art_opcode != ARTNET_OPCODE_OPDMX) - error = true; //not a DMX packet + if (sbuff->art_opcode != ARTNET_OPCODE_OPDMX && sbuff->art_opcode != ARTNET_OPCODE_OPPOLL) + error = true; //not a DMX or poll packet } else { //E1.31 error handling if (htonl(sbuff->root_vector) != ESPAsyncE131::VECTOR_ROOT) error = true; diff --git a/wled00/src/dependencies/e131/ESPAsyncE131.h b/wled00/src/dependencies/e131/ESPAsyncE131.h index 4cf522d8a9..66b2ee9a7b 100644 --- a/wled00/src/dependencies/e131/ESPAsyncE131.h +++ b/wled00/src/dependencies/e131/ESPAsyncE131.h @@ -54,6 +54,8 @@ typedef struct ip_addr ip4_addr_t; #define DDP_TIMECODE_FLAG 0x10 #define ARTNET_OPCODE_OPDMX 0x5000 +#define ARTNET_OPCODE_OPPOLL 0x2000 +#define ARTNET_OPCODE_OPPOLLREPLY 0x2100 #define P_E131 0 #define P_ARTNET 1 @@ -151,6 +153,48 @@ typedef union { uint8_t raw[1458]; } e131_packet_t; +typedef union { + struct { + uint8_t reply_id[8]; + uint16_t reply_opcode; + uint8_t reply_ip[4]; + uint16_t reply_port; + uint8_t reply_version_h; + uint8_t reply_version_l; + uint8_t reply_net_sw; + uint8_t reply_sub_sw; + uint8_t reply_oem_h; + uint8_t reply_oem_l; + uint8_t reply_ubea_ver; + uint8_t reply_status_1; + uint16_t reply_esta_man; + uint8_t reply_short_name[18]; + uint8_t reply_long_name[64]; + uint8_t reply_node_report[64]; + uint8_t reply_num_ports_h; + uint8_t reply_num_ports_l; + uint8_t reply_port_types[4]; + uint8_t reply_good_input[4]; + uint8_t reply_good_output_a[4]; + uint8_t reply_sw_in[4]; + uint8_t reply_sw_out[4]; + uint8_t reply_sw_video; + uint8_t reply_sw_macro; + uint8_t reply_sw_remote; + uint8_t reply_spare[3]; + uint8_t reply_style; + uint8_t reply_mac[6]; + uint8_t reply_bind_ip[4]; + uint8_t reply_bind_index; + uint8_t reply_status_2; + uint8_t reply_good_output_b[4]; + uint8_t reply_status_3; + uint8_t reply_filler[21]; + } __attribute__((packed)); + + uint8_t raw[239]; +} ArtPollReply; + // new packet callback typedef void (*e131_packet_callback_function) (e131_packet_t* p, IPAddress clientIP, byte protocol); diff --git a/wled00/src/dependencies/network/Network.cpp b/wled00/src/dependencies/network/Network.cpp index 38ff70df0c..d86bf127fd 100644 --- a/wled00/src/dependencies/network/Network.cpp +++ b/wled00/src/dependencies/network/Network.cpp @@ -43,6 +43,34 @@ IPAddress NetworkClass::gatewayIP() return INADDR_NONE; } +void NetworkClass::localMAC(uint8_t* MAC) +{ +#if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET) + // ETH.macAddress(MAC); // Does not work because of missing ETHClass:: in ETH.ccp + + // Start work around + String macString = ETH.macAddress(); + char macChar[18]; + char * octetEnd = macChar; + + strlcpy(macChar, macString.c_str(), 18); + + for (uint8_t i = 0; i < 6; i++) { + MAC[i] = (uint8_t)strtol(octetEnd, &octetEnd, 16); + octetEnd++; + } + // End work around + + for (uint8_t i = 0; i < 6; i++) { + if (MAC[i] != 0x00) { + return; + } + } +#endif + WiFi.macAddress(MAC); + return; +} + bool NetworkClass::isConnected() { #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET) diff --git a/wled00/src/dependencies/network/Network.h b/wled00/src/dependencies/network/Network.h index 4dd8678adc..9201d514ea 100644 --- a/wled00/src/dependencies/network/Network.h +++ b/wled00/src/dependencies/network/Network.h @@ -14,6 +14,7 @@ class NetworkClass IPAddress localIP(); IPAddress subnetMask(); IPAddress gatewayIP(); + void localMAC(uint8_t* MAC); bool isConnected(); bool isEthernet(); }; diff --git a/wled00/wled.h b/wled00/wled.h index 11866f0461..1785a1f1a9 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -358,6 +358,7 @@ WLED_GLOBAL byte DMXOldDimmer _INIT(0); // only update WLED_GLOBAL byte e131LastSequenceNumber[E131_MAX_UNIVERSE_COUNT]; // to detect packet loss WLED_GLOBAL bool e131Multicast _INIT(false); // multicast or unicast WLED_GLOBAL bool e131SkipOutOfSequence _INIT(false); // freeze instead of flickering +WLED_GLOBAL uint16_t pollReplyCount _INIT(0); // count number of replies for ArtPoll node report WLED_GLOBAL bool mqttEnabled _INIT(false); WLED_GLOBAL char mqttDeviceTopic[33] _INIT(""); // main MQTT topic (individual per device, default is wled/mac)