From 2549203c13bd63f3c2ef0a6f6ea3d07f8d60b9a6 Mon Sep 17 00:00:00 2001 From: Stephan Hadinger Date: Sun, 13 Nov 2022 18:22:39 +0100 Subject: [PATCH] Added WS2812 and Light ArtNet DMX control over UDP port 6454 --- CHANGELOG.md | 1 + .../jsmn-shadinger-1.0/src/JsonParser.cpp | 3 + .../jsmn-shadinger-1.0/src/JsonParser.h | 1 + tasmota/include/i18n.h | 4 + tasmota/include/tasmota_types.h | 8 +- tasmota/my_user_config.h | 3 + tasmota/tasmota_xdrv_driver/xdrv_04_light.ino | 40 +- .../xdrv_04_light_artnet.ino | 425 ++++++++++++++++++ 8 files changed, 472 insertions(+), 13 deletions(-) create mode 100644 tasmota/tasmota_xdrv_driver/xdrv_04_light_artnet.ino diff --git a/CHANGELOG.md b/CHANGELOG.md index c7c6a0588..63408c2e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ All notable changes to this project will be documented in this file. - Support for Plantower PMSx003T AQI models with temperature and humidity (#16971) - Support for Dingtian x595/x165 shift register based relay boards by Barbudor (#17032) - Added ``FUNC_NETWORK_UP`` and ``FUNC_NETWORK_DOWN`` events +- Added WS2812 and Light ArtNet DMX control over UDP port 6454 ### Breaking Changed diff --git a/lib/default/jsmn-shadinger-1.0/src/JsonParser.cpp b/lib/default/jsmn-shadinger-1.0/src/JsonParser.cpp index 0668faa1a..22229e3f1 100644 --- a/lib/default/jsmn-shadinger-1.0/src/JsonParser.cpp +++ b/lib/default/jsmn-shadinger-1.0/src/JsonParser.cpp @@ -370,6 +370,9 @@ uint64_t JsonParserToken::getULong(void) const { return getULong(0); } float JsonParserToken::getFloat(void) const { return getFloat(0); } const char * JsonParserToken::getStr(void) const { return getStr(""); } +bool JsonParserObject::getBool(const char * needle, bool val) const { + return (*this)[needle].getBool(val); +} int32_t JsonParserObject::getInt(const char * needle, int32_t val) const { return (*this)[needle].getInt(val); } diff --git a/lib/default/jsmn-shadinger-1.0/src/JsonParser.h b/lib/default/jsmn-shadinger-1.0/src/JsonParser.h index 03a0aba65..9d61044fd 100644 --- a/lib/default/jsmn-shadinger-1.0/src/JsonParser.h +++ b/lib/default/jsmn-shadinger-1.0/src/JsonParser.h @@ -162,6 +162,7 @@ public: const char * findConstCharNull(const char * needle) const; // all-in-one methods: search for key (case insensitive), convert value and set default + bool getBool(const char *, bool val) const; int32_t getInt(const char *, int32_t) const; uint32_t getUInt(const char *, uint32_t) const; uint64_t getULong(const char *, uint64_t) const; diff --git a/tasmota/include/i18n.h b/tasmota/include/i18n.h index ff4546996..85445545b 100644 --- a/tasmota/include/i18n.h +++ b/tasmota/include/i18n.h @@ -502,6 +502,10 @@ #define D_CMND_PALETTE "Palette" #define D_CMND_PIXELS "Pixels" #define D_CMND_STEPPIXELS "StepPixels" +#define D_CMND_ARTNET_START "ArtNetStart" +#define D_CMND_ARTNET_STOP "ArtNetStop" +#define D_CMND_ARTNET_CONFIG "ArtNetConfig" +#define D_SO_ARTNET_AUTORUN "ArtNetAutorun" #define D_CMND_RGBWWTABLE "RGBWWTable" #define D_CMND_ROTATION "Rotation" #define D_CMND_SCHEME "Scheme" diff --git a/tasmota/include/tasmota_types.h b/tasmota/include/tasmota_types.h index 7f952b8f5..d1dd5c9eb 100644 --- a/tasmota/include/tasmota_types.h +++ b/tasmota/include/tasmota_types.h @@ -181,7 +181,7 @@ typedef union { // Restricted by MISRA-C Rule 18.4 bu struct { // SetOption146 .. SetOption177 uint32_t use_esp32_temperature : 1; // bit 0 (v12.1.1.1) - SetOption146 - (ESP32) Show ESP32 internal temperature sensor uint32_t mqtt_disable_sserialrec : 1; // bit 1 (v12.1.1.2) - SetOption147 - (MQTT) Disable publish SSerialReceived MQTT messages, you must use event trigger rules instead. - uint32_t spare02 : 1; // bit 2 + uint32_t artnet_autorun : 1; // bit 2 (v12.2.0.4) - SetOption148 - (Light) start DMX ArtNet at boot, listen to UDP port as soon as network is up uint32_t spare03 : 1; // bit 3 uint32_t spare04 : 1; // bit 4 uint32_t spare05 : 1; // bit 5 @@ -722,14 +722,14 @@ typedef struct { char user_template_name[15]; // 720 15 bytes - Backward compatibility since v8.2.0.3 #ifdef ESP8266 - mytmplt8285 ex_user_template8; // 72F 14 bytes (ESP8266) - Free since 9.0.0.1 + uint8_t ex_user_template8[5]; // 72F 14 bytes (ESP8266) - Free since 9.0.0.1 - only 5 bytes referenced now #endif // ESP8266 #ifdef ESP32 uint8_t webcam_clk; // 72F WebCamCfg2 webcam_config2; // 730 - - uint8_t free_esp32_734[9]; // 734 #endif // ESP32 + uint16_t artnet_universe; // 734 + uint8_t free_esp32_734[7]; // 736 uint8_t novasds_startingoffset; // 73D uint8_t web_color[18][3]; // 73E diff --git a/tasmota/my_user_config.h b/tasmota/my_user_config.h index b5cd3ebd1..c59d4a5ea 100644 --- a/tasmota/my_user_config.h +++ b/tasmota/my_user_config.h @@ -571,6 +571,9 @@ #define USE_DGR_LIGHT_SEQUENCE // Add support for device group light sequencing (requires USE_DEVICE_GROUPS) (+0k2 code) //#define USE_LSC_MCSL // Add support for GPE Multi color smart light as sold by Action in the Netherlands (+1k1 code) +// #define USE_LIGHT_ARTNET // Add support for DMX/ArtNet via UDP on port 6454 (+3.5k code) + #define USE_LIGHT_ARTNET_MCAST 239,255,25,54 // Multicast address used to listen: 239.255.25.24 + // -- Counter input ------------------------------- #define USE_COUNTER // Enable inputs as counter (+0k8 code) diff --git a/tasmota/tasmota_xdrv_driver/xdrv_04_light.ino b/tasmota/tasmota_xdrv_driver/xdrv_04_light.ino index 164e4d708..e6cafd584 100644 --- a/tasmota/tasmota_xdrv_driver/xdrv_04_light.ino +++ b/tasmota/tasmota_xdrv_driver/xdrv_04_light.ino @@ -135,7 +135,7 @@ const uint8_t LIGHT_COLOR_SIZE = 25; // Char array scolor size const char kLightCommands[] PROGMEM = "|" // No prefix // SetOptions synonyms D_SO_CHANNELREMAP "|" D_SO_MULTIPWM "|" D_SO_ALEXACTRANGE "|" D_SO_POWERONFADE "|" D_SO_PWMCT "|" - D_SO_WHITEBLEND "|" + D_SO_WHITEBLEND "|" D_SO_ARTNET_AUTORUN "|" // Other commands D_CMND_COLOR "|" D_CMND_COLORTEMPERATURE "|" D_CMND_DIMMER "|" D_CMND_DIMMER_RANGE "|" D_CMND_DIMMER_STEP "|" D_CMND_LEDTABLE "|" D_CMND_FADE "|" D_CMND_RGBWWTABLE "|" D_CMND_SCHEME "|" D_CMND_SPEED "|" D_CMND_WAKEUP "|" D_CMND_WAKEUPDURATION "|" @@ -150,11 +150,14 @@ const char kLightCommands[] PROGMEM = "|" // No prefix #ifdef USE_DGR_LIGHT_SEQUENCE "|" D_CMND_SEQUENCE_OFFSET #endif // USE_DGR_LIGHT_SEQUENCE +#ifdef USE_LIGHT_ARTNET + "|" D_CMND_ARTNET_START "|" D_CMND_ARTNET_STOP "|" D_CMND_ARTNET_CONFIG +#endif "|UNDOCA" ; SO_SYNONYMS(kLightSynonyms, 37, 68, 82, 91, 92, - 105, + 105, 148, ); void (* const LightCommand[])(void) PROGMEM = { @@ -171,6 +174,9 @@ void (* const LightCommand[])(void) PROGMEM = { #ifdef USE_DGR_LIGHT_SEQUENCE &CmndSequenceOffset, #endif // USE_DGR_LIGHT_SEQUENCE +#ifdef USE_LIGHT_ARTNET + &CmndArtNetStart, &CmndArtNetStop, &CmndArtNetConfig, +#endif &CmndUndocA }; // Light color mode, either RGB alone, or white-CT alone, or both only available if ct_rgb_linked is false @@ -1862,7 +1868,7 @@ void LightAnimate(void) break; #endif default: - XlgtCall(FUNC_SET_SCHEME); + XlgtCall(FUNC_SET_SCHEME); } #ifdef USE_DEVICE_GROUPS @@ -2229,6 +2235,10 @@ void LightSetOutputs(const uint16_t *cur_col_10) { XdrvMailbox.data = (char*)cur_col; XdrvMailbox.topic = (char*)scale_col; XdrvMailbox.command = (char*)cur_col_10; +#ifdef USE_LIGHT_ARTNET + if (ArtNetSetChannels()) { /* Serviced */} + else +#endif if (XlgtCall(FUNC_SET_CHANNELS)) { /* Serviced */ } else if (XdrvCall(FUNC_SET_CHANNELS)) { /* Serviced */ } XdrvMailbox.data = tmp_data; @@ -3413,6 +3423,9 @@ bool Xdrv04(uint32_t function) LightSetOutputs(Light.fade_cur_10); } } +#ifdef USE_LIGHT_ARTNET + ArtNetLoop(); +#endif // USE_LIGHT_ARTNET break; case FUNC_EVERY_50_MSECOND: LightAnimate(); @@ -3428,12 +3441,6 @@ bool Xdrv04(uint32_t function) case FUNC_BUTTON_MULTI_PRESSED: result = XlgtCall(FUNC_BUTTON_MULTI_PRESSED); break; - case FUNC_NETWORK_UP: - XlgtCall(FUNC_NETWORK_UP); - break; - case FUNC_NETWORK_DOWN: - XlgtCall(FUNC_NETWORK_DOWN); - break; #ifdef USE_WEBSERVER case FUNC_WEB_ADD_MAIN_BUTTON: XlgtCall(FUNC_WEB_ADD_MAIN_BUTTON); @@ -3451,6 +3458,21 @@ bool Xdrv04(uint32_t function) case FUNC_PRE_INIT: LightInit(); break; +#ifdef USE_LIGHT_ARTNET + case FUNC_JSON_APPEND: + ArtNetJSONAppend(); + break; + case FUNC_NETWORK_UP: + if (Settings->flag6.artnet_autorun) { + if (!ArtNetStart()) { + Settings->flag6.artnet_autorun = false; // disable autorun if it failed, avoid nasty loop errors + } + } + break; + case FUNC_NETWORK_DOWN: + ArtNetStop(); + break; +#endif // USE_LIGHT_ARTNET } } return result; diff --git a/tasmota/tasmota_xdrv_driver/xdrv_04_light_artnet.ino b/tasmota/tasmota_xdrv_driver/xdrv_04_light_artnet.ino new file mode 100644 index 000000000..4ce80f126 --- /dev/null +++ b/tasmota/tasmota_xdrv_driver/xdrv_04_light_artnet.ino @@ -0,0 +1,425 @@ +/* + 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 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, ignoring packet")); + } + + 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(); + datalen = datalen - (datalen % pix_size); + + 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; + } + } + } + + // 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 + size_t 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 + uint8_t r8 = buf[idx+1]; + uint8_t g8 = buf[idx]; + uint8_t b8 = buf[idx+2]; + uint16_t dimmer10 = changeUIntScale(artnet_conf.dimm, 0, 100, 0, 1023); + uint16_t color[LST_MAX] = {0}; + color[0] = changeUIntScale(r8, 0, 255, 0, dimmer10); + color[1] = changeUIntScale(g8, 0, 255, 0, dimmer10); + color[2] = changeUIntScale(b8, 0, 255, 0, dimmer10); + // 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[UDP_BUFFER_SIZE]; // buffer to hold incoming UDP/SSDP packet + + packet_len = ArtNetUdp->read(packet_buffer, 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(); + } + } + + // 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; +} + +// +// Command `ArtNetStart` +// Params: XXX +// +void CmndArtNetStart(void) { + if (ArtNetStart()) { + ResponseCmndDone(); + } else { + ResponseCmndError(); + } +} + +// 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 CmndArtNetStop(void) { + ArtNetStop(); + // restore default scheme + Settings->light_scheme = LS_POWER; + // Restore sleep value + TasmotaGlobal.sleep = Settings->sleep; + // OK + ResponseCmndDone(); +} + +#endif // USE_LIGHT_ARTNET +#endif // USE_LIGHT