From 7a685185f4e2a0008f0cd2ab54d9fe08c63c2a56 Mon Sep 17 00:00:00 2001 From: LordGrey <48840279+Lord-Grey@users.noreply.github.com> Date: Mon, 14 Sep 2020 17:20:00 +0200 Subject: [PATCH] AtmoOrb Fix (#988) * AtmoOrb UdpSocket-Bind Fix * Cleanup and update defaults (to work via PowerLan) * Cleanup and update defaults (to work via PowerLan) * AtmoOrb identification support, small updates * AtmoOrb discovery & identification support, fixes and stability updates * Small clean-ups * Type fix * Add missing include * Adalight - Update default config and levels * Update Atmoorb sketch * Yeelight - Update default value --- assets/firmware/esp8266/AtmoOrb/AtmoOrb.ino | 354 ++++++++++++++++++ assets/webconfig/i18n/en.json | 4 + assets/webconfig/js/content_leds.js | 6 + assets/webconfig/js/wizard.js | 279 +++++++++++++- libsrc/leddevice/LedDevice.cpp | 5 +- libsrc/leddevice/dev_net/LedDeviceAtmoOrb.cpp | 253 ++++++++++--- libsrc/leddevice/dev_net/LedDeviceAtmoOrb.h | 40 +- .../leddevice/dev_net/LedDeviceNanoleaf.cpp | 4 +- libsrc/leddevice/dev_net/LedDeviceWled.cpp | 2 - .../leddevice/dev_net/LedDeviceYeelight.cpp | 2 +- libsrc/leddevice/schemas/schema-adalight.json | 9 +- libsrc/leddevice/schemas/schema-atmoorb.json | 41 +- libsrc/leddevice/schemas/schema-yeelight.json | 4 +- 13 files changed, 898 insertions(+), 105 deletions(-) create mode 100644 assets/firmware/esp8266/AtmoOrb/AtmoOrb.ino diff --git a/assets/firmware/esp8266/AtmoOrb/AtmoOrb.ino b/assets/firmware/esp8266/AtmoOrb/AtmoOrb.ino new file mode 100644 index 00000000..8a19fd72 --- /dev/null +++ b/assets/firmware/esp8266/AtmoOrb/AtmoOrb.ino @@ -0,0 +1,354 @@ +// AtmoOrb by Lightning303 & Rick164, Additions by Lord-Grey +// +// ESP8266 Standalone Version +// +// +// You may change the settings that are commented + +#define FASTLED_ALLOW_INTERRUPTS 0 +// To make sure that all leds get changed 100% of the time, we need to allow FastLED to disabled interrupts for a short while. +// If you experience problems, please set this value to 1. +// This is only needed for 3 wire (1 data line + Vcc and GND) chips (e.g. WS2812B). If you are using WS2801, APA102 or similar chipsets, you can set the value back to 1. + +#include +#include +#include + +#define NUM_LEDS 24 // Number of leds +#define DATA_PIN 7 // Data pin for leds (the default pin 7 might correspond to pin 13 on some boards) +#define SERIAL_DEBUG 0 // Serial debugging (0=Off, 1=On) + +#define ID 1 // Id of this lamp + +// Smoothing +#define SMOOTH_STEPS 20 // Steps to take for smoothing colors +#define SMOOTH_DELAY 10 // Delay between smoothing steps +#define SMOOTH_BLOCK 0 // Block incoming colors while smoothing + +// Startup color +#define STARTUP_RED 255 // Color shown directly after power on +#define STARTUP_GREEN 175 // Color shown directly after power on +#define STARTUP_BLUE 100 // Color shown directly after power on + +// White adjustment +#define RED_CORRECTION 220 // Color Correction +#define GREEN_CORRECTION 255 // Color Correction +#define BLUE_CORRECTION 180 // Color Correction + +// RC Switch +#define RC_SWITCH 0 // RF transmitter to swtich remote controlled power sockets (0=Off, 1=On) +#if RC_SWITCH == 1 + #include + #define RC_PIN 2 // Data pin for RF transmitter + #define RC_SLEEP_DELAY 900000 // Delay until RF transmitter send signals + char* rcCode0 = "10001"; // First part of the transmission code + char* rcCode1 = "00010"; // Second part of the transmission code + RCSwitch mySwitch = RCSwitch(); + boolean remoteControlled = false; +#endif + +// Network settings +const char* ssid = "***"; // WiFi SSID +const char* password = "***"; // WiFi password + +const IPAddress multicastIP(239,255,255,250); // Multicast IP address +const int multicastPort = 49692; // Multicast port number +IPAddress ip_null(0,0,0,0); +IPAddress local_IP(0,0,0,0); +WiFiUDP Udp; + +int timeout = 20000; // wait 20 sec for successfull login +boolean is_connect = false; // ... not yet connected + +CRGB leds[NUM_LEDS]; + +byte nextColor[3]; +byte prevColor[3]; +byte currentColor[3]; +byte smoothStep = SMOOTH_STEPS; +unsigned long smoothMillis; + +void setColor(byte red, byte green, byte blue); +void setSmoothColor(byte red, byte green, byte blue); +void smoothColor(); +void clearSmoothColors(); + +void setup() +{ + FastLED.addLeds(leds, NUM_LEDS); + //FastLED.setCorrection(TypicalSMD5050); + FastLED.setCorrection(CRGB(RED_CORRECTION, GREEN_CORRECTION, BLUE_CORRECTION)); + FastLED.showColor(CRGB(STARTUP_RED, STARTUP_GREEN, STARTUP_BLUE)); + + #if RC_SWITCH == 1 + mySwitch.enableTransmit(RC_PIN); + #endif + + #if SERIAL_DEBUG == 1 + Serial.begin(115200); + #endif + + #if SERIAL_DEBUG == 1 + Serial.printf("Connecting to %s ", ssid); + #endif + + // .... wait for WiFi gets valid !!! + unsigned long tick = millis(); // get start-time for login + WiFi.begin(ssid, password); + while ( (!is_connect) && ((millis() - tick) < timeout) ) + { + yield(); // ... for safety + is_connect = WiFi.status(); // connected ? + if (!is_connect) // only if not yet connected ! + { + #if SERIAL_DEBUG == 1 + Serial.print("."); // print a dot while waiting + #endif + delay(50); + } + } + + if (is_connect) + { + #if SERIAL_DEBUG == 1 + Serial.print("after "); + Serial.print(millis() - tick); + Serial.println(" ms"); + #endif + // .... wait for local_IP becomes valid !!! + is_connect = false; + tick = millis(); // get start-time for login + while ( (!is_connect) && ((millis() - tick) < timeout) ) + { + yield(); // ... for safety + local_IP = WiFi.localIP(); + is_connect = local_IP != ip_null; // connected ? + if (!is_connect) // only if not yet connected ! + { + #if SERIAL_DEBUG == 1 + Serial.print("."); // print a dot while waiting + #endif + delay(50); + } + } + if (is_connect) + { + #if SERIAL_DEBUG == 1 + Serial.print("local_IP valid after "); + Serial.print(millis() - tick); + Serial.println(" ms"); + Serial.println(""); + Serial.print(F("Connected to ")); + Serial.println(ssid); + #endif + + // ... now start UDP and check the result: + is_connect = Udp.beginMulticast(local_IP, multicastIP, multicastPort); + if (is_connect) + { + #if SERIAL_DEBUG == 1 + Serial.print("Listening to Multicast at "); + Serial.print(multicastIP); + Serial.println(":" + String(multicastPort)); + #endif + } + else + { + #if SERIAL_DEBUG == 1 + Serial.println(" - ERROR beginMulticast !"); + #endif + } + } + else + { + #if SERIAL_DEBUG == 1 + Serial.println("local_IP invalid after timeout !"); + #endif + } + } + else + { + #if SERIAL_DEBUG == 1 + Serial.println("- invalid after timeout !"); + #endif + } +} + +void loop() +{ + #if SERIAL_DEBUG == 1 + if (WiFi.status() != WL_CONNECTED) + { + Serial.print(F("Lost connection to ")); + Serial.print(ssid); + Serial.println(F(".")); + Serial.println(F("Trying to reconnect.")); + while (WiFi.status() != WL_CONNECTED) + { + delay(500); + Serial.print(F(".")); + } + Serial.println(""); + Serial.println(F("Reconnected.")); + } + #endif + if (Udp.parsePacket()) + { + byte len = Udp.available(); + byte rcvd[len]; + Udp.read(rcvd, len); + + #if SERIAL_DEBUG == 1 + Serial.print(F("UDP Packet from ")); + Serial.print(Udp.remoteIP()); + Serial.print(F(":")); + Serial.print(Udp.remotePort()); + Serial.print(F(" to ")); + Serial.println(Udp.destinationIP()); + for (byte i = 0; i < len; i++) + { + Serial.print(rcvd[i]); + Serial.print(F(" ")); + } + Serial.println(""); + #endif + if (len >= 8 && rcvd[0] == 0xC0 && rcvd[1] == 0xFF && rcvd[2] == 0xEE && (rcvd[4] == ID || rcvd[4] == 0)) + { + switch (rcvd[3]) + { + case 1: + smoothStep = SMOOTH_STEPS; + forceLedsOFF(); + break; + case 2: + default: + setSmoothColor(rcvd[5], rcvd[6], rcvd[7]); + break; + case 4: + setColor(rcvd[5], rcvd[6], rcvd[7]); + smoothStep = SMOOTH_STEPS; + break; + case 8: + #if SERIAL_DEBUG == 1 + Serial.print(F("Announce myself. OrbID: ")); + Serial.println(ID); + #endif + Udp.beginPacket(Udp.remoteIP(), Udp.remotePort()); + Udp.write(ID); + Udp.endPacket(); + break; + case 9: + #if SERIAL_DEBUG == 1 + Serial.print(F("Identify myself. OrbID: ")); + Serial.println(ID); + #endif + identify(); + break; + } + } + } + if (smoothStep < SMOOTH_STEPS && millis() >= (smoothMillis + (SMOOTH_DELAY * (smoothStep + 1)))) + { + smoothColor(); + } + #if RC_SWITCH == 1 + if (remoteControlled && currentColor[0] == 0 && currentColor[1] == 0 && currentColor[2] == 0 && millis() >= smoothMillis + RC_SLEEP_DELAY) + { + // Send this signal only once every seconds + smoothMillis += 1000; + mySwitch.switchOff(rcCode0, rcCode1); + } + #endif +} + +// Display color on leds +void setColor(byte red, byte green, byte blue) +{ + // Is the new color already active? + if (currentColor[0] == red && currentColor[1] == green && currentColor[2] == blue) + { + return; + } + currentColor[0] = red; + currentColor[1] = green; + currentColor[2] = blue; + + FastLED.showColor(CRGB(red, green, blue)); +} + +// Set a new color to smooth to +void setSmoothColor(byte red, byte green, byte blue) +{ + if (smoothStep == SMOOTH_STEPS || SMOOTH_BLOCK == 0) + { + // Is the new color the same as the one we already are smoothing towards? + // If so dont do anything. + if (nextColor[0] == red && nextColor[1] == green && nextColor[2] == blue) + { + return; + } + // Is the new color the same as we have right now? + // If so stop smoothing and keep the current color. + else if (currentColor[0] == red && currentColor[1] == green && currentColor[2] == blue) + { + smoothStep = SMOOTH_STEPS; + return; + } + + prevColor[0] = currentColor[0]; + prevColor[1] = currentColor[1]; + prevColor[2] = currentColor[2]; + + nextColor[0] = red; + nextColor[1] = green; + nextColor[2] = blue; + + smoothMillis = millis(); + smoothStep = 0; + + #if RC_SWITCH == 1 + if (!remoteControlled) + { + remoteControlled = true; + } + #endif + } +} + +// Display one step to the next color +void smoothColor() +{ + smoothStep++; + + byte red = prevColor[0] + (((nextColor[0] - prevColor[0]) * smoothStep) / SMOOTH_STEPS); + byte green = prevColor[1] + (((nextColor[1] - prevColor[1]) * smoothStep) / SMOOTH_STEPS); + byte blue = prevColor[2] + (((nextColor[2] - prevColor[2]) * smoothStep) / SMOOTH_STEPS); + + setColor(red, green, blue); +} + +// Force all leds OFF +void forceLedsOFF() +{ + setColor(0,0,0); + clearSmoothColors(); +} + +// Clear smooth color byte arrays +void clearSmoothColors() +{ + memset(prevColor, 0, sizeof(prevColor)); + memset(currentColor, 0, sizeof(nextColor)); + memset(nextColor, 0, sizeof(nextColor)); +} + +void identify() +{ + for (byte i = 0; i < 3; i++) + { + FastLED.showColor(CRGB::LemonChiffon); + delay(500); + FastLED.showColor(CRGB::Black); + delay(500); + } +} diff --git a/assets/webconfig/i18n/en.json b/assets/webconfig/i18n/en.json index 8bb1e643..2487fd58 100644 --- a/assets/webconfig/i18n/en.json +++ b/assets/webconfig/i18n/en.json @@ -397,6 +397,10 @@ "wiz_yeelight_desc2" : "Now choose which lamps should be added. The position assigns the lamp to a specific position on your \"picture\". Disabled lamps won't be added. To identify single lamps press the button on the right.", "wiz_yeelight_noLights": "No Yeelights found! Please get the lights connected to the network or configure them mannually.", "wiz_yeelight_unsupported" : "Unsupported", + "wiz_atmoorb_title" : "AtmoOrb Wizard", + "wiz_atmoorb_intro1" : "This wizards configures Hyperion for AtmoOrbs. Features are the AtmoOrb auto detection, setting each light to a specific position on your picture or disable it and tune the Hyperion settings automatically! So in short: All you need are some clicks and you are done!", + "wiz_atmoorb_desc2" : "Now choose which Orbs should be added. The position assigns the lamp to a specific position on your \"picture\". Disabled lamps won't be added. To identify single lamps press the button on the right.", + "wiz_atmoorb_noLights": "No AtmoOrbs found! Please get the lights connected to the network or configure them mannually.", "wiz_pos": "Position/State", "wiz_ids_disabled" : "Deactivated", "wiz_ids_entire" : "Whole picture", diff --git a/assets/webconfig/js/content_leds.js b/assets/webconfig/js/content_leds.js index 5db3676f..8a092d9e 100644 --- a/assets/webconfig/js/content_leds.js +++ b/assets/webconfig/js/content_leds.js @@ -543,6 +543,12 @@ $(document).ready(function() { changeWizard(data, wled_title, startWizardWLED); } */ + else if(ledType == "atmoorb") { + var ledWizardType = (this.checked) ? "atmoorb" : ledType; + var data = { type: ledWizardType }; + var atmoorb_title = 'wiz_atmoorb_title'; + changeWizard(data, atmoorb_title, startWizardAtmoOrb); + } else if(ledType == "yeelight") { var ledWizardType = (this.checked) ? "yeelight" : ledType; var data = { type: ledWizardType }; diff --git a/assets/webconfig/js/wizard.js b/assets/webconfig/js/wizard.js index aeee28c8..e4ba1700 100644 --- a/assets/webconfig/js/wizard.js +++ b/assets/webconfig/js/wizard.js @@ -1259,16 +1259,16 @@ function startWizardWLED(e) // For testing only discover_wled(); - + var hostAddress = conf_editor.getEditor("root.specificOptions.host").getValue(); if(hostAddress != "") { getProperties_wled(hostAddress,"info"); identify_wled(hostAddress) } - + // For testing only - + }); } @@ -1552,13 +1552,14 @@ function assign_yeelight_lights(){ options+= '>'+$.i18n(txt+val)+''; } + var enabled = 'enabled' if (! models.includes (lights[lightid].model) ) { var enabled = 'disabled'; options = ''; } - $('.lidsb').append(createTableRow([(parseInt(lightid, 10) + 1)+'. '+lightName+' ('+lightHostname+')', '' + options + '',''])); @@ -1615,6 +1616,276 @@ function identify_yeelight_device(hostname, port){ } } +//**************************** +// Wizard AtmoOrb +//**************************** +var lights = null; +function startWizardAtmoOrb(e) +{ + //create html + + var atmoorb_title = 'wiz_atmoorb_title'; + var atmoorb_intro1 = 'wiz_atmoorb_intro1'; + + $('#wiz_header').html(''+$.i18n(atmoorb_title)); + $('#wizp1_body').html('

'+$.i18n(atmoorb_title)+'

'+$.i18n(atmoorb_intro1)+'

'); + + $('#wizp1_footer').html(''); + + $('#wizp2_body').html('
'); + + $('#wh_topcontainer').append(''); + + $('#wizp2_body').append(''); + + createTable("lidsh", "lidsb", "orb_ids_t"); + $('.lidsh').append(createTableRow([$.i18n('edt_dev_spec_lights_title'),$.i18n('wiz_pos'),$.i18n('wiz_identify')], true)); + $('#wizp2_footer').html('' + +$.i18n('general_btn_cancel')+''); + + //open modal + $("#wizard_modal").modal({backdrop : "static", keyboard: false, show: true }); + + //listen for continue + $('#btn_wiz_cont').off().on('click',function() { + beginWizardAtmoOrb(); + $('#wizp1').toggle(false); + $('#wizp2').toggle(true); + }); +} + +function beginWizardAtmoOrb() +{ + lights = []; + configuredLights = []; + + configruedOrbIds = conf_editor.getEditor("root.specificOptions.orbIds").getValue().trim(); + if ( configruedOrbIds.length !== 0 ) + { + configuredLights = configruedOrbIds.split(",").map( Number ); + } + + var multiCastGroup = conf_editor.getEditor("root.specificOptions.output").getValue(); + var multiCastPort = parseInt(conf_editor.getEditor("root.specificOptions.port").getValue()); + + discover_atmoorb_lights(multiCastGroup, multiCastPort); + + $('#btn_wiz_save').off().on("click", function(){ + var atmoorbLedConfig = []; + var finalLights = []; + + //create atmoorb led config + for(var key in lights) + { + if($('#orb_'+key).val() !== "disabled") + { + // Set Name to layout-position, if empty + if ( lights[key].name === "" ) + { + lights[key].name = $.i18n( 'conf_leds_layout_cl_'+$('#orb_'+key).val() ); + } + + finalLights.push( lights[key].id); + + var name = lights[key].id; + if ( lights[key].host !== "") + name += ':' + lights[key].host; + + var idx_content = assignLightPos(key, $('#orb_'+key).val(), name); + atmoorbLedConfig.push(JSON.parse(JSON.stringify(idx_content))); + } + } + + //LED layout + window.serverConfig.leds = atmoorbLedConfig; + + //LED device config + //Start with a clean configuration + var d = {}; + + d.type = 'atmoorb'; + d.hardwareLedCount = finalLights.length; + d.colorOrder = conf_editor.getEditor("root.generalOptions.colorOrder").getValue(); + + d.orbIds = finalLights.toString(); + d.useOrbSmoothing = (eV("useOrbSmoothing") == true); + + d.output = conf_editor.getEditor("root.specificOptions.output").getValue(); + d.port = parseInt(conf_editor.getEditor("root.specificOptions.port").getValue()); + d.latchTime = parseInt(conf_editor.getEditor("root.specificOptions.latchTime").getValue());; + + window.serverConfig.device = d; + + requestWriteConfig(window.serverConfig, true); + resetWizard(); + }); + + $('#btn_wiz_abort').off().on('click', resetWizard); +} + +function getIdInLights(id) { + return lights.filter( + function(lights) { + return lights.id === id + } + ); +} + +async function discover_atmoorb_lights(multiCastGroup, multiCastPort){ + + var light = {}; + + if ( multiCastGroup === "" ) + multiCastGroup = "239.255.255.250"; + + if ( multiCastPort === "") + multiCastPort = 49692; + + let params = { multiCastGroup : multiCastGroup, multiCastPort : multiCastPort}; + + // Get discovered lights + const res = await requestLedDeviceDiscovery ('atmoorb', params); + + // TODO: error case unhandled + // res can be: false (timeout) or res.error (not found) + if(res && !res.error){ + const r = res.info + + // Process devices returned by discovery + for(const device of r.devices) + { + if( device.id !== "") + { + if ( getIdInLights ( device.id ).length === 0 ) + { + light = {}; + light.id = device.id; + light.ip = device.ip; + light.host = device.hostname; + lights.push(light); + } + } + } + + // Add additional items from configuration + for(const keyConfig in configuredLights) + { + if ( configuredLights[keyConfig] !== "" && !isNaN(configuredLights[keyConfig]) ) + { + if ( getIdInLights ( configuredLights[keyConfig] ).length === 0 ) + { + light = {}; + light.id = configuredLights[keyConfig]; + light.ip = ""; + light.host = ""; + lights.push(light); + } + } + } + + lights.sort((a, b) => (a.id > b.id) ? 1 : -1); + + assign_atmoorb_lights(); + } +} + +function assign_atmoorb_lights(){ + + // If records are left for configuration + if(Object.keys(lights).length > 0) + { + $('#wh_topcontainer').toggle(false); + $('#orb_ids_t, #btn_wiz_save').toggle(true); + + var lightOptions = [ + "top", "topleft", "topright", + "bottom", "bottomleft", "bottomright", + "left", "lefttop", "leftmiddle", "leftbottom", + "right", "righttop", "rightmiddle", "rightbottom", + "entire" + ]; + + lightOptions.unshift("disabled"); + + $('.lidsb').html(""); + var pos = ""; + + for(var lightid in lights) + { + var orbId = lights[lightid].id; + var orbIp = lights[lightid].ip; + var orbHostname = lights[lightid].host; + + if ( orbHostname === "" ) + orbHostname = $.i18n('edt_dev_spec_lights_itemtitle'); + + var options = ""; + for(var opt in lightOptions) + { + var val = lightOptions[opt]; + var txt = (val !== 'entire' && val !== 'disabled') ? 'conf_leds_layout_cl_' : 'wiz_ids_'; + options+= ''; + } + + var lightAnnotation =""; + if ( orbIp !== "" ) + { + lightAnnotation = ': '+orbIp+'
('+orbHostname+')'; + } + + $('.lidsb').append(createTableRow([orbId + lightAnnotation, '',''])); + } + + $('.orb_sel_watch').bind("change", function(){ + var cC = 0; + for(var key in lights) + { + if($('#orb_'+key).val() !== "disabled") + { + cC++; + } + } + if ( cC === 0) + $('#btn_wiz_save').attr("disabled",true); + else + $('#btn_wiz_save').attr("disabled",false); + }); + $('.orb_sel_watch').trigger('change'); + } + else + { + var noLightsTxt = '

'+$.i18n('wiz_atmoorb_noLights')+'

'; + $('#wizp2_body').append(noLightsTxt); + } +} + +function identify_atmoorb_device(orbId){ + + let params = { id : orbId }; + + const res = requestLedDeviceIdentification ("atmoorb", params); + // TODO: error case unhandled + // res can be: false (timeout) or res.error (not found) + if(res && !res.error){ + const r = res.info + } +} + //**************************** // Wizard/Routines Nanoleaf //**************************** diff --git a/libsrc/leddevice/LedDevice.cpp b/libsrc/leddevice/LedDevice.cpp index 4229f2da..e1adcce5 100644 --- a/libsrc/leddevice/LedDevice.cpp +++ b/libsrc/leddevice/LedDevice.cpp @@ -165,7 +165,10 @@ void LedDevice::startRefreshTimer() void LedDevice::stopRefreshTimer() { - _refreshTimer->stop(); + if ( _refreshTimer != nullptr ) + { + _refreshTimer->stop(); + } } int LedDevice::updateLeds(const std::vector& ledValues) diff --git a/libsrc/leddevice/dev_net/LedDeviceAtmoOrb.cpp b/libsrc/leddevice/dev_net/LedDeviceAtmoOrb.cpp index e9dceed0..f8c1a06d 100644 --- a/libsrc/leddevice/dev_net/LedDeviceAtmoOrb.cpp +++ b/libsrc/leddevice/dev_net/LedDeviceAtmoOrb.cpp @@ -4,20 +4,30 @@ // qt includes #include +#include +#include +#include -const quint16 MULTICAST_GROUPL_DEFAULT_PORT = 49692; -const int LEDS_DEFAULT_NUMBER = 24; +#include + +// Constants +namespace { + +const QString MULTICAST_GROUP_DEFAULT_ADDRESS = "239.255.255.250"; +const quint16 MULTICAST_GROUP_DEFAULT_PORT = 49692; + +constexpr std::chrono::milliseconds DEFAULT_DISCOVERY_TIMEOUT{2000}; + +} //End of constants LedDeviceAtmoOrb::LedDeviceAtmoOrb(const QJsonObject &deviceConfig) : LedDevice(deviceConfig) , _udpSocket (nullptr) - , _multiCastGroupPort (MULTICAST_GROUPL_DEFAULT_PORT) + , _multicastGroup(MULTICAST_GROUP_DEFAULT_ADDRESS) + , _multiCastGroupPort (MULTICAST_GROUP_DEFAULT_PORT) , _joinedMulticastgroup (false) , _useOrbSmoothing (false) - , _transitiontime (0) , _skipSmoothingDiff (0) - , _numLeds (LEDS_DEFAULT_NUMBER) - { } @@ -38,14 +48,24 @@ bool LedDeviceAtmoOrb::init(const QJsonObject &deviceConfig) if ( LedDevice::init(deviceConfig) ) { - _multicastGroup = deviceConfig["output"].toString().toStdString().c_str(); + _multicastGroup = deviceConfig["output"].toString(MULTICAST_GROUP_DEFAULT_ADDRESS); + _multiCastGroupPort = static_cast(deviceConfig["port"].toInt(MULTICAST_GROUP_DEFAULT_PORT)); _useOrbSmoothing = deviceConfig["useOrbSmoothing"].toBool(false); - _transitiontime = deviceConfig["transitiontime"].toInt(0); _skipSmoothingDiff = deviceConfig["skipSmoothingDiff"].toInt(0); - _multiCastGroupPort = static_cast(deviceConfig["port"].toInt(MULTICAST_GROUPL_DEFAULT_PORT)); - _numLeds = deviceConfig["numLeds"].toInt(LEDS_DEFAULT_NUMBER); - QStringList orbIds = QStringUtils::split(deviceConfig["orbIds"].toString().simplified().remove(" "),",", QStringUtils::SplitBehavior::SkipEmptyParts); + + Debug(_log, "DeviceType : %s", QSTRING_CSTR( this->getActiveDeviceType() )); + Debug(_log, "LedCount : %u", this->getLedCount()); + Debug(_log, "ColorOrder : %s", QSTRING_CSTR( this->getColorOrder() )); + Debug(_log, "RefreshTime : %d", _refreshTimerInterval_ms); + Debug(_log, "LatchTime : %d", this->getLatchTime()); + + Debug(_log, "MulticastGroup : %s", QSTRING_CSTR(_multicastGroup)); + Debug(_log, "MulticastGroupPort: %d", _multiCastGroupPort); + Debug(_log, "Orb ID list : %s", QSTRING_CSTR(deviceConfig["orbIds"].toString())); + Debug(_log, "Use Orb Smoothing : %d", _useOrbSmoothing); + Debug(_log, "Skip SmoothingDiff: %d", _skipSmoothingDiff); + _orbIds.clear(); for (auto & id_str : orbIds) @@ -69,6 +89,9 @@ bool LedDeviceAtmoOrb::init(const QJsonObject &deviceConfig) } } + uint numberOrbs = _orbIds.size(); + uint configuredLedCount = this->getLedCount(); + if ( _orbIds.empty() ) { this->setInError("No valid OrbIds found!"); @@ -76,8 +99,23 @@ bool LedDeviceAtmoOrb::init(const QJsonObject &deviceConfig) } else { - _udpSocket = new QUdpSocket(this); - isInitOK = true; + if ( numberOrbs < configuredLedCount ) + { + QString errorReason = QString("Not enough Orbs [%1] for configured LEDs [%2] found!") + .arg(numberOrbs) + .arg(configuredLedCount); + this->setInError(errorReason); + isInitOK = false; + } + else + { + if ( numberOrbs > configuredLedCount ) + { + Info(_log, "%s: More Orbs [%u] than configured LEDs [%u].", QSTRING_CSTR(this->getActiveDeviceType()), numberOrbs, configuredLedCount ); + } + + isInitOK = true; + } } } return isInitOK; @@ -88,30 +126,39 @@ int LedDeviceAtmoOrb::open() int retval = -1; _isDeviceReady = false; + if ( _udpSocket == nullptr ) + { + _udpSocket = new QUdpSocket(); + } + // Try to bind the UDP-Socket if ( _udpSocket != nullptr ) { - _groupAddress = QHostAddress(_multicastGroup); - if ( !_udpSocket->bind(QHostAddress::AnyIPv4, _multiCastGroupPort, QUdpSocket::ShareAddress | QUdpSocket::ReuseAddressHint) ) + if ( _udpSocket->state() != QAbstractSocket::BoundState ) { - QString errortext = QString ("(%1) %2, MulticastGroup: (%3)").arg(_udpSocket->error()).arg(_udpSocket->errorString(), _multicastGroup); - this->setInError( errortext ); - } - else - { - _joinedMulticastgroup = _udpSocket->joinMulticastGroup(_groupAddress); - if ( !_joinedMulticastgroup ) + if ( !_udpSocket->bind(QHostAddress(QHostAddress::AnyIPv4), 0 ) ) { - QString errortext = QString ("(%1) %2, MulticastGroup: (%3)").arg(_udpSocket->error()).arg(_udpSocket->errorString(), _multicastGroup); + QString errortext = QString ("Socket bind failed: (%1) %2, MulticastGroup: (%3)").arg(_udpSocket->error()).arg(_udpSocket->errorString(), _multicastGroup); this->setInError( errortext ); } else { - // Everything is OK, device is ready - _isDeviceReady = true; - retval = 0; + _groupAddress = QHostAddress(_multicastGroup); + _joinedMulticastgroup = _udpSocket->joinMulticastGroup(_groupAddress); + if ( !_joinedMulticastgroup ) + { + QString errortext = QString ("Joining Multicastgroup failed: (%1) %2, MulticastGroup: (%3)").arg(_udpSocket->error()).arg(_udpSocket->errorString(), _multicastGroup); + this->setInError( errortext ); + } } } + + if ( ! _isDeviceInError ) + { + // Everything is OK, device is ready + _isDeviceReady = true; + retval = 0; + } } return retval; } @@ -123,6 +170,11 @@ int LedDeviceAtmoOrb::close() if ( _udpSocket != nullptr ) { + if ( _udpSocket->state() == QAbstractSocket::BoundState ) + { + _udpSocket->leaveMulticastGroup(_groupAddress); + } + // Test, if device requires closing if ( _udpSocket->isOpen() ) { @@ -155,12 +207,17 @@ int LedDeviceAtmoOrb::write(const std::vector &ledValues) commandType = 2; } - // Iterate through colors and set Orb color - // Start off with idx 1 as 0 is reserved for controlling all orbs at once - int idx = 1; - - for (const ColorRgb &color : ledValues) + ColorRgb color; + for (int idx = 0; idx < _orbIds.size(); idx++ ) { + if ( idx < static_cast(ledValues.size()) ) + { + color = ledValues[idx]; + } + else + { + color = ColorRgb::BLACK; + } // Retrieve last send colors int lastRed = lastColorRedMap[idx]; int lastGreen = lastColorGreenMap[idx]; @@ -171,33 +228,16 @@ int LedDeviceAtmoOrb::write(const std::vector &ledValues) abs(color.green - lastGreen) >= _skipSmoothingDiff)) { // Skip Orb smoothing when using (command type 4) - for (int i = 0; i < _orbIds.size(); i++) - { - if (_orbIds[i] == idx) - { - setColor(idx, color, 4); - } - } - } - else - { - // Send color - for (int i = 0; i < _orbIds.size(); i++) - { - if (_orbIds[i] == idx) - { - setColor(idx, color, commandType); - } - } + commandType = 4; } + // Send color + setColor(_orbIds[idx], color, commandType); + // Store last colors send for light id lastColorRedMap[idx] = color.red; lastColorGreenMap[idx] = color.green; lastColorBlueMap[idx] = color.blue; - - // Next light id. - idx++; } return 0; @@ -227,12 +267,117 @@ void LedDeviceAtmoOrb::setColor(int orbId, const ColorRgb &color, int commandTyp bytes[6] = static_cast(color.green); bytes[7] = static_cast(color.blue); - //std::cout << "Orb [" << orbId << "] Cmd [" << bytes.toHex(':').toStdString() <<"]"<< std::endl; - sendCommand(bytes); } void LedDeviceAtmoOrb::sendCommand(const QByteArray &bytes) { + //Debug ( _log, "command: [%s] -> %s:%u", QSTRING_CSTR( QString(bytes.toHex())), QSTRING_CSTR(_groupAddress.toString()), _multiCastGroupPort ); _udpSocket->writeDatagram(bytes.data(), bytes.size(), _groupAddress, _multiCastGroupPort); } + +QJsonObject LedDeviceAtmoOrb::discover() +{ + QJsonObject devicesDiscovered; + devicesDiscovered.insert("ledDeviceType", _activeDeviceType ); + + QJsonArray deviceList; + + if ( open() == 0 ) + { + Debug ( _log, "Send discovery requests to all AtmoOrbs" ); + setColor(0, ColorRgb::BLACK, 8); + + if ( _udpSocket->waitForReadyRead(DEFAULT_DISCOVERY_TIMEOUT.count()) ) + { + while (_udpSocket->waitForReadyRead(500)) + { + QByteArray datagram; + + while (_udpSocket->hasPendingDatagrams()) + { + datagram.resize(_udpSocket->pendingDatagramSize()); + QHostAddress senderIP; + quint16 senderPort; + + _udpSocket->readDatagram(datagram.data(), datagram.size(), &senderIP, &senderPort); + + if ( datagram.size() == 1 ) + { + unsigned char orbId = datagram[0]; + if ( orbId > 0 ) + { + Debug(_log, "Orb ID (%d) discovered at [%s]", orbId, QSTRING_CSTR(senderIP.toString())); + _services.insert(orbId, senderIP); + } + } + } + } + } + + close(); + } + + QMap::iterator i; + for (i = _services.begin(); i != _services.end(); ++i) + { + QJsonObject obj; + + obj.insert("id", i.key()); + obj.insert("ip", i.value().toString()); + + QHostInfo hostInfo = QHostInfo::fromName(i.value().toString()); + if (hostInfo.error() == QHostInfo::NoError ) + { + QString hostname = hostInfo.hostName(); + //Seems that for Windows no local domain name is resolved + if (!hostInfo.localDomainName().isEmpty() ) + { + obj.insert("hostname", hostname.remove("."+hostInfo.localDomainName())); + obj.insert("domain", hostInfo.localDomainName()); + } + else + { + int domainPos = hostname.indexOf('.'); + obj.insert("hostname", hostname.left(domainPos)); + obj.insert("domain", hostname.mid(domainPos+1)); + } + } + + deviceList << obj; + } + + devicesDiscovered.insert("devices", deviceList); + Debug(_log, "devicesDiscovered: [%s]", QString(QJsonDocument(devicesDiscovered).toJson(QJsonDocument::Compact)).toUtf8().constData() ); + + return devicesDiscovered; +} + +void LedDeviceAtmoOrb::identify(const QJsonObject& params) +{ + //Debug(_log, "params: [%s]", QString(QJsonDocument(params).toJson(QJsonDocument::Compact)).toUtf8().constData()); + + int orbId = 0; + if ( params["id"].isString() ) + { + orbId = params["id"].toString().toInt(); + } + else + { + orbId = params["id"].toInt(); + } + + if ( orbId >0 && orbId < 256 ) + { + Debug (_log, "Orb ID [%d]", orbId); + if ( open() == 0 ) + { + setColor(orbId, ColorRgb::BLACK, 9); + close(); + } + } + else + { + Warning(_log, "Identification of Orb with ID='%d' skipped. ID must be in range 1-255", orbId); + } +} diff --git a/libsrc/leddevice/dev_net/LedDeviceAtmoOrb.h b/libsrc/leddevice/dev_net/LedDeviceAtmoOrb.h index 3f1a9bb6..ce2f966b 100644 --- a/libsrc/leddevice/dev_net/LedDeviceAtmoOrb.h +++ b/libsrc/leddevice/dev_net/LedDeviceAtmoOrb.h @@ -11,13 +11,10 @@ class QUdpSocket; -/** - * Implementation for the AtmoOrb - * - * To use set the device to "atmoorb". - * - * @author RickDB (github) - */ +/// +/// Implementation of the LedDevice interface for sending to +/// AtmoOrb devices via network +/// class LedDeviceAtmoOrb : public LedDevice { Q_OBJECT @@ -43,6 +40,27 @@ public: /// static LedDevice* construct(const QJsonObject &deviceConfig); + /// + /// @brief Discover AtmoOrb devices available (for configuration). + /// + /// @return A JSON structure holding a list of devices found + /// + virtual QJsonObject discover() override; + + /// + /// @brief Send an update to the AtmoOrb device to identify it. + /// + /// Following parameters are required + /// @code + /// { + /// "orbId" : "orb identifier in the range of (1-255)", + /// } + ///@endcode + /// + /// @param[in] params Parameters to address device + /// + virtual void identify(const QJsonObject& params) override; + protected: /// @@ -111,15 +129,9 @@ private: /// use Orbs own (external) smoothing algorithm bool _useOrbSmoothing; - /// Transition time between colors (not implemented) - int _transitiontime; - // Maximum allowed color difference, will skip Orb (external) smoothing once reached int _skipSmoothingDiff; - /// Number of leds in Orb, used to determine buffer size - int _numLeds; - /// Array of the orb ids. QVector _orbIds; @@ -127,6 +139,8 @@ private: QMap lastColorRedMap; QMap lastColorGreenMap; QMap lastColorBlueMap; + + QMultiMap _services; }; #endif // LEDEVICEATMOORB_H diff --git a/libsrc/leddevice/dev_net/LedDeviceNanoleaf.cpp b/libsrc/leddevice/dev_net/LedDeviceNanoleaf.cpp index 5b45a4d5..2db5dcd0 100644 --- a/libsrc/leddevice/dev_net/LedDeviceNanoleaf.cpp +++ b/libsrc/leddevice/dev_net/LedDeviceNanoleaf.cpp @@ -319,7 +319,7 @@ bool LedDeviceNanoleaf::initLedsConfiguration() { if ( _panelLedCount > this->getLedCount() ) { - Info(_log, "Nanoleaf: More panels [%u] than configured LEDs [%u].", _panelLedCount, configuredLedCount ); + Info(_log, "%s: More panels [%u] than configured LEDs [%u].", QSTRING_CSTR(this->getActiveDeviceType()), _panelLedCount, configuredLedCount ); } // Check, if start position + number of configured LEDs is greater than number of panels available @@ -449,9 +449,7 @@ QJsonObject LedDeviceNanoleaf::getProperties(const QJsonObject& params) void LedDeviceNanoleaf::identify(const QJsonObject& params) { Debug(_log, "params: [%s]", QString(QJsonDocument(params).toJson(QJsonDocument::Compact)).toUtf8().constData() ); - QJsonObject properties; - // Get Nanoleaf device properties QString host = params["host"].toString(""); if ( !host.isEmpty() ) { diff --git a/libsrc/leddevice/dev_net/LedDeviceWled.cpp b/libsrc/leddevice/dev_net/LedDeviceWled.cpp index 4884e3f3..7255d2ea 100644 --- a/libsrc/leddevice/dev_net/LedDeviceWled.cpp +++ b/libsrc/leddevice/dev_net/LedDeviceWled.cpp @@ -235,9 +235,7 @@ void LedDeviceWled::identify(const QJsonObject& /*params*/) { #if 0 Debug(_log, "params: [%s]", QString(QJsonDocument(params).toJson(QJsonDocument::Compact)).toUtf8().constData()); - QJsonObject properties; - // Get Nanoleaf device properties QString host = params["host"].toString(""); if ( !host.isEmpty() ) { diff --git a/libsrc/leddevice/dev_net/LedDeviceYeelight.cpp b/libsrc/leddevice/dev_net/LedDeviceYeelight.cpp index 2fe39504..968b4561 100644 --- a/libsrc/leddevice/dev_net/LedDeviceYeelight.cpp +++ b/libsrc/leddevice/dev_net/LedDeviceYeelight.cpp @@ -1357,7 +1357,7 @@ QJsonObject LedDeviceYeelight::discover() QJsonArray deviceList; - // Discover WLED Devices + // Discover Yeelight Devices SSDPDiscover discover; discover.setPort(SSDP_PORT); discover.skipDuplicateKeys(true); diff --git a/libsrc/leddevice/schemas/schema-adalight.json b/libsrc/leddevice/schemas/schema-adalight.json index e127f7b6..3d42e4cf 100644 --- a/libsrc/leddevice/schemas/schema-adalight.json +++ b/libsrc/leddevice/schemas/schema-adalight.json @@ -5,26 +5,29 @@ "output": { "type": "string", "title":"edt_dev_spec_outputPath_title", - "default":"ttyACM0", + "default":"auto", "propertyOrder" : 1 }, "rate": { "type": "integer", "title":"edt_dev_spec_baudrate_title", - "default": 1000000, + "default": 115200, + "access" : "advanced", "propertyOrder" : 2 }, "delayAfterConnect": { "type": "integer", "title":"edt_dev_spec_delayAfterConnect_title", - "default": 1500, + "default": 0, "append" : "ms", + "access" : "expert", "propertyOrder" : 3 }, "lightberry_apa102_mode": { "type": "boolean", "title":"edt_dev_spec_LBap102Mode_title", "default": false, + "access" : "advanced", "propertyOrder" : 4 }, "latchTime": { diff --git a/libsrc/leddevice/schemas/schema-atmoorb.json b/libsrc/leddevice/schemas/schema-atmoorb.json index ba0cb012..0f7b6e25 100644 --- a/libsrc/leddevice/schemas/schema-atmoorb.json +++ b/libsrc/leddevice/schemas/schema-atmoorb.json @@ -2,22 +2,24 @@ "type":"object", "required":true, "properties":{ - "output": { - "type": "string", - "title":"edt_dev_spec_multicastGroup_title", - "default" : "239.15.18.2", - "propertyOrder" : 1 - }, "orbIds": { "type": "string", "title":"edt_dev_spec_orbIds_title", - "default": "1", + "default": "", + "propertyOrder" : 1 + }, + "useOrbSmoothing": { + "type": "boolean", + "title":"edt_dev_spec_useOrbSmoothing_title", + "default": true, + "access" : "advanced", "propertyOrder" : 2 }, - "numLeds": { - "type": "integer", - "title":"edt_dev_spec_numberOfLeds_title", - "default": 24, + "output": { + "type": "string", + "title":"edt_dev_spec_multicastGroup_title", + "default" : "239.255.255.250", + "access" : "expert", "propertyOrder" : 3 }, "port": { @@ -25,25 +27,20 @@ "title":"edt_dev_spec_port_title", "minimum" : 0, "maximum" : 65535, - "default": 49.692, + "default": 49692, + "access" : "expert", "propertyOrder" : 4 - }, - "useOrbSmoothing": { - "type": "boolean", - "title":"edt_dev_spec_useOrbSmoothing_title", - "default": true, - "propertyOrder" : 5 - }, + }, "latchTime": { "type": "integer", "title":"edt_dev_spec_latchtime_title", - "default": 0, + "default": 30, "append" : "edt_append_ms", "minimum": 0, "maximum": 1000, "access" : "expert", - "propertyOrder" : 6 - } + "propertyOrder" : 5 + } }, "additionalProperties": true } diff --git a/libsrc/leddevice/schemas/schema-yeelight.json b/libsrc/leddevice/schemas/schema-yeelight.json index 6635e0ab..d91f7675 100644 --- a/libsrc/leddevice/schemas/schema-yeelight.json +++ b/libsrc/leddevice/schemas/schema-yeelight.json @@ -47,9 +47,9 @@ "type": "integer", "title":"edt_dev_spec_transistionTimeExtra_title", "default" : 0, - "step": 10, + "step": 100, "minimum" : 0, - "maximum" : 3000, + "maximum" : 8000, "append" : "ms", "access" : "advanced", "propertyOrder" : 4