diff --git a/CHANGELOG.md b/CHANGELOG.md index 99c19d8e..60dbb706 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Breaking ### Added +- Cololight support (Cololight Plus & Strip) incl. configuration wizard +- Provide additional details on Hardware/CPU information +- Allow execution with option "--version", while another hyperion daemon is running - New language support: Russian and Chinese (simplified) (#1005) - added libcec to deb/rpm dependency list - updated some language files @@ -21,8 +24,7 @@ Allow execution with option "--version", while another hyperion daemon is runnin - DirectX9 Grabber (#1039) - Added DirectX SDK to CompileHowto - Hide Systray on exit & Install DirectX Redistributable - -- Read-Only configuration database support +- Read-Only configuration database suppor ### Changed - boblight: reduce cpu time spent on memcopy and parsing rgb values (#1016) @@ -33,6 +35,8 @@ Allow execution with option "--version", while another hyperion daemon is runnin - Update LICENSE - Change links from http to https (#1067) +- UI: Separate LED-Layout creation from UI code + ### Fixed - Properly save Hue light state between sessions (#1014) - AVAHI included in Webserver (#996) diff --git a/assets/webconfig/i18n/en.json b/assets/webconfig/i18n/en.json index 5e6f3d9d..fdf6db78 100644 --- a/assets/webconfig/i18n/en.json +++ b/assets/webconfig/i18n/en.json @@ -855,7 +855,6 @@ "update_versreminder": "Your version: $1", "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_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 optimise the Hyperion settings automatically! So in short: All you need are some clicks and you are done!", - "wiz_atmoorb_noLights": "No AtmoOrbs found! Please get the lights connected to the network or configure them manually.", "wiz_atmoorb_title": "AtmoOrb Wizard", "wiz_cc_adjustgamma": "Gamma: What you have to do is, adjust gamma levels of each channel until you have the same perceived amount of each channel. Hint: Neutral is 1.0! For example, if your Grey is a bit reddish it means that you have to increase red gamma to reduce the amount of red (the more gamma, the less amount of color).", "wiz_cc_adjustit": "Adjust your \"$1\", until your are fine with it. Take notice: The more you adjust away from the default value the color spectrum will be limited (Also for all colors in between). Depending on TV/LED color spectrum the results will vary.", @@ -879,6 +878,10 @@ "wiz_cc_testintrok": "Push on a button below to start a test video.", "wiz_cc_testintrowok": "Check out the following link to download test videos:", "wiz_cc_title": "Colour calibration wizard", + "wiz_cololight_desc2": "Now choose which Cololights should be added. To identify single lights, press the button on the right.", + "wiz_cololight_intro1": "This wizards configures Hyperion for the Cololight system. Features are the Cololight auto detection and tune the Hyperion settings automatically! In short: All you need are some clicks and you are done!
Note: In case of Cololight Strip, you might need to manually correct the LED count and layout.", + "wiz_cololight_noprops": "Not able to get device properties - Define Hardware LED count manually", + "wiz_cololight_title": "Cololight Wizard", "wiz_guideyou": "The $1 will guide you through the settings. Just press the button!", "wiz_hue_blinkblue": "Let ID $1 light up blue", "wiz_hue_clientkey": "Clientkey:", @@ -912,6 +915,7 @@ "wiz_identify_light": "Identify $1", "wiz_ids_disabled": "Deactivated", "wiz_ids_entire": "Whole picture", + "wiz_noLights": "No $1 found! Please get the lights connected to the network or configure them manually.", "wiz_pos": "Position/State", "wiz_rgb_expl": "The color dot switches every x seconds the color (red, green), at the same time your LEDs switch the color too. Answer the questions at the bottom to check/correct your byte order.", "wiz_rgb_intro1": "This wizard will guide you through the finding process of the correct color order for your leds. Click on continue to begin.", @@ -924,7 +928,6 @@ "wiz_wizavail": "Wizard available", "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_intro1": "This wizards configures Hyperion for the Yeelight system. Features are the Yeelighs' 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_yeelight_noLights": "No Yeelights found! Please get the lights connected to the network or configure them manually.", "wiz_yeelight_title": "Yeelight Wizard", "wiz_yeelight_unsupported": "Unsupported" } diff --git a/assets/webconfig/js/content_leds.js b/assets/webconfig/js/content_leds.js index 7080286c..d2c365ff 100644 --- a/assets/webconfig/js/content_leds.js +++ b/assets/webconfig/js/content_leds.js @@ -59,40 +59,36 @@ function createLedPreview(leds, origin){ } -function createClassicLeds(){ - //get values - var ledstop = parseInt($("#ip_cl_top").val()); - var ledsbottom = parseInt($("#ip_cl_bottom").val()); - var ledsleft = parseInt($("#ip_cl_left").val()); - var ledsright = parseInt($("#ip_cl_right").val()); - var ledsglength = parseInt($("#ip_cl_glength").val()); - var ledsgpos = parseInt($("#ip_cl_gpos").val()); - var position = parseInt($("#ip_cl_position").val()); - var reverse = $("#ip_cl_reverse").is(":checked"); +function createClassicLedLayoutSimple( ledstop,ledsleft,ledsright,ledsbottom,position,reverse ){ - //advanced values - var ledsVDepth = parseInt($("#ip_cl_vdepth").val())/100; - var ledsHDepth = parseInt($("#ip_cl_hdepth").val())/100; - var edgeVGap = parseInt($("#ip_cl_edgegap").val())/100/2; - //var cornerVGap = parseInt($("#ip_cl_cornergap").val())/100/2; - var overlap = $("#ip_cl_overlap").val()/100; + let params = { + ledstop: 0, ledsleft: 0, ledsright: 0, ledsbottom: 0, + ledsglength: 0, ledsgpos: 0, position: 0, + ledsHDepth: 0.08, ledsVDepth: 0.05, overlap: 0, + edgeVGap: 0, + ptblh: 0, ptblv: 1, ptbrh: 1, ptbrv: 1, + pttlh: 0, pttlv: 0, pttrh: 1, pttrv: 0, + reverse:false + }; + + params.ledstop = ledstop; + params.ledsleft = ledsleft; + params.ledsright = ledsright; + params.ledsbottom = ledsbottom; + params.position = position; + params.reverse = reverse; - //trapezoid values % -> float - var ptblh = parseInt($("#ip_cl_pblh").val())/100; - var ptblv = parseInt($("#ip_cl_pblv").val())/100; - var ptbrh = parseInt($("#ip_cl_pbrh").val())/100; - var ptbrv = parseInt($("#ip_cl_pbrv").val())/100; - var pttlh = parseInt($("#ip_cl_ptlh").val())/100; - var pttlv = parseInt($("#ip_cl_ptlv").val())/100; - var pttrh = parseInt($("#ip_cl_ptrh").val())/100; - var pttrv = parseInt($("#ip_cl_ptrv").val())/100; + return createClassicLedLayout( params ); +} + +function createClassicLedLayout( params ){ //helper - var edgeHGap = edgeVGap/(16/9); + var edgeHGap = params.edgeVGap/(16/9); var ledArray = []; function createFinalArray(array){ - finalLedArray = []; + var finalLedArray = []; for(var i = 0; i-1; i--){ - var hmin = ovl("-",ptblh+(steph*Number([i]))+edgeHGap); - var hmax = ovl("+",ptblh+(steph*Number([i+1]))+edgeHGap); - var vmax= ptblv+(stepv*Number([i])); - var vmin = vmax-ledsHDepth; + for (var i = params.ledsbottom-1; i>-1; i--){ + var hmin = ovl("-",params.ptblh+(steph*Number([i]))+edgeHGap); + var hmax = ovl("+",params.ptblh+(steph*Number([i+1]))+edgeHGap); + var vmax= params.ptblv+(stepv*Number([i])); + var vmin = vmax-params.ledsHDepth; createLedArray(hmin, hmax, vmin, vmax); } } function createLeftLeds(){ - var steph = (ptblh - pttlh)/ledsleft; - var stepv = (ptblv - pttlv - (2*edgeVGap))/ledsleft; + var steph = (params.ptblh - params.pttlh)/params.ledsleft; + var stepv = (params.ptblv - params.pttlv - (2*params.edgeVGap))/params.ledsleft; - for (var i = ledsleft-1; i>-1; i--){ - var hmin = pttlh+(steph*Number([i])); - var hmax = hmin+ledsVDepth; - var vmin = ovl("-",pttlv+(stepv*Number([i]))+edgeVGap); - var vmax = ovl("+",pttlv+(stepv*Number([i+1]))+edgeVGap); + for (var i = params.ledsleft-1; i>-1; i--){ + var hmin = params.pttlh+(steph*Number([i])); + var hmax = hmin+params.ledsVDepth; + var vmin = ovl("-",params.pttlv+(stepv*Number([i]))+params.edgeVGap); + var vmax = ovl("+",params.pttlv+(stepv*Number([i+1]))+params.edgeVGap); createLedArray(hmin, hmax, vmin, vmax); } @@ -202,47 +198,87 @@ function createClassicLeds(){ createRightLeds(); createBottomLeds(); createLeftLeds(); - + //check led gap pos - if (ledsgpos+ledsglength > ledArray.length) + if (params.ledsgpos+params.ledsglength > ledArray.length) { - var mpos = Math.max(0,ledArray.length-ledsglength); - $('#ip_cl_ledsgpos').val(mpos); + var mpos = Math.max(0,ledArray.length-params.ledsglength); + //$('#ip_cl_ledsgpos').val(mpos); ledsgpos = mpos; } //check led gap length - if(ledsglength >= ledArray.length) + if(params.ledsglength >= ledArray.length) { - $('#ip_cl_ledsglength').val(ledArray.length-1); - ledsglength = ledArray.length-ledsglength-1; + //$('#ip_cl_ledsglength').val(ledArray.length-1); + params.ledsglength = ledArray.length-params.ledsglength-1; } - if(ledsglength != 0){ - ledArray.splice(ledsgpos, ledsglength); + if(params.ledsglength != 0){ + ledArray.splice(params.ledsgpos, params.ledsglength); } - if (position != 0){ - rotateArray(ledArray, position); + if (params.position != 0){ + rotateArray(ledArray, params.position); } - if (reverse) + if (params.reverse) ledArray.reverse(); - createFinalArray(ledArray); + return createFinalArray(ledArray); } -function createMatrixLeds(){ +function createClassicLeds(){ + + //get values + let params = { + ledstop : parseInt($("#ip_cl_top").val()), + ledsbottom : parseInt($("#ip_cl_bottom").val()), + ledsleft : parseInt($("#ip_cl_left").val()), + ledsright : parseInt($("#ip_cl_right").val()), + ledsglength : parseInt($("#ip_cl_glength").val()), + ledsgpos : parseInt($("#ip_cl_gpos").val()), + position : parseInt($("#ip_cl_position").val()), + reverse : $("#ip_cl_reverse").is(":checked"), + + //advanced values + ledsVDepth : parseInt($("#ip_cl_vdepth").val())/100, + ledsHDepth : parseInt($("#ip_cl_hdepth").val())/100, + edgeVGap : parseInt($("#ip_cl_edgegap").val())/100/2, + //cornerVGap : parseInt($("#ip_cl_cornergap").val())/100/2, + overlap : $("#ip_cl_overlap").val()/100, + + //trapezoid values % -> float + ptblh : parseInt($("#ip_cl_pblh").val())/100, + ptblv : parseInt($("#ip_cl_pblv").val())/100, + ptbrh : parseInt($("#ip_cl_pbrh").val())/100, + ptbrv : parseInt($("#ip_cl_pbrv").val())/100, + pttlh : parseInt($("#ip_cl_ptlh").val())/100, + pttlv : parseInt($("#ip_cl_ptlv").val())/100, + pttrh : parseInt($("#ip_cl_ptrh").val())/100, + pttrv : parseInt($("#ip_cl_ptrv").val())/100, + } + + finalLedArray = createClassicLedLayout( params ); + + //check led gap pos + if (params.ledsgpos+params.ledsglength > finalLedArray.length) { + var mpos = Math.max(0,finalLedArray.length-params.ledsglength); + $('#ip_cl_ledsgpos').val(mpos); + } + //check led gap length + if(params.ledsglength >= finalLedArray.length) { + $('#ip_cl_ledsglength').val(finalLedArray.length-1); + } + + createLedPreview(finalLedArray, 'classic'); +} + + +function createMatrixLayout( ledshoriz, ledsvert, cabling, start){ // Big thank you to RanzQ (Juha Rantanen) from Github for this script // https://raw.githubusercontent.com/RanzQ/hyperion-audio-effects/master/matrix-config.js - //get values - var ledshoriz = parseInt($("#ip_ma_ledshoriz").val()); - var ledsvert = parseInt($("#ip_ma_ledsvert").val()); - var cabling = $("#ip_ma_cabling").val(); - //var order = $("#ip_ma_order").val(); - var start = $("#ip_ma_start").val(); - var parallel = false var leds = [] var hblock = 1.0 / ledshoriz @@ -298,9 +334,23 @@ function createMatrixLeds(){ endX = tmp } } - finalLedArray =[]; - finalLedArray = leds - createLedPreview(leds, 'matrix'); + + return leds; +} + + +function createMatrixLeds(){ +// Big thank you to RanzQ (Juha Rantanen) from Github for this script +// https://raw.githubusercontent.com/RanzQ/hyperion-audio-effects/master/matrix-config.js + + //get values + var ledshoriz = parseInt($("#ip_ma_ledshoriz").val()); + var ledsvert = parseInt($("#ip_ma_ledsvert").val()); + var cabling = $("#ip_ma_cabling").val(); + var start = $("#ip_ma_start").val(); + + finalLedArray = createMatrixLayout(ledshoriz,ledsvert,cabling,start); + createLedPreview(finalLedArray, 'matrix'); } function migrateLedConfig(slConfig){ @@ -560,6 +610,12 @@ $(document).ready(function() { var atmoorb_title = 'wiz_atmoorb_title'; changeWizard(data, atmoorb_title, startWizardAtmoOrb); } + else if(ledType == "cololight") { + var ledWizardType = (this.checked) ? "cololight" : ledType; + var data = { type: ledWizardType }; + var cololight_title = 'wiz_cololight_title'; + changeWizard(data, cololight_title, startWizardCololight); + } else if(ledType == "yeelight") { var ledWizardType = (this.checked) ? "yeelight" : ledType; var data = { type: ledWizardType }; @@ -582,10 +638,11 @@ $(document).ready(function() { var devRPiSPI = ['apa102', 'apa104', 'ws2801', 'lpd6803', 'lpd8806', 'p9813', 'sk6812spi', 'sk6822spi', 'sk9822', 'ws2812spi']; var devRPiPWM = ['ws281x']; var devRPiGPIO = ['piblaster']; - var devNET = ['atmoorb', 'fadecandy', 'philipshue', 'nanoleaf', 'tinkerforge', 'tpm2net', 'udpe131', 'udpartnet', 'udph801', 'udpraw', 'wled', 'yeelight']; + + var devNET = ['atmoorb', 'cololight', 'fadecandy', 'philipshue', 'nanoleaf', 'tinkerforge', 'tpm2net', 'udpe131', 'udpartnet', 'udph801', 'udpraw', 'wled', 'yeelight']; var devUSB = ['adalight', 'dmx', 'atmo', 'hyperionusbasp', 'lightpack', 'paintpack', 'rawhid', 'sedu', 'tpm2', 'karate']; - var optArr = [[]]; + var optArr = [[]]; optArr[1]=[]; optArr[2]=[]; optArr[3]=[]; diff --git a/assets/webconfig/js/wizard.js b/assets/webconfig/js/wizard.js index 1c62e885..73eb0568 100644 --- a/assets/webconfig/js/wizard.js +++ b/assets/webconfig/js/wizard.js @@ -152,9 +152,11 @@ function beginWizardRGB() { $('#btn_wizard_byteorder').off().on('click', startWizardRGB); //color calibration wizard + var kodiHost = document.location.hostname; var kodiPort = 9090; var kodiAddress = kodiHost; + var wiz_editor; var colorLength; var cobj; @@ -168,6 +170,7 @@ var picnr = 0; var availVideos = ["Sweet_Cocoon", "Caminandes_2_GranDillama", "Caminandes_3_Llamigos"]; if (getStorage("kodiAddress") != null) { + kodiAddress = getStorage("kodiAddress"); [kodiHost, kodiPort] = kodiAddress.split(":", 2); @@ -585,6 +588,30 @@ function assignLightPos(id, pos, name) { return i; } +function getHostInLights(hostname) { + return lights.filter( + function (lights) { + return lights.host === hostname + } + ); +} + +function getIpInLights(ip) { + return lights.filter( + function (lights) { + return lights.ip === ip + } + ); +} + +function getIdInLights(id) { + return lights.filter( + function (lights) { + return lights.id === id + } + ); +} + //**************************** // Wizard Philips Hue //**************************** @@ -798,17 +825,8 @@ async function getProperties_hue_bridge(hostAddress, username, resourceFilter) { } function identify_hue_device(hostAddress, username, id) { - console.log("identify_hue_device"); - let params = { host: hostAddress, user: username, lightId: id }; - - const res = requestLedDeviceIdentification("philipshue", params); - // TODO: error case unhandled - // res can be: false (timeout) or res.error (not found) - if (res && !res.error) { - const r = res.info - console.log(r); - } + requestLedDeviceIdentification("philipshue", params); } function getHueIPs() { @@ -1271,14 +1289,7 @@ async function getProperties_wled(hostAddress, resourceFilter) { function identify_wled(hostAddress) { let params = { host: hostAddress }; - - const res = requestLedDeviceIdentification("wled", params); - // TODO: error case unhandled - // res can be: false (timeout) or res.error (not found) - if (res && !res.error) { - const r = res.info - console.log(r); - } + requestLedDeviceIdentification("wled", params); } //**************************** @@ -1390,14 +1401,6 @@ function beginWizardYeelight() { $('#btn_wiz_abort').off().on('click', resetWizard); } -function getHostInLights(hostname) { - return lights.filter( - function (lights) { - return lights.host === hostname - } - ); -} - async function discover_yeelight_lights() { var light = {}; // Get discovered lights @@ -1414,10 +1417,9 @@ async function discover_yeelight_lights() { if (device.hostname !== "") { if (getHostInLights(device.hostname).length === 0) { - light = {}; + var light = {}; light.host = device.hostname; light.port = device.port; - light.name = device.other.name; light.model = device.other.model; lights.push(light); @@ -1435,7 +1437,7 @@ async function discover_yeelight_lights() { if (host !== "") if (getHostInLights(host).length === 0) { - light = {}; + var light = {}; light.host = host; light.port = port; light.name = configuredLights[keyConfig].name; @@ -1454,7 +1456,7 @@ function assign_yeelight_lights() { // If records are left for configuration if (Object.keys(lights).length > 0) { $('#wh_topcontainer').toggle(false); - $('#yee_ids_t,#btn_wiz_save').toggle(true); + $('#yee_ids_t, #btn_wiz_save').toggle(true); var lightOptions = [ "top", "topleft", "topright", @@ -1486,7 +1488,7 @@ function assign_yeelight_lights() { options += '>' + $.i18n(txt + val) + ''; } - var enabled = 'enabled' + var enabled = 'enabled'; if (!models.includes(lights[lightid].model)) { var enabled = 'disabled'; options = ''; @@ -1514,7 +1516,7 @@ function assign_yeelight_lights() { $('.yee_sel_watch').trigger('change'); } else { - var noLightsTxt = '

