/* xdrv_04_light_artnet.ino - Converter functions for lights Copyright (C) 2020 Stephan Hadinger & Theo Arends This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifdef USE_LIGHT #ifdef USE_LIGHT_ARTNET #ifndef USE_WS2812 #define USE_WS2812 // needed since USE_LIGHT_ARTNET is enabled for ESP32 by default #endif #ifndef WS2812_ARTNET_UDP_BUFFER_SIZE #define WS2812_ARTNET_UDP_BUFFER_SIZE 140 // Max 30 columns with 4 bytes per pixel #endif #ifndef WS2812_ARTNET_UDP_MAX_PACKETS #define WS2812_ARTNET_UDP_MAX_PACKETS 30 // Max 30 rows (packets consecutive) #endif #ifndef WS2812_ARTNET_MAX_SLEEP #define WS2812_ARTNET_MAX_SLEEP 5 // sleep at most 5ms #endif typedef struct { uint8_t rows = 1; // number of rows (min:1) uint8_t cols = 0; // number of columns (if cols == 0 then apply to the entire light) uint8_t offs = 0; // offset in the led strip where the matrix starts (min: 0) bool alt = false; // are the rows in alternate directions uint16_t univ = 0; // start at universe number (+1) uint16_t port = 6454; // UDP port number uint8_t dimm = 100; // Dimmer 0..100 bool on = true; bool matrix = true; // true if light is a WS2812 matrix, false if single light // metrics uint32_t packet_received = 0; uint32_t packet_accepted = 0; uint32_t strip_refresh = 0; } ArtNetConfig; uint32_t * packets_per_row = nullptr; ArtNetConfig artnet_conf; #ifdef ESP8266 #include "UdpListener.h" UdpListener * ArtNetUdp = nullptr; #else WiFiUDP * ArtNetUdp; #endif bool artnet_udp_connected = false; // IPAddress artnet_udp_remote_ip; // remote IP address // uint16_t artnet_udp_remote_port; // remote port /*********************************************************************************************\ * ArtNet support \*********************************************************************************************/ void ArtNetLoadSettings(void) { // read settings and copy locally artnet_conf.dimm = Settings->light_dimmer; artnet_conf.cols = Settings->light_step_pixels; artnet_conf.rows = (artnet_conf.cols != 0) ? Settings->light_pixels / artnet_conf.cols : 0; artnet_conf.offs = Settings->light_rotation; artnet_conf.alt = Settings->flag.ws_clock_reverse; // SetOption16 artnet_conf.univ = Settings->artnet_universe; artnet_conf.on = (Light.power & 1); ArtNetValidate(); } // validate that parameters in artnet_conf are in valid ranges void ArtNetValidate(void) { if (artnet_conf.dimm > 100) { artnet_conf.dimm = 100; } if (artnet_conf.cols == 0 || artnet_conf.rows == 0) { artnet_conf.rows = 1; } // if single light, both are supposed to be 0 artnet_conf.matrix = (artnet_conf.cols > 0) && Ws2812StripConfigured(); if (artnet_conf.univ > 32767) { artnet_conf.univ = 0; } if (artnet_conf.port == 0) { artnet_conf.port = 6454; } } void ArtNetSaveSettings(void) { ArtNetValidate(); // write to settings Settings->light_dimmer = artnet_conf.dimm; Settings->light_step_pixels = artnet_conf.cols; if (artnet_conf.cols > 0) { Settings->light_pixels = artnet_conf.rows * artnet_conf.cols; } Settings->light_rotation = artnet_conf.offs; Settings->artnet_universe = artnet_conf.univ; Settings->flag.ws_clock_reverse = artnet_conf.alt; // SetOption16 } bool ArtNetSetChannels(void) { if (artnet_udp_connected && ArtNetUdp != nullptr) { // ArtNet is running if (artnet_conf.matrix) { if (Light.power & 1) { return true; } // serviced, don't cascade to WS2812 } else { return false; // if regular bulb, cascade change } } else if (Settings->flag6.artnet_autorun) { // ArtNet is not running but is planned to get running return true; // if ArtNet autorun is own but has not started yet, block update to lights } return false; } // process ArtNet packet // returns `true` if strip is dirty, i.e. we changed the value of some leds void ArtNetProcessPacket(uint8_t * buf, size_t len) { artnet_conf.packet_received++; if (buf == nullptr || len <= 18) { return; } // is the signature correct? // 4172742D4E657400 static const char ARTNET_SIG[] = "Art-Net"; if (memcmp(buf, ARTNET_SIG, sizeof(ARTNET_SIG))) { return; } uint16_t opcode = buf[8] | (buf[9] << 8); uint16_t protocol = (buf[10] << 8) | buf[11]; // Big Endian uint16_t universe = buf[14] | (buf[15] << 8); uint16_t datalen = (buf[16] << 8) | buf[17]; // AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("DMX: opcode=0x%04X procotol=%i universe=%i datalen=%i univ_start=%i univ_end=%i"), opcode, protocol, universe, datalen, artnet_conf.univ, artnet_conf.univ + artnet_conf.rows); if (opcode != 0x5000 || protocol != 14) { return; } // if (len + 18 < datalen) { // AddLog(LOG_LEVEL_DEBUG, PSTR("DMX: packet is truncated. Expected: %u Bytes, Received: %u Bytes."), datalen, len + 18); // } if (universe < artnet_conf.univ || universe >= artnet_conf.univ + artnet_conf.rows) { return; } // universe is not ours, ignore size_t idx = 18; // start of payload data in the UDP frame uint16_t row = universe - artnet_conf.univ; if (artnet_conf.matrix) { // Ws2812 led strip size_t pix_size = Ws2812StripGetPixelSize(); // check that datalen does not exceed the number of columns if (datalen > artnet_conf.cols * pix_size) { datalen = artnet_conf.cols * pix_size; } // round to exact number of pixels datalen = datalen - (datalen % pix_size); size_t offset_in_matrix = 0; if (artnet_conf.alt && (row % 2)) { for (int32_t i = idx, j = idx + datalen - pix_size; i < j; i += pix_size, j -= pix_size) { for (int32_t k = 0; k < pix_size; k++) { uint8_t temp = buf[i+k]; buf[i+k] = buf[j+k]; buf[j+k] = temp; } } offset_in_matrix = artnet_conf.cols * pix_size - datalen; // add a potential offset if the frame is smaller than the columns } // process dimmer if (artnet_conf.dimm != 100) { // No Gamma for now if (pix_size == 3) { for (int32_t i = idx; i < idx+datalen; i += pix_size) { buf[i] = changeUIntScale(buf[i], 0, 100, 0, artnet_conf.dimm); buf[i+1] = changeUIntScale(buf[i+1], 0, 100, 0, artnet_conf.dimm); buf[i+2] = changeUIntScale(buf[i+2], 0, 100, 0, artnet_conf.dimm); } } else if (pix_size == 4) { for (int32_t i = idx; i < idx+datalen; i += pix_size) { buf[i] = changeUIntScale(buf[i], 0, 100, 0, artnet_conf.dimm); buf[i+1] = changeUIntScale(buf[i+1], 0, 100, 0, artnet_conf.dimm); buf[i+2] = changeUIntScale(buf[i+2], 0, 100, 0, artnet_conf.dimm); buf[i+3] = changeUIntScale(buf[i+2], 0, 100, 0, artnet_conf.dimm); } } } // process pixels size_t h_bytes = artnet_conf.cols * pix_size; // size in bytes of a single row offset_in_matrix += artnet_conf.offs * pix_size + row * h_bytes; if (datalen > h_bytes) { datalen = h_bytes; } // copy at most one line Ws2812CopyPixels(&buf[idx], datalen, offset_in_matrix); } else { // single light size_t offsidx = artnet_conf.offs + idx; uint8_t r8 = buf[offsidx+1]; uint8_t g8 = buf[offsidx]; uint8_t b8 = buf[offsidx+2]; uint8_t w8 = buf[offsidx+3]; uint8_t ww8 = buf[offsidx+4]; // scale dimmer values to RGBWWTable calibration uint16_t r_dimmer = changeUIntScale(artnet_conf.dimm, 0, 100, 0, Settings->rgbwwTable[0]) * 4; uint16_t g_dimmer = changeUIntScale(artnet_conf.dimm, 0, 100, 0, Settings->rgbwwTable[1]) * 4; uint16_t b_dimmer = changeUIntScale(artnet_conf.dimm, 0, 100, 0, Settings->rgbwwTable[2]) * 4; uint16_t w_dimmer = changeUIntScale(artnet_conf.dimm, 0, 100, 0, Settings->rgbwwTable[3]) * 4; uint16_t ww_dimmer = changeUIntScale(artnet_conf.dimm, 0, 100, 0, Settings->rgbwwTable[4]) * 4; uint16_t color[LST_MAX] = {0}; color[0] = changeUIntScale(r8, 0, 255, 0, r_dimmer); color[1] = changeUIntScale(g8, 0, 255, 0, g_dimmer); color[2] = changeUIntScale(b8, 0, 255, 0, b_dimmer); color[3] = changeUIntScale(w8, 0, 255, 0, w_dimmer); color[4] = changeUIntScale(ww8, 0, 255, 0, ww_dimmer); // AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("DMX: %02X-%02X-%02X univ=%i rows=%i max_univ=%i"), buf[idx+1], buf[idx], buf[idx+2], universe, row, artnet_conf.univ + artnet_conf.rows); LightSetOutputs(color); } // AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("DMX: ok universe=%i datalen=%i"), universe, datalen); artnet_conf.packet_accepted++; if (packets_per_row) { packets_per_row[row]++; } } // // Called at event loop, checks for incoming data from the CC2530 // void ArtNetLoop(void) { if (artnet_udp_connected && ArtNetUdp != nullptr) { ArtNetLoadSettings(); bool packet_ready = false; int32_t packet_len = 0; #ifdef ESP8266 packet_ready = ArtNetUdp->next(); while (packet_ready) { UdpPacket *packet; packet = ArtNetUdp->read(); uint8_t * packet_buffer = (uint8_t*) &packet->buf; packet_len = packet->len; #else packet_len = ArtNetUdp->parsePacket(); packet_ready = (packet_len > 0); while (packet_ready) { uint8_t packet_buffer[WS2812_ARTNET_UDP_BUFFER_SIZE]; // buffer to hold incoming UDP/SSDP packet packet_len = ArtNetUdp->read(packet_buffer, WS2812_ARTNET_UDP_BUFFER_SIZE); ArtNetUdp->flush(); // Finish reading the current packet #endif // AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("UDP: Packet %*_H (%d)"), 32, packet_buffer, packet_len); if (artnet_conf.on) { ArtNetProcessPacket(packet_buffer, packet_len); } #ifdef ESP8266 packet_ready = ArtNetUdp->next(); if (!packet_ready) { // if no more incoming packet, still wait for 20 microseconds delay(1); // delayMicroseconds seems broken, need to packet_ready = ArtNetUdp->next(); } #else packet_len = ArtNetUdp->parsePacket(); packet_ready = (packet_len > 0); if (!packet_ready) { // if no more incoming packet, still wait for 20 microseconds delayMicroseconds(20); packet_len = ArtNetUdp->parsePacket(); packet_ready = (packet_len > 0); } #endif } if (artnet_conf.on) { // ignore action if not on if (artnet_conf.matrix) { if (Ws2812StripRefresh()) { artnet_conf.strip_refresh++; // record metric } } } } } // // Published state // void ArtNetJSONAppend(void) { if (artnet_udp_connected) { ResponseAppend_P(PSTR(",\"ArtNet\":{\"PacketsReceived\":%u,\"PacketsAccepted\":%u,\"Frames\":%u"), artnet_conf.packet_received, artnet_conf.packet_accepted, artnet_conf.strip_refresh); if (packets_per_row) { ResponseAppend_P(PSTR(",\"PacketsPerRow\":[")); for (int32_t i = 0; i < artnet_conf.rows; i++) { ResponseAppend_P(PSTR("%s%i"), i ? "," : "", packets_per_row[i]); } ResponseAppend_P(PSTR("]")); } ResponseAppend_P(PSTR("}")); } } // // Command `ArtNetConfig` // Params: JSON // {"Rows":5, "Cols":5, "Offset":0, "Alternate":false, "Universe":0, "Port":6454} // void CmndArtNetConfig() { bool was_running = artnet_udp_connected; if (was_running) { ArtNetStop(); } ArtNetLoadSettings(); TrimSpace(XdrvMailbox.data); if (strlen(XdrvMailbox.data) > 0) { JsonParser parser(XdrvMailbox.data); JsonParserObject root = parser.getRootObject(); if (!root) { ResponseCmndChar_P(PSTR(D_JSON_INVALID_JSON)); return; } artnet_conf.rows = root.getUInt(PSTR("Rows"), artnet_conf.rows); artnet_conf.cols = root.getUInt(PSTR("Cols"), artnet_conf.cols); artnet_conf.offs = root.getUInt(PSTR("Offset"), artnet_conf.offs); artnet_conf.alt = root.getBool(PSTR("Alternate"), artnet_conf.alt); artnet_conf.univ = root.getUInt(PSTR("Universe"), artnet_conf.univ); artnet_conf.port = root.getUInt(PSTR("Port"), artnet_conf.port); artnet_conf.dimm = root.getUInt(PSTR("Dimmer"), artnet_conf.dimm); ArtNetSaveSettings(); } if (was_running) { ArtNetStart(); } // display the current or new configuration // {"Rows":5, "Cols":5, "Offset":0, "Alternate":false, "Universe":0, "Port":6454} Response_P(PSTR("{\"Rows\":%u,\"Cols\":%u,\"Dimmer\":%u,\"Offset\":%u" ",\"Alternate\":%s,\"Universe\":%u,\"Port\":%u}"), artnet_conf.rows, artnet_conf.cols, artnet_conf.dimm, artnet_conf.offs, artnet_conf.alt ? "true":"false", artnet_conf.univ, artnet_conf.port); } // ArtNetStart // Returns true if ok bool ArtNetStart(void) { ArtNetLoadSettings(); if (!artnet_udp_connected && !TasmotaGlobal.restart_flag) { if (ArtNetUdp == nullptr) { #ifdef ESP8266 ArtNetUdp = new UdpListener(WS2812_ARTNET_UDP_MAX_PACKETS); #else ArtNetUdp = new WiFiUDP(); #endif if (ArtNetUdp == nullptr) { AddLog(LOG_LEVEL_INFO, PSTR("DMX: cannot allocate memory")); return false; } } #ifdef ESP8266 ArtNetUdp->reset(); ip_addr_t addr = IPADDR4_INIT(INADDR_ANY); if ((igmp_joingroup(WiFi.localIP(), IPAddress(USE_LIGHT_ARTNET_MCAST)) == ERR_OK) && (ArtNetUdp->listen(&addr, artnet_conf.port))) { // if (ArtNetUdp->listen(&addr, artnet_conf.port)) { #else if (ArtNetUdp->beginMulticast(IPAddress(USE_LIGHT_ARTNET_MCAST), artnet_conf.port)) { #endif // OK AddLog(LOG_LEVEL_INFO, PSTR("DMX: listening to port %i"), artnet_conf.port); artnet_udp_connected = true; packets_per_row = (uint32_t*) malloc(artnet_conf.rows * sizeof(uint32_t*)); if (packets_per_row) { memset((void*)packets_per_row, 0, artnet_conf.rows * sizeof(uint32_t*)); } // set sleep to at most 5 if (TasmotaGlobal.sleep > WS2812_ARTNET_MAX_SLEEP) { TasmotaGlobal.sleep = WS2812_ARTNET_MAX_SLEEP; } // change settings to ArtNet specific scheme Settings->flag6.artnet_autorun = true; // change strip configuration if (artnet_conf.matrix) { if ((Settings->light_pixels != artnet_conf.rows * artnet_conf.cols + artnet_conf.offs) || (Settings->light_rotation != 0)) { Settings->light_pixels = artnet_conf.rows * artnet_conf.cols + artnet_conf.offs; Settings->light_rotation = 0; Ws2812ReinitStrip(); } else { Ws2812Clear(); } } // turn power on if it's not if (!(Light.power & 1)) { LightPowerOn(); } } else { AddLog(LOG_LEVEL_INFO, PSTR("DMX: error opening port %i"), artnet_conf.port); return false; } } return true; } // Stop the ArtNet UDP flow and disconnect server void ArtNetStop(void) { artnet_udp_connected = false; if (ArtNetUdp != nullptr) { #ifdef ESP8266 ArtNetUdp->disconnect(); #else ArtNetUdp->stop(); #endif delete ArtNetUdp; ArtNetUdp = nullptr; } if (packets_per_row) { free((void*)packets_per_row); packets_per_row = nullptr; } } void CmndArtNet(void) { if (0 == XdrvMailbox.payload) { ArtNetStop(); Settings->flag6.artnet_autorun = false; // SetOption148 - (Light) start DMX ArtNet at boot, listen to UDP port as soon as network is up // Settings->light_scheme = LS_POWER; // restore default scheme TasmotaGlobal.sleep = Settings->sleep; // Restore sleep value Light.update = true; // Restore old color } if (1 == XdrvMailbox.payload) { if (!ArtNetStart()) { Settings->flag6.artnet_autorun = false; // SetOption148 - (Light) start DMX ArtNet at boot, listen to UDP port as soon as network is up } } ResponseCmndStateText(artnet_udp_connected & Settings->flag6.artnet_autorun); } /*********************************************************************************************\ * Interface \*********************************************************************************************/ bool artnet_network_up = false; void ArtNetFuncNetworkUp(void) { if (!artnet_network_up) { artnet_network_up = true; if (Settings->flag6.artnet_autorun) { if (!ArtNetStart()) { Settings->flag6.artnet_autorun = false; // disable autorun if it failed, avoid nasty loop errors } } } } void ArtNetFuncNetworkDown(void) { if (artnet_network_up) { artnet_network_up = false; ArtNetStop(); } } #endif // USE_LIGHT_ARTNET #endif // USE_LIGHT