' + $.i18n('wiz_yeelight_noLights') + '

'; + var noLightsTxt = '

' + $.i18n('wiz_noLights','Yeelights') + '

'; $('#wizp2_body').append(noLightsTxt); } } @@ -1535,13 +1537,8 @@ async function getProperties_yeelight(hostname, port) { } function identify_yeelight_device(hostname, port) { - let params = { hostname: hostname, port: port }; - const res = requestLedDeviceIdentification("yeelight", params); - // TODO: error case unhandled - // res can be: false (timeout) or res.error (not found) - if (res && !res.error) { - //const r = res.info; - } + let params = { hostname: hostname, port: port + requestLedDeviceIdentification("yeelight", params); } //**************************** @@ -1648,14 +1645,6 @@ function beginWizardAtmoOrb() { $('#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 = {}; @@ -1673,13 +1662,13 @@ async function discover_atmoorb_lights(multiCastGroup, multiCastPort) { // TODO: error case unhandled // res can be: false (timeout) or res.error (not found) if (res && !res.error) { - const r = res.info - + 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 = {}; + var light = {}; light.id = device.id; light.ip = device.ip; light.host = device.hostname; @@ -1692,7 +1681,7 @@ async function discover_atmoorb_lights(multiCastGroup, multiCastPort) { for (const keyConfig in configuredLights) { if (configuredLights[keyConfig] !== "" && !isNaN(configuredLights[keyConfig])) { if (getIdInLights(configuredLights[keyConfig]).length === 0) { - light = {}; + var light = {}; light.id = configuredLights[keyConfig]; light.ip = ""; light.host = ""; @@ -1743,9 +1732,9 @@ function assign_atmoorb_lights() { options += '>' + $.i18n(txt + val) + ''; } - var enabled = 'enabled' + var enabled = 'enabled'; if (orbId < 1 || orbId > 255) { - enabled = 'disabled' + enabled = 'disabled'; options = ''; } @@ -1775,20 +1764,14 @@ function assign_atmoorb_lights() { $('.orb_sel_watch').trigger('change'); } else { - var noLightsTxt = '

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

'; + var noLightsTxt = '

' + $.i18n('wiz_noLights','AtmoOrbs') + '

'; $('#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 - } + requestLedDeviceIdentification("atmoorb", params); } //**************************** @@ -1837,16 +1820,197 @@ async function getProperties_nanoleaf(hostAddress, authToken, resourceFilter) { function identify_nanoleaf(hostAddress, authToken) { let params = { host: hostAddress, token: authToken }; + requestLedDeviceIdentification("nanoleaf", params); +} + +//**************************** +// Wizard Cololight +//**************************** +var lights = null; +var selectedLightId = null; + +function startWizardCololight(e) { + //create html + + var cololight_title = 'wiz_cololight_title'; + var cololight_intro1 = 'wiz_cololight_intro1'; + + $('#wiz_header').html('' + $.i18n(cololight_title)); + $('#wizp1_body').html('

' + $.i18n(cololight_title) + '

' + $.i18n(cololight_intro1) + '

'); + $('#wizp1_footer').html(''); + + $('#wizp2_body').html('
'); + + $('#wh_topcontainer').append(''); + + $('#wizp2_body').append(''); + + createTable("lidsh", "lidsb", "colo_ids_t"); + $('.lidsh').append(createTableRow([$.i18n('edt_dev_spec_lights_title'), $.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 () { + beginWizardCololight(); + $('#wizp1').toggle(false); + $('#wizp2').toggle(true); + }); +} + +function beginWizardCololight() { + lights = []; + + discover_cololights(); + + $('#btn_wiz_save').off().on("click", function () { + //LED device config + //Start with a clean configuration + var d = {}; + + d.type = 'cololight'; + + //Cololight does not resolve into stable hostnames (as devices named the same), therefore use IP + if (!lights[selectedLightId].ip) { + d.host = lights[selectedLightId].host; + } else { + d.host = lights[selectedLightId].ip; + } + + var coloLightProperties = lights[selectedLightId].props; + if (Object.keys(coloLightProperties).length === 0) { + alert($.i18n('wiz_cololight_noprops')); + d.hardwareLedCount = 1; + } else { + if (coloLightProperties.ledCount > 0) { + d.hardwareLedCount = coloLightProperties.ledCount; + } else if (coloLightProperties.modelType === "Strip") + d.hardwareLedCount = 120; + } + + d.colorOrder = conf_editor.getEditor("root.generalOptions.colorOrder").getValue(); + d.latchTime = parseInt(conf_editor.getEditor("root.specificOptions.latchTime").getValue());; + + window.serverConfig.device = d; + + //LED layout - have initial layout prepared matching the LED-count + + var coloLightLedConfig = []; + + if (coloLightProperties.modelType === "Strip") { + coloLightLedConfig = createClassicLedLayoutSimple(d.hardwareLedCount / 2, d.hardwareLedCount / 4, d.hardwareLedCount / 4, 0, d.hardwareLedCount / 4 * 3, false); + } else { + coloLightLedConfig = createClassicLedLayoutSimple(0, 0, 0, d.hardwareLedCount, 0, true); + } + + window.serverConfig.leds = coloLightLedConfig; + + //smoothing off + window.serverConfig.smoothing.enable = false; + + requestWriteConfig(window.serverConfig, true); + + resetWizard(); + }); + + $('#btn_wiz_abort').off().on('click', resetWizard); +} + +async function discover_cololights() { + const res = await requestLedDeviceDiscovery('cololight'); - const res = requestLedDeviceIdentification("nanoleaf", params); - // TODO: error case unhandled - // res can be: false (timeout) or res.error (not found) if (res && !res.error) { - const r = res.info - console.log(r); + const r = res.info; + + // Process devices returned by discovery + for (const device of r.devices) { + if (device.ip !== "") { + if (getIpInLights(device.ip).length === 0) { + var light = {}; + light.ip = device.ip; + light.host = device.hostname; + light.name = device.name; + light.type = device.type; + lights.push(light); + } + } + } + assign_cololight_lights(); } } +function assign_cololight_lights() { + // If records are left for configuration + if (Object.keys(lights).length > 0) { + $('#wh_topcontainer').toggle(false); + $('#colo_ids_t, #btn_wiz_save').toggle(true); + + $('.lidsb').html(""); + + var options = ""; + + for (var lightid in lights) { + lights[lightid].id = lightid; + + var lightHostname = lights[lightid].host; + var lightIP = lights[lightid].ip; + + var val = lightHostname + " (" + lightIP + ")"; + options += ''; + } + + var enabled = 'enabled'; + + $('.lidsb').append(createTableRow(['', ''])); + + $('.colo_sel_watch').bind("change", function () { + selectedLightId = $('#colo_select_id').val(); + var lightIP = lights[selectedLightId].ip; + + $('#wiz_identify_btn').unbind().bind('click', function (event) { identify_cololight_device(lightIP); }); + + if (!lights[selectedLightId].props) { + getProperties_cololight(lightIP); + } + }); + $('.colo_sel_watch').trigger('change'); + } + else { + var noLightsTxt = '

' + $.i18n('wiz_noLights','Cololights') + '

'; + $('#wizp2_body').append(noLightsTxt); + } +} + +async function getProperties_cololight(ip) { + let params = { host: ip }; + + const res = await requestLedDeviceProperties('cololight', params); + + if (res && !res.error) { + var coloLightProperties = res.info; + + //Store properties along light with given IP-address + var id = getIpInLights(ip)[0].id; + lights[id].props = coloLightProperties; + } +} + +function identify_cololight_device(hostAddress) { + let params = { host: hostAddress }; + requestLedDeviceIdentification("cololight", params); +} + //**************************** // Wizard/Routines RS232-Devices //**************************** diff --git a/include/leddevice/LedDevice.h b/include/leddevice/LedDevice.h index a9dcf0c5..b7c9dfac 100644 --- a/include/leddevice/LedDevice.h +++ b/include/leddevice/LedDevice.h @@ -60,7 +60,7 @@ public: /// /// @brief Set the number of LEDs supported by the device. /// - /// @param[in] ledCount Number of device LEDs + /// @param[in] ledCount Number of device LEDs, 0 = unknown number /// void setLedCount(unsigned int ledCount); @@ -191,7 +191,7 @@ public slots: /// /// @brief Get the number of LEDs supported by the device. /// - /// @return Number of device's LEDs + /// @return Number of device's LEDs, 0 = unknown number /// unsigned int getLedCount() const { return _ledCount; } @@ -350,6 +350,14 @@ protected: /// @return array as string of hex values QString uint8_t_to_hex_string(const uint8_t * data, const qint64 size, qint64 number = -1) const; + /// + /// @brief Converts a ByteArray to hex string. + /// + /// @param data ByteArray + /// @param number Number of array items to be converted. + /// @return array as string of hex values + QString toHex(const QByteArray& data, int number = -1) const; + /// Current device's type QString _activeDeviceType; @@ -368,17 +376,17 @@ protected: // Device configuration parameters - /// Number of hardware LEDs supported by device. - unsigned int _ledCount; - unsigned int _ledRGBCount; - unsigned int _ledRGBWCount; - /// Refresh interval in milliseconds int _refreshTimerInterval_ms; /// Time a device requires mandatorily between two writes (in milliseconds) int _latchTime_ms; + /// Number of hardware LEDs supported by device. + uint _ledCount; + uint _ledRGBCount; + uint _ledRGBWCount; + /// Does the device allow restoring the original state? bool _isRestoreOrigState; diff --git a/libsrc/leddevice/LedDevice.cpp b/libsrc/leddevice/LedDevice.cpp index 3d1906e9..77484ede 100644 --- a/libsrc/leddevice/LedDevice.cpp +++ b/libsrc/leddevice/LedDevice.cpp @@ -24,6 +24,7 @@ LedDevice::LedDevice(const QJsonObject& deviceConfig, QObject* parent) , _refreshTimer(nullptr) , _refreshTimerInterval_ms(0) , _latchTime_ms(0) + , _ledCount(0) , _isRestoreOrigState(false) , _isEnabled(false) , _isDeviceInitialised(false) @@ -454,3 +455,17 @@ QString LedDevice::uint8_t_to_hex_string(const uint8_t * data, const qint64 size return bytes.toHex(); #endif } + +QString LedDevice::toHex(const QByteArray& data, int number) const +{ + if ( number <= 0 || number > data.size()) + { + number = data.size(); + } + +#if (QT_VERSION >= QT_VERSION_CHECK(5, 9, 0)) + return data.left(number).toHex(':'); +#else + return data.left(number).toHex(); +#endif +} diff --git a/libsrc/leddevice/LedDeviceSchemas.qrc b/libsrc/leddevice/LedDeviceSchemas.qrc index 5722e62a..a07064c5 100644 --- a/libsrc/leddevice/LedDeviceSchemas.qrc +++ b/libsrc/leddevice/LedDeviceSchemas.qrc @@ -35,5 +35,6 @@ schemas/schema-nanoleaf.json schemas/schema-wled.json schemas/schema-yeelight.json + schemas/schema-cololight.json diff --git a/libsrc/leddevice/dev_net/LedDeviceCololight.cpp b/libsrc/leddevice/dev_net/LedDeviceCololight.cpp new file mode 100644 index 00000000..68e25cd3 --- /dev/null +++ b/libsrc/leddevice/dev_net/LedDeviceCololight.cpp @@ -0,0 +1,772 @@ +#include "LedDeviceCololight.h" + +#include +#include +#include +#include +#include + +#include + +// Constants +namespace { +const bool verbose = false; +const bool verbose3 = false; + +// Configuration settings + +const char CONFIG_HW_LED_COUNT[] = "hardwareLedCount"; + +// Cololight discovery service + +const int API_DEFAULT_PORT = 8900; + +const char DISCOVERY_ADDRESS[] = "255.255.255.255"; +const quint16 DISCOVERY_PORT = 12345; +const char DISCOVERY_MESSAGE[] = "Z-SEARCH * \r\n"; +constexpr std::chrono::milliseconds DEFAULT_DISCOVERY_TIMEOUT{ 5000 }; +constexpr std::chrono::milliseconds DEFAULT_READ_TIMEOUT{ 1000 }; +constexpr std::chrono::milliseconds DEFAULT_IDENTIFY_TIME{ 2000 }; + +const char COLOLIGHT_MODEL[] = "mod"; +const char COLOLIGHT_MODEL_TYPE[] = "subkey"; +const char COLOLIGHT_MAC[] = "sn"; +const char COLOLIGHT_NAME[] = "name"; + +const char COLOLIGHT_MODEL_IDENTIFIER[] = "OD_WE_QUAN"; + +const int COLOLIGHT_BEADS_PER_MODULE = 19; +const int COLOLIGHT_MIN_STRIP_SEGMENT_SIZE = 30; + +enum verbs { + GET = 0x03, + SET = 0x04, + SETEEPROM = 0x07, + SETVAR = 0x0b +}; + +enum commandTypes { + STATE_OFF = 0x80, + STATE_ON = 0x81, + BRIGTHNESS = 0xCF, + SETCOLOR = 0xFF +}; + +enum idxTypes { + BRIGTHNESS_CONTROL = 0x01, + COLOR_CONTROL = 0x02, + COLOR_DIRECT_CONTROL = 0x81, + READ_INFO_FROM_STORAGE = 0x86 +}; + + enum bufferMode { + MONOCROME = 0x01, + LIGHTBEAD = 0x02, + }; + +enum ledLayout { + STRIP_LAYOUT, + MODLUE_LAYOUT +}; + +enum modelType { + STRIP, + PLUS +}; + +const uint8_t PACKET_HEADER[] = + { + 'S', 'Z', // Tag "SZ" + 0x30, 0x30, // Version "00" + 0x00, 0x00, // AppID, 0x0000 = TL1 command mode + 0x00, 0x00, 0x00, 0x00 // Size +}; + +const uint8_t PACKET_SECU[] = + { + 0x00, 0x00, 0x00, 0x00, // Dict + 0x00, 0x00, 0x00, 0x00, // Sum + 0x00, 0x00, 0x00, 0x00, // Salt + 0x00, 0x00, 0x00, 0x00 // SN +}; + +const uint8_t TL1_CMD_FIXED_PART[] = + { + 0x00, 0x00, 0x00, 0x00, // DISTID + 0x00, 0x00, 0x00, 0x00, // SRCID + 0x00, // SECU + 0x00, // VERB + 0x00, // CTAG + 0x00 // LENGTH +}; +} //End of constants + +LedDeviceCololight::LedDeviceCololight(const QJsonObject& deviceConfig) + : ProviderUdp(deviceConfig) + , _modelType(-1) + , _ledLayoutType(STRIP_LAYOUT) + , _ledBeadCount(0) + , _distance(0) + , _sequenceNumber(1) +{ + _packetFixPart.append(reinterpret_cast(PACKET_HEADER), sizeof(PACKET_HEADER)); + _packetFixPart.append(reinterpret_cast(PACKET_SECU), sizeof(PACKET_SECU)); +} + +LedDevice* LedDeviceCololight::construct(const QJsonObject& deviceConfig) +{ + return new LedDeviceCololight(deviceConfig); +} + +bool LedDeviceCololight::init(const QJsonObject& deviceConfig) +{ + bool isInitOK = false; + + _port = API_DEFAULT_PORT; + + if (ProviderUdp::init(deviceConfig)) + { + // Initialise LedDevice configuration and execution environment + Debug(_log, "DeviceType : %s", QSTRING_CSTR(this->getActiveDeviceType())); + Debug(_log, "ColorOrder : %s", QSTRING_CSTR(this->getColorOrder())); + Debug(_log, "LatchTime : %d", this->getLatchTime()); + + if (initLedsConfiguration()) + { + initDirectColorCmdTemplate(); + isInitOK = true; + } + } + return isInitOK; +} + +bool LedDeviceCololight::initLedsConfiguration() +{ + bool isInitOK = false; + + if (!getInfo()) + { + QString errorReason = QString("Cololight device (%1) not accessible to get additional properties!") + .arg(getAddress().toString()); + setInError(errorReason); + } + else + { + QString modelTypeText; + + switch (_modelType) { + case 0: + modelTypeText = "Strip"; + _ledLayoutType = STRIP_LAYOUT; + break; + case 1: + _ledLayoutType = MODLUE_LAYOUT; + modelTypeText = "Plus"; + break; + default: + _modelType = STRIP; + modelTypeText = "Strip"; + _ledLayoutType = STRIP_LAYOUT; + Info(_log, "Model not identified, assuming Cololight %s", QSTRING_CSTR(modelTypeText)); + break; + } + Debug(_log, "Model type : %s", QSTRING_CSTR(modelTypeText)); + + if (getLedCount() == 0) + { + setLedCount(static_cast(_devConfig[CONFIG_HW_LED_COUNT].toInt(0))); + } + + if (_modelType == STRIP && (getLedCount() % COLOLIGHT_MIN_STRIP_SEGMENT_SIZE != 0)) + { + QString errorReason = QString("Hardware LED count must be multiple of %1 for Cololight Strip!") + .arg(COLOLIGHT_MIN_STRIP_SEGMENT_SIZE); + this->setInError(errorReason); + } + else + { + Debug(_log, "LedCount : %d", getLedCount()); + + uint configuredLedCount = static_cast(_devConfig["currentLedCount"].toInt(1)); + + if (getLedCount() < configuredLedCount) + { + QString errorReason = QString("Not enough LEDs [%1] for configured LEDs in layout [%2] found!") + .arg(getLedCount()) + .arg(configuredLedCount); + this->setInError(errorReason); + } + else + { + if (getLedCount() > configuredLedCount) + { + Info(_log, "%s: More LEDs [%u] than configured LEDs in layout [%u].", QSTRING_CSTR(this->getActiveDeviceType()), getLedCount(), configuredLedCount); + } + isInitOK = true; + } + } + } + + return isInitOK; +} + +void LedDeviceCololight::initDirectColorCmdTemplate() +{ + int ledNumber = static_cast(this->getLedCount()); + + _directColorCommandTemplate.clear(); + + //Packet + _directColorCommandTemplate.append(static_cast(bufferMode::LIGHTBEAD)); // idx + + int beads = 1; + if (_ledLayoutType == MODLUE_LAYOUT) + { + beads = COLOLIGHT_BEADS_PER_MODULE; + } + + for (int i = 0; i < ledNumber; ++i) + { + _directColorCommandTemplate.append(static_cast(i * beads + 1)); + _directColorCommandTemplate.append(static_cast(i * beads + beads)); + _directColorCommandTemplate.append(3, static_cast(0x00)); + } +} + +bool LedDeviceCololight::getInfo() +{ + bool isCmdOK = false; + + QByteArray command; + + const quint8 packetSize = 2; + int fixPartsize = sizeof(TL1_CMD_FIXED_PART); + + command.resize(sizeof(TL1_CMD_FIXED_PART) + packetSize); + command.fill('\0'); + + command[fixPartsize - 3] = static_cast(SETVAR); // verb + command[fixPartsize - 2] = static_cast(_sequenceNumber); // ctag + command[fixPartsize - 1] = static_cast(packetSize); // length + + //Packet + command[fixPartsize] = static_cast(READ_INFO_FROM_STORAGE); // idx + command[fixPartsize + 1] = static_cast(0x01); // idx + + if (sendRequest(TL1_CMD, command)) + { + QByteArray response; + if (readResponse(response)) + { + DebugIf(verbose, _log, "#[0x%x], Data returned: [%s]", _sequenceNumber, QSTRING_CSTR(toHex(response))); + + quint16 ledNum = qFromBigEndian(response.data() + 1); + + if (ledNum != 0xFFFF) + { + _ledBeadCount = ledNum; + if (ledNum % COLOLIGHT_BEADS_PER_MODULE == 0) + { + _modelType = MODLUE_LAYOUT; + _distance = ledNum / COLOLIGHT_BEADS_PER_MODULE; + setLedCount(static_cast(_distance)); + } + } + else + { + _modelType = STRIP; + setLedCount(0); + } + + Debug(_log, "#LEDs found [0x%x], [%u], distance [%d]", _ledBeadCount, _ledBeadCount, _distance); + + isCmdOK = true; + } + } + + return isCmdOK; +} + +bool LedDeviceCololight::setEffect(const effect effect) +{ + return setColor(static_cast(effect)); +} + +bool LedDeviceCololight::setColor(const ColorRgb colorRgb) +{ + uint32_t color = colorRgb.blue | (colorRgb.green << 8) | (colorRgb.red << 16) | (0x00 << 24); + + return setColor(color); +} + +bool LedDeviceCololight::setColor(const uint32_t color) +{ + bool isCmdOK = false; + + QByteArray command; + + const quint8 packetSize = 6; + int fixPartsize = sizeof(TL1_CMD_FIXED_PART); + + command.resize(sizeof(TL1_CMD_FIXED_PART) + packetSize); + command.fill('\0'); + + command[fixPartsize - 3] = static_cast(SET); // verb + command[fixPartsize - 2] = static_cast(_sequenceNumber); // ctag + command[fixPartsize - 1] = static_cast(packetSize); // length + + //Packet + command[fixPartsize] = static_cast(0x02); // idx + command[fixPartsize + 1] = static_cast(0xff); // set color or dynamic effect + + qToBigEndian(color, command.data() + fixPartsize + 2); + + if (sendRequest(TL1_CMD, command)) + { + QByteArray response; + if (readResponse(response)) + { + DebugIf(verbose, _log, "#[0x%x], Data returned: [%s]", _sequenceNumber, QSTRING_CSTR(toHex(response))); + isCmdOK = true; + } + } + + return isCmdOK; +} + +bool LedDeviceCololight::setState(bool isOn) +{ + bool isCmdOK = false; + + quint8 type = isOn ? STATE_ON : STATE_OFF; + + QByteArray command; + + const quint8 packetSize = 3; + int fixPartsize = sizeof(TL1_CMD_FIXED_PART); + + command.resize(sizeof(TL1_CMD_FIXED_PART) + packetSize); + command.fill('\0'); + + command[fixPartsize - 3] = static_cast(SET); // verb + command[fixPartsize - 2] = static_cast(_sequenceNumber); // ctag + command[fixPartsize - 1] = static_cast(packetSize); // length + + //Packet + command[fixPartsize] = static_cast(BRIGTHNESS_CONTROL); // idx + command[fixPartsize + 1] = static_cast(type); // type + command[fixPartsize + 2] = static_cast(isOn); // value + + if (sendRequest(TL1_CMD, command)) + { + QByteArray response; + if (readResponse(response)) + { + DebugIf(verbose, _log, "#[0x%x], Data returned: [%s]", _sequenceNumber, QSTRING_CSTR(toHex(response))); + isCmdOK = true; + } + } + + return isCmdOK; +} + +bool LedDeviceCololight::setStateDirect(bool isOn) +{ + bool isCmdOK = false; + + QByteArray command; + + //Packet + command.append(static_cast(0x04)); // idx + command.append(static_cast(isOn)); // idx + command.append(static_cast(0xd7)); // idx + + if (sendRequest(DIRECT_CONTROL, command)) + { + QByteArray response; + if (readResponse(response)) + { + DebugIf(verbose, _log, "#[0x%x], Data returned: [%s]", _sequenceNumber, QSTRING_CSTR(toHex(response))); + isCmdOK = true; + } + } + + return isCmdOK; +} + +bool LedDeviceCololight::setColor(const std::vector& ledValues) +{ + int ledNumber = static_cast(ledValues.size()); + + QByteArray command = _directColorCommandTemplate; + + //Update LED values, start from offset (mode + first start/stop pair) = 3 + for (int i = 0; i < ledNumber; ++i) + { + command[3 + i * 5] = static_cast(ledValues[i].red); + command[3 + i * 5 + 1] = static_cast(ledValues[i].green); + command[3 + i * 5 + 2] = static_cast(ledValues[i].blue); + } + + bool isCmdOK = sendRequest(DIRECT_CONTROL, command); + + return isCmdOK; +} + +bool LedDeviceCololight::setTL1CommandMode(bool isOn) +{ + bool isCmdOK = false; + + quint8 type = isOn ? STATE_ON : STATE_OFF; + + QByteArray command; + + const quint8 packetSize = 2; + int fixPartsize = sizeof(TL1_CMD_FIXED_PART); + + command.resize(sizeof(TL1_CMD_FIXED_PART) + packetSize); + command.fill('\0'); + + command[fixPartsize - 3] = static_cast(SETEEPROM); // verb + command[fixPartsize - 2] = static_cast(_sequenceNumber); // ctag + command[fixPartsize - 1] = static_cast(packetSize); // length + + //Packet + command[fixPartsize] = static_cast(COLOR_CONTROL); // idx + command[fixPartsize + 1] = static_cast(type); // type + + if (sendRequest(TL1_CMD, command)) + { + QByteArray response; + if (readResponse(response)) + { + DebugIf(verbose, _log, "#[0x%x], Data returned: [%s]", _sequenceNumber, QSTRING_CSTR(toHex(response))); + isCmdOK = true; + } + } + + return isCmdOK; +} + +bool LedDeviceCololight::sendRequest(const appID appID, const QByteArray& command) +{ + bool isSendOK = true; + QByteArray packet(_packetFixPart); + packet.append(static_cast(_sequenceNumber)); + packet.append(command); + + quint32 size = sizeof(PACKET_SECU) + 1 + command.size(); + + qToBigEndian(appID, packet.data() + 4); + + qToBigEndian(size, packet.data() + 6); + + ++_sequenceNumber; + + DebugIf(verbose3, _log, "packet: ([0x%x], [%u])[%s]", size, size, QSTRING_CSTR(toHex(packet, 64))); + + if (writeBytes(packet) < 0) + { + isSendOK = false; + } + + return isSendOK; +} + +bool LedDeviceCololight::readResponse() +{ + QByteArray response; + return readResponse(response); +} + +bool LedDeviceCololight::readResponse(QByteArray& response) +{ + bool isRequestOK = false; + if (_udpSocket->waitForReadyRead(DEFAULT_READ_TIMEOUT.count())) + { + while (_udpSocket->waitForReadyRead(200)) + { + QByteArray datagram; + + while (_udpSocket->hasPendingDatagrams()) + { + datagram.resize(static_cast(_udpSocket->pendingDatagramSize())); + QHostAddress senderIP; + quint16 senderPort; + + _udpSocket->readDatagram(datagram.data(), datagram.size(), &senderIP, &senderPort); + + if (datagram.size() >= 10) + { + DebugIf(verbose3, _log, "response: [%s]", QSTRING_CSTR(toHex(datagram, 64))); + + quint16 appID = qFromBigEndian(datagram.mid(4, sizeof(appID))); + + if (verbose && appID == 0x8000) + { + QString tagVersion = datagram.left(2); + quint32 packetSize = qFromBigEndian(datagram.mid(sizeof(PACKET_HEADER) - sizeof(packetSize))); + + Debug(_log, "Response HEADER: tagVersion [%s], appID: [0x%.2x][%u], packet size: [0x%.4x][%u]", QSTRING_CSTR(tagVersion), appID, appID, packetSize, packetSize); + + quint32 dictionary = qFromBigEndian(datagram.mid(sizeof(PACKET_HEADER))); + quint32 checkSum = qFromBigEndian(datagram.mid(sizeof(PACKET_HEADER) + sizeof(dictionary))); + quint32 salt = qFromBigEndian(datagram.mid(sizeof(PACKET_HEADER) + sizeof(dictionary) + sizeof(checkSum), sizeof(salt))); + quint32 sequenceNumber = qFromBigEndian(datagram.mid(sizeof(PACKET_HEADER) + sizeof(dictionary) + sizeof(checkSum) + sizeof(salt))); + + Debug(_log, "Response SECU : Dict: [0x%.4x][%u], Sum: [0x%.4x][%u], Salt: [0x%.4x][%u], SN: [0x%.4x][%u]", dictionary, dictionary, checkSum, checkSum, salt, salt, sequenceNumber, sequenceNumber); + + quint8 packetSN = static_cast(datagram.at(sizeof(PACKET_HEADER) + sizeof(PACKET_SECU))); + Debug(_log, "Response packSN: [0x%.4x][%u]", packetSN, packetSN); + } + + quint8 errorCode = static_cast(datagram.at(sizeof(PACKET_HEADER) + sizeof(PACKET_SECU) + 1)); + + int dataPartStart = sizeof(PACKET_HEADER) + sizeof(PACKET_SECU) + sizeof(TL1_CMD_FIXED_PART); + + if (errorCode != 0) + { + quint8 originalVerb = static_cast(datagram.at(dataPartStart - 2) - 0x80); + quint8 originalRequestPacketSN = static_cast(datagram.at(dataPartStart - 1)); + + if (errorCode == 16) + { + //TL1 Command failure + Error(_log, "Request [0x%x] failed =with error [%u], appID [%u], originalVerb [0x%x]", originalRequestPacketSN, errorCode, appID, originalVerb); + } + else + { + Error(_log, "Request [0x%x] failed with error [%u], appID [%u]", originalRequestPacketSN, errorCode, appID); + } + } + else + { + // TL1 Protocol + if (appID == 0x8000) + { + if (dataPartStart < datagram.size()) + { + quint8 dataLength = static_cast(datagram.at(dataPartStart)); + + response = datagram.mid(dataPartStart + 1, dataLength); + if (verbose) + { + quint8 originalVerb = static_cast(datagram.at(dataPartStart - 2) - 0x80); + Debug(_log, "Cmd [0x%x], Data returned: [%s]", originalVerb, QSTRING_CSTR(toHex(response))); + } + } + else + { + DebugIf(verbose, _log, "No additional data returned"); + } + } + isRequestOK = true; + } + } + } + } + } + return isRequestOK; +} + +int LedDeviceCololight::write(const std::vector& ledValues) +{ + int rc = -1; + + if (setColor(ledValues)) + { + rc = 0; + } + + return rc; +} + +bool LedDeviceCololight::powerOn() +{ + bool on = true; + if (_isDeviceReady) + { + if (!setState(false) || !setTL1CommandMode(false)) + { + on = false; + } + } + return on; +} + +bool LedDeviceCololight::powerOff() +{ + bool off = true; + if (_isDeviceReady) + { + writeBlack(); + off = setStateDirect(false); + setTL1CommandMode(false); + } + return off; +} + +QJsonObject LedDeviceCololight::discover() +{ + QJsonObject devicesDiscovered; + devicesDiscovered.insert("ledDeviceType", _activeDeviceType); + + QJsonArray deviceList; + + QUdpSocket udpSocket; + + udpSocket.writeDatagram(QString(DISCOVERY_MESSAGE).toUtf8(), QHostAddress(DISCOVERY_ADDRESS), DISCOVERY_PORT); + + if (udpSocket.waitForReadyRead(DEFAULT_DISCOVERY_TIMEOUT.count())) + { + while (udpSocket.waitForReadyRead(500)) + { + QByteArray datagram; + + while (udpSocket.hasPendingDatagrams()) + { + datagram.resize(static_cast(udpSocket.pendingDatagramSize())); + QHostAddress senderIP; + quint16 senderPort; + + udpSocket.readDatagram(datagram.data(), datagram.size(), &senderIP, &senderPort); + + QString data(datagram); + + QMap headers; + // parse request + QStringList entries = QStringUtils::split(data, "\n", QStringUtils::SplitBehavior::SkipEmptyParts); + for (auto entry : entries) + { + // split into key=value, be aware that value field may contain also a "=" + entry = entry.simplified(); + int pos = entry.indexOf("="); + if (pos == -1) + { + continue; + } + + const QString key = entry.left(pos).trimmed().toLower(); + const QString value = entry.mid(pos + 1).trimmed(); + headers[key] = value; + } + + if (headers.value("mod") == COLOLIGHT_MODEL_IDENTIFIER) + { + QString ipAddress = QHostAddress(senderIP.toIPv4Address()).toString(); + _services.insert(ipAddress, headers); + + Debug(_log, "Cololight discovered at [%s]", QSTRING_CSTR(ipAddress)); + DebugIf(verbose3, _log, "_data: [%s]", QSTRING_CSTR(data)); + } + } + } + } + + QMap>::iterator i; + for (i = _services.begin(); i != _services.end(); ++i) + { + QJsonObject obj; + + obj.insert("ip", i.key()); + obj.insert("model", i.value().value(COLOLIGHT_MODEL)); + obj.insert("type", i.value().value(COLOLIGHT_MODEL_TYPE)); + obj.insert("mac", i.value().value(COLOLIGHT_MAC)); + obj.insert("name", i.value().value(COLOLIGHT_NAME)); + + QHostInfo hostInfo = QHostInfo::fromName(i.key()); + if (hostInfo.error() == QHostInfo::NoError) + { + QString hostname = hostInfo.hostName(); + //Seems that for Windows no local domain name is resolved + if (!QHostInfo::localDomainName().isEmpty()) + { + obj.insert("hostname", hostname.remove("." + QHostInfo::localDomainName())); + obj.insert("domain", QHostInfo::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); + DebugIf(verbose, _log, "devicesDiscovered: [%s]", QString(QJsonDocument(devicesDiscovered).toJson(QJsonDocument::Compact)).toUtf8().constData()); + + return devicesDiscovered; +} + +QJsonObject LedDeviceCololight::getProperties(const QJsonObject& params) +{ + DebugIf(verbose, _log, "params: [%s]", QString(QJsonDocument(params).toJson(QJsonDocument::Compact)).toUtf8().constData()); + QJsonObject properties; + + QString apiHostname = params["host"].toString(""); + quint16 apiPort = static_cast(params["port"].toInt(API_DEFAULT_PORT)); + + if (!apiHostname.isEmpty()) + { + QJsonObject deviceConfig; + + deviceConfig.insert("host", apiHostname); + deviceConfig.insert("port", apiPort); + if (ProviderUdp::init(deviceConfig)) + { + if (getInfo()) + { + QString modelTypeText; + + switch (_modelType) { + case 1: + modelTypeText = "Plus"; + break; + default: + modelTypeText = "Strip"; + break; + } + properties.insert("modelType", modelTypeText); + properties.insert("ledCount", static_cast(getLedCount())); + properties.insert("ledBeadCount", _ledBeadCount); + properties.insert("distance", _distance); + } + } + } + + DebugIf(verbose, _log, "properties: [%s]", QString(QJsonDocument(properties).toJson(QJsonDocument::Compact)).toUtf8().constData()); + + return properties; +} + +void LedDeviceCololight::identify(const QJsonObject& params) +{ + DebugIf(verbose, _log, "params: [%s]", QString(QJsonDocument(params).toJson(QJsonDocument::Compact)).toUtf8().constData()); + + QString apiHostname = params["host"].toString(""); + quint16 apiPort = static_cast(params["port"].toInt(API_DEFAULT_PORT)); + + if (!apiHostname.isEmpty()) + { + QJsonObject deviceConfig; + + deviceConfig.insert("host", apiHostname); + deviceConfig.insert("port", apiPort); + if (ProviderUdp::init(deviceConfig)) + { + if (setStateDirect(false) && setState(true)) + { + setEffect(THE_CIRCUS); + + QEventLoop loop; + QTimer::singleShot(DEFAULT_IDENTIFY_TIME.count(), &loop, &QEventLoop::quit); + loop.exec(); + + setColor(ColorRgb::BLACK); + } + } + } +} diff --git a/libsrc/leddevice/dev_net/LedDeviceCololight.h b/libsrc/leddevice/dev_net/LedDeviceCololight.h new file mode 100644 index 00000000..3cce7548 --- /dev/null +++ b/libsrc/leddevice/dev_net/LedDeviceCololight.h @@ -0,0 +1,246 @@ +#ifndef LEDEVICECOLOLIGHT_H +#define LEDEVICECOLOLIGHT_H + +// LedDevice includes +#include +#include "ProviderUdp.h" + +enum appID { + TL1_CMD = 0x00, + DIRECT_CONTROL = 0x01, + TRANSMIT_FILE = 0x02, + CLEAR_FILES = 0x03, + WRITE_FILE = 0x04, + READ_FILE = 0x05, + MODIFY_SECU = 0x06 +}; + +enum effect : uint32_t { + SAVANNA = 0x04970400, + SUNRISE = 0x01c10a00, + UNICORNS = 0x049a0e00, + PENSIEVE = 0x04c40600, + THE_CIRCUS = 0x04810130, + INSTASHARE = 0x03bc0190, + EIGTHIES = 0x049a0000, + CHERRY_BLOS = 0x04940800, + RAINBOW = 0x05bd0690, + TEST = 0x03af0af0, + CHRISTMAS = 0x068b0900 +}; + +/// +/// Implementation of a Cololight LedDevice +/// +class LedDeviceCololight : public ProviderUdp +{ +public: + + /// + /// @brief Constructs a Cololight LED-device + /// + /// @param deviceConfig Device's configuration as JSON-Object + /// + explicit LedDeviceCololight(const QJsonObject& deviceConfig); + + /// + /// @brief Constructs the LED-device + /// + /// @param[in] deviceConfig Device's configuration as JSON-Object + /// @return LedDevice constructed + /// + static LedDevice* construct(const QJsonObject& deviceConfig); + + /// + /// @brief Discover Cololight devices available (for configuration). + /// + /// @return A JSON structure holding a list of devices found + /// + QJsonObject discover() override; + + /// + /// @brief Get a Cololight device's resource properties + /// + /// Following parameters are required + /// @code + /// { + /// "host" : "hostname or IP", + /// } + ///@endcode + /// + /// @param[in] params Parameters to query device + /// @return A JSON structure holding the device's properties + /// + QJsonObject getProperties(const QJsonObject& params) override; + + /// + /// @brief Send an update to the Cololight device to identify it. + /// + /// Following parameters are required + /// @code + /// { + /// "host" : "hostname or IP", + /// } + ///@endcode + /// + /// @param[in] params Parameters to address device + /// + void identify(const QJsonObject& params) override; + +protected: + + /// + /// @brief Initialise the device's configuration + /// + /// @param[in] deviceConfig the JSON device configuration + /// @return True, if success + /// + bool init(const QJsonObject& deviceConfig) override; + + /// + /// @brief Writes the RGB-Color values to the LEDs. + /// + /// @param[in] ledValues The RGB-color per LED + /// @return Zero on success, else negative + /// + int write(const std::vector& ledValues) override; + + /// + /// @brief Power-/turn on the Cololight device. + /// + /// @return True if success + /// + bool powerOn() override; + + /// + /// @brief Power-/turn off the Cololight device. + /// + /// @return True if success + /// + bool powerOff() override; + +private: + + bool initLedsConfiguration(); + void initDirectColorCmdTemplate(); + + /// + /// @brief Read additional information from Cololight + /// + /// @return True if success + /// + bool getInfo(); + + /// + /// @brief Set a Cololight effect + /// + /// @param[in] effect from effect list + /// + /// @return True if success + /// + bool setEffect(const effect effect); + + /// + /// @brief Set a color + /// + /// @param[in] color in RGB + /// + /// @return True if success + /// + bool setColor(const ColorRgb colorRgb); + + /// + /// @brief Set a color (or effect) + /// + /// @param[in] color in four bytes (red, green, blue, mode) + /// + /// @return True if success + /// + bool setColor(const uint32_t color); + + /// + /// @brief Set colors per LED as per given list + /// + /// @param[in] list of color per LED + /// + /// @return True if success + /// + bool setColor(const std::vector& ledValues); + + /// + /// @brief Set the Cololight device in TL1 command mode + /// + /// @param[in] isOn, Enable TL1 command mode = true + /// + /// @return True if success + /// + bool setTL1CommandMode(bool isOn); + + /// + /// @brief Set the Cololight device's state (on/off) in TL1 mode + /// + /// @param[in] isOn, on=true + /// + /// @return True if success + /// + bool setState(bool isOn); + + /// + /// @brief Set the Cololight device's state (on/off) in Direct Mode + /// + /// @param[in] isOn, on=true + /// + /// @return True if success + /// + bool setStateDirect(bool isOn); + + /// + /// @brief Send a request to the Cololight device for execution + /// + /// @param[in] appID + /// @param[in] command + /// + /// @return True if success + /// + bool sendRequest(const appID appID, const QByteArray& command); + + /// + /// @brief Read response for a send request + /// + /// @return True if success + /// + bool readResponse(); + + /// + /// @brief Read response for a send request + /// + /// @param[out] response + /// + /// @return True if success + /// + bool readResponse(QByteArray& response); + + // Cololight model, e.g. CololightPlus, CololightStrip + int _modelType; + + // Defines how Cololight LED are organised (multiple light beads in a module or individual lights on a strip + int _ledLayoutType; + + // Count of overall LEDs across all modules + int _ledBeadCount; + + // Distance (in #modules) of the module farest away from the main controller + int _distance; + + QByteArray _packetFixPart; + QByteArray _DataPart; + + QByteArray _directColorCommandTemplate; + + quint32 _sequenceNumber; + + //Cololights discovered and their response message details + QMultiMap> _services; +}; + +#endif // LEDEVICECOLOLIGHT_H diff --git a/libsrc/leddevice/dev_net/ProviderUdp.cpp b/libsrc/leddevice/dev_net/ProviderUdp.cpp index 7ab04b8c..1a1341c1 100644 --- a/libsrc/leddevice/dev_net/ProviderUdp.cpp +++ b/libsrc/leddevice/dev_net/ProviderUdp.cpp @@ -22,7 +22,7 @@ ProviderUdp::ProviderUdp(const QJsonObject &deviceConfig) , _port(1) , _defaultHost("127.0.0.1") { - _latchTime_ms = 1; + _latchTime_ms = 0; } ProviderUdp::~ProviderUdp() @@ -138,3 +138,13 @@ int ProviderUdp::writeBytes(const unsigned size, const uint8_t * data) return retVal; } + +int ProviderUdp::writeBytes(const QByteArray &bytes) +{ + qint64 retVal = _udpSocket->writeDatagram(bytes,_address,_port); + + WarningIf((retVal<0), _log, "&s", QSTRING_CSTR(QString + ("(%1:%2) Write Error: (%3) %4").arg(_address.toString()).arg(_port).arg(_udpSocket->error()).arg(_udpSocket->errorString()))); + + return retVal; +} diff --git a/libsrc/leddevice/dev_net/ProviderUdp.h b/libsrc/leddevice/dev_net/ProviderUdp.h index 3ceed9d4..e6149f3b 100644 --- a/libsrc/leddevice/dev_net/ProviderUdp.h +++ b/libsrc/leddevice/dev_net/ProviderUdp.h @@ -28,6 +28,8 @@ public: /// ~ProviderUdp() override; + QHostAddress getAddress() const { return _address; } + protected: /// @@ -53,8 +55,7 @@ protected: int close() override; /// - /// @brief Writes the given bytes/bits to the UDP-device and sleeps the latch time to ensure that the - /// values are latched. + /// @brief Writes the given bytes to the UDP-device /// /// @param[in] size The length of the data /// @param[in] data The data @@ -63,6 +64,15 @@ protected: /// int writeBytes(const unsigned size, const uint8_t *data); + /// + /// @brief Writes the given bytes to the UDP-device + /// + /// @param[in] data The data + /// + /// @return Zero on success, else negative + /// + int writeBytes(const QByteArray &bytes); + /// QUdpSocket * _udpSocket; QHostAddress _address; diff --git a/libsrc/leddevice/schemas/schema-cololight.json b/libsrc/leddevice/schemas/schema-cololight.json new file mode 100644 index 00000000..349826d1 --- /dev/null +++ b/libsrc/leddevice/schemas/schema-cololight.json @@ -0,0 +1,22 @@ +{ + "type":"object", + "required":true, + "properties": { + "host" : { + "type": "string", + "title":"edt_dev_spec_targetIpHost_title", + "propertyOrder" : 1 + }, + "latchTime": { + "type": "integer", + "title":"edt_dev_spec_latchtime_title", + "default": 0, + "append" : "edt_append_ms", + "minimum": 0, + "maximum": 1000, + "access" : "expert", + "propertyOrder" : 2 + } + }, + "additionalProperties": true +}