diff --git a/README.md b/README.md index 3cded58..abed147 100644 --- a/README.md +++ b/README.md @@ -39,31 +39,33 @@ A Cue View "plugin" is a system for communicating with a type of device, for exa ### plugin.js ```js -exports.defaultName = 'Example Plugin'; -exports.connectionType = 'osc' or 'TCPsocket' or 'UDPsocket' -exports.searchOptions = { - type: 'Bonjour', - bonjourNane: 'device' +exports.config = { + defaultName: 'Example Plugin', + connectionType: 'osc' or 'TCPsocket' or 'UDPsocket', + searchOptions: { + type: 'Bonjour', + bonjourNane: 'device' + }, + searchOptions: { + type: 'UDPsocket', + searchBuffer: Buffer.from([0x00, 0x01, 0x02]), + devicePort: 1234, // port the device receives messages on + listenPort: 2345, // port Cue View should listen for responses on + validateResponse: function(msg, info){ + // if this function returns true, Cue View adds the responding IP address to the list + } + }, + searchOptions: { + type: 'TCPport', + searchBuffer: Buffer.from('are you there'), + testPort: 1234, // port the device receives messages on + validateResponse: function(msg, info){ + // if this function returns true, Cue View adds the responding IP address to the list + } + }, + defaultPort: 1234, // only available for TCPsocket and UDPsocket devices + heartbeatInterval: 5000 // how frequently, in ms, to send the heartbeat message } -exports.searchOptions = { - type: 'UDPsocket', - searchBuffer: Buffer.from([0x00, 0x01, 0x02]), - devicePort: 1234, // port the device receives messages on - listenPort: 2345, // port Cue View should listen for responses on - validateResponse: function(msg, info){ - // if this function returns true, Cue View adds the responding IP address to the list - } -} -exports.searchOptions = { - type: 'TCPport', - searchBuffer: Buffer.from('are you there'), - testPort: 1234, // port the device receives messages on - validateResponse: function(msg, info){ - // if this function returns true, Cue View adds the responding IP address to the list - } -} -exports.defaultPort = 1234; // only available for TCPsocket and UDPsocket devices -exports.heartbeatInterval = 5000; // how frequently, in ms, to send the heartbeat message exports.ready = function (device){ // runs when Cue View identifies a new device. send all data requests here diff --git a/index.html b/index.html index 81e7818..9476233 100644 --- a/index.html +++ b/index.html @@ -35,62 +35,67 @@ -
- - - +
+
+ + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + +
Name: + +
Type: + +
Addr: + +
Port: + + Pin:
+ +

No Device Selected

+
-
- - - - - - - - - - - - - - - - - - - -
Name: - -
Type: - -
Addr: - -
Port: - - Pin:
- -

No Device Selected

-
diff --git a/plugins/artnet/main.js b/plugins/artnet/main.js index eded517..e897d7a 100644 --- a/plugins/artnet/main.js +++ b/plugins/artnet/main.js @@ -1,16 +1,18 @@ const _ = require('lodash'); -exports.defaultName = "Art-Net"; -exports.connectionType = 'UDPsocket'; -exports.heartbeatInterval = 10000; - -exports.searchOptions = { - type: 'UDPsocket', - searchBuffer: Buffer.from([0x00]), - devicePort: 6454, - listenPort: 6454, - validateResponse (msg, info, devices) { - return msg.toString('utf8', 0, 7) === "Art-Net"; +exports.config = { + defaultName: "Art-Net", + connectionType: "UDPsocket", + heartbeatInterval: 10000, + searchOptions: { + type: 'UDPsocket', + searchBuffer: Buffer.from([0x00]), + devicePort: 6454, + listenPort: 6454, + mayChangePort: false, + validateResponse (msg, info, devices) { + return msg.toString('utf8', 0, 7) === "Art-Net"; + } } }; diff --git a/plugins/eos/main.js b/plugins/eos/main.js index 67877e6..e9a9f64 100644 --- a/plugins/eos/main.js +++ b/plugins/eos/main.js @@ -3,20 +3,23 @@ const fs = require('fs'); const path = require('path'); const Cue = require('./cue'); -exports.defaultName = 'ETC Eos'; -exports.connectionType = 'osc'; -exports.searchOptions = { - type: 'TCPport', - searchBuffer: Buffer.from( - '\xc0/eos/ping\x00\x00\x2c\x00\x00\x00\xc0', - 'ascii' - ), - testPort: 3032, - validateResponse(msg, info) { - return msg.toString().indexOf('/eos/out'); - }, -}; -exports.defaultPort = 3032; +exports.config = { + defaultName: "ETC Eos", + connectionType: "osc", + defaultPort: 3032, + mayChangePort: false, + searchOptions: { + type: 'TCPport', + searchBuffer: Buffer.from( + '\xc0/eos/ping\x00\x00\x2c\x00\x00\x00\xc0', + 'ascii' + ), + testPort: 3032, + validateResponse(msg, info) { + return msg.toString().indexOf('/eos/out'); + } + } +} exports.ready = function ready(_device) { const device = _device; diff --git a/plugins/pjlink/main.js b/plugins/pjlink/main.js index 1d1f4b4..c6a7f01 100644 --- a/plugins/pjlink/main.js +++ b/plugins/pjlink/main.js @@ -1,19 +1,32 @@ const md5 = require('md5'); -exports.defaultName = 'PJLink Projector'; -exports.connectionType = 'TCPsocket'; -exports.heartbeatInterval = 5000; -exports.heartbeatTimeout = 15000; -exports.searchOptions = { - type: 'UDPsocket', - searchBuffer: Buffer.from([0x25, 0x32, 0x53, 0x52, 0x43, 0x48, 0x0d]), - devicePort: 4352, - listenPort: 4352, - validateResponse (msg, info) { - return msg.toString().indexOf('%2ACKN=') >= 0; +exports.config = { + defaultName: 'PJLink Projector', + connectionType: "TCPsocket", + heartbeatInterval: 5000, + heartbeatTimeout: 15000, + defaultPort: 4352, + mayChangePort: false, + searchOptions: { + type: 'UDPsocket', + searchBuffer: Buffer.from([0x25, 0x32, 0x53, 0x52, 0x43, 0x48, 0x0d]), + devicePort: 4352, + listenPort: 4352, + validateResponse (msg, info) { + console.log(msg.toString()); + return msg.toString().indexOf('%2ACKN=') >= 0; + } }, -}; -exports.defaultPort = 4352; + fields: [{ + key: "password", + label: "Pass", + type: "textinput", + value: "", + action: function(device){ + device.plugin.heartbeat(device); + } + }] +} exports.ready = function ready(device) { // Power status query @@ -34,7 +47,8 @@ const PJLinkCmds = [ '%2SVER=' ] -let password = false; +let passwordMD5 = false; +let passwordSeed = false; function processPJLink(_device, str, that) { const arr = str.split('%'); @@ -104,21 +118,42 @@ exports.data = function data(device, message) { this.deviceInfoUpdate(device, 'status', 'ok'); const msg = message.toString(); + //console.log(msg); + if (msg.substring(0, 8) === 'PJLINK 1') { - password = md5(`${msg.substring(9, 17)}JBMIAProjectorLink`); + passwordSeed = msg.substring(9, 17); + passwordMD5 = md5(`${passwordSeed}${device.fields.password}`); + device.data.authentication = "ON"; + device.send( - `${password}%1POWR ?\r%1INPT ?\r%1AVMT ?\r%1ERST ?\r%1LAMP ?\r%1NAME ?\r%1INF1 ?\r%1INF2 ?\r%2SNUM ?\r%2SVER ?\r` + `${passwordMD5}%1POWR ?\r%1INPT ?\r%1AVMT ?\r%1ERST ?\r%1LAMP ?\r%1NAME ?\r%1INF1 ?\r%1INF2 ?\r%2SNUM ?\r%2SVER ?\r` ); + device.draw(); + + }else if(msg.substring(0, 8) === 'PJLINK 0') { + device.data.authentication = "OFF"; + device.draw(); + + }else if(msg.startsWith('PJLINK ERRA')) { + device.data.passwordOK = false; + device.draw(); } + if(PJLinkCmds.includes(msg.substring(0,7))){ - processPJLink(device,msg,this) + processPJLink(device,msg,this); + device.data.passwordOK = true; } }; exports.heartbeat = function heartbeat(device) { - if (password) { + passwordMD5 = md5(`${passwordSeed}${device.fields.password}`); + if (device.fields.password.length>0) { device.send( - `${password}%1POWR ?\r%1INPT ?\r%1AVMT ?\r%1ERST ?\r%1LAMP ?\r%1NAME ?\r%1INF1 ?\r%1INF2 ?\r%2SNUM ?\r%2SVER ?\r` + `${passwordMD5}%1POWR ?\r%1INPT ?\r%1AVMT ?\r%1ERST ?\r%1LAMP ?\r%1NAME ?\r%1INF1 ?\r%1INF2 ?\r%2SNUM ?\r%2SVER ?\r` + ); + }else{ + device.send( + `%1POWR ?\r%1INPT ?\r%1AVMT ?\r%1ERST ?\r%1LAMP ?\r%1NAME ?\r%1INF1 ?\r%1INF2 ?\r%2SNUM ?\r%2SVER ?\r` ); } device.draw(); diff --git a/plugins/pjlink/styles.css b/plugins/pjlink/styles.css index 05252a2..0ed35ba 100644 --- a/plugins/pjlink/styles.css +++ b/plugins/pjlink/styles.css @@ -7,3 +7,7 @@ .ok { color: #79b757; } +table{ + margin-bottom: 30px; + width: 350px; +} diff --git a/plugins/pjlink/template.ejs b/plugins/pjlink/template.ejs index 3df8cd2..7338ee8 100644 --- a/plugins/pjlink/template.ejs +++ b/plugins/pjlink/template.ejs @@ -92,4 +92,19 @@ Version
<%= data.version %>
+ + + + + +
Authentication + <% if(!data.passwordOK){ %> +
INCORRECT PASSWORD
+ <% }else if(data.authentication=="ON"){ %> +
ON
+ <% }else{ %> +
<%= data.authentication %>
+ <% } %> + +
\ No newline at end of file diff --git a/plugins/qlab/main.js b/plugins/qlab/main.js index 8521b25..6f9537e 100644 --- a/plugins/qlab/main.js +++ b/plugins/qlab/main.js @@ -2,6 +2,27 @@ const _ = require('lodash'); const fs = require('fs'); const path = require('path'); +exports.config = { + defaultName: "QLab", + connectionType: "osc", + heartbeatInterval: 50, + heartbeatTimeout: 2000, + mayChangePort: true, + searchOptions: { + type: 'Bonjour', + bonjourName: 'qlab', + }, + fields: [{ + key: "passcode", + label: "Pass", + type: "textinput", + value: "", + action: function(device){ + device.send('/workspaces'); + } + }] +} + let lastElapsedUpdate = Date.now(); let interval = 5; let heartbeatCount = 0; @@ -16,14 +37,6 @@ const cueTemplate = _.template(fs.readFileSync(path.join(__dirname, `cue.ejs`))) const tileTemplate = _.template(fs.readFileSync(path.join(__dirname, `tile.ejs`))); const cartTemplate = _.template(fs.readFileSync(path.join(__dirname, `cart.ejs`))); -exports.defaultName = 'QLab'; -exports.connectionType = 'osc'; -exports.heartbeatInterval = 50; -exports.heartbeatTimeout = 2000; -exports.searchOptions = { - type: 'Bonjour', - bonjourName: 'qlab', -}; exports.ready = function ready(device) { device.send(`/version`); @@ -54,7 +67,7 @@ exports.data = function data(_device, oscData) { cueLists: {}, cues: {} } - device.send(`/workspace/${wksp.uniqueID}/connect`); + device.send(`/workspace/${wksp.uniqueID}/connect`, device.fields.passcode); }); }else if(match(oscAddressParts, ["reply", "workspace", "*", "connect"])){ diff --git a/plugins/sacn/main.js b/plugins/sacn/main.js index 488d2cf..dfa31b3 100644 --- a/plugins/sacn/main.js +++ b/plugins/sacn/main.js @@ -1,18 +1,21 @@ const _ = require('lodash'); -exports.defaultName = 'sACN'; -exports.connectionType = 'multicast'; -exports.defaultPort = 5568; -exports.heartbeatInterval = 5000; -exports.searchOptions = { - type: "multicast", - address: getMulticastGroup(1), - port: 5568, - validateResponse (msg, info) { - return msg.toString('utf8', 4, 13) === "ASC-E1.17"; - }, -}; -exports.defaultPort = 5568; +exports.config = { + defaultName: "sACN", + connectionType: "multicast", + defaultPort: 5568, + heartbeatInterval: 5000, + defaultPort: 5568, + mayChangePort: false, + searchOptions: { + type: "multicast", + address: getMulticastGroup(1), + port: 5568, + validateResponse (msg, info) { + return msg.toString('utf8', 4, 13) === "ASC-E1.17"; + } + } +} exports.ready = function ready(device) { diff --git a/plugins/watchout/main.js b/plugins/watchout/main.js index f80ad4a..896784d 100644 --- a/plugins/watchout/main.js +++ b/plugins/watchout/main.js @@ -1,15 +1,18 @@ -exports.defaultName = 'Dataton Watchout'; -exports.connectionType = 'TCPsocket'; -exports.heartbeatInterval = 500; -exports.searchOptions = { - type: 'TCPport', - searchBuffer: Buffer.from('authenticate 1\n', 'ascii'), - testPort: 3040, - validateResponse (msg, info) { - return msg.toString().substring(0, 5) === 'Ready'; - }, -}; -exports.defaultPort = 3040; +exports.config = { + defaultName: "Dataton Watchout", + connectionType: "TCPsocket", + heartbeatInterval: 500, + defaultPort: 3040, + mayChangePort: false, + searchOptions: { + type: 'TCPport', + searchBuffer: Buffer.from('authenticate 1\n', 'ascii'), + testPort: 3040, + validateResponse (msg, info) { + return msg.toString().substring(0, 5) === 'Ready'; + } + } +} exports.ready = function ready(device) { device.send('authenticate 1\n'); diff --git a/plugins/x32/main.js b/plugins/x32/main.js index 020fadc..700a961 100644 --- a/plugins/x32/main.js +++ b/plugins/x32/main.js @@ -1,16 +1,19 @@ -exports.defaultName = 'X32 Mixer'; -exports.connectionType = 'osc-udp'; -exports.heartbeatInterval = 9000; -exports.searchOptions = { - type: 'UDPsocket', - searchBuffer: Buffer.from([0x2f, 0x78, 0x69, 0x6e, 0x66, 0x6f]), - devicePort: 10023, - listenPort: 0, - validateResponse(msg, info) { - return msg.toString().indexOf('/xinfo') === 0; - }, -}; -exports.defaultPort = 10023; +exports.config = { + defaultName: "X32 Mixer", + connectionType: "osc-udp", + heartbeatInterval: 9000, + defaultPort: 10023, + mayChangePort: false, + searchOptions: { + type: 'UDPsocket', + searchBuffer: Buffer.from([0x2f, 0x78, 0x69, 0x6e, 0x66, 0x6f]), + devicePort: 10023, + listenPort: 0, + validateResponse(msg, info) { + return msg.toString().indexOf('/xinfo') === 0; + } + } +} exports.ready = function ready(device) { const d = device; diff --git a/plugins/xair/main.js b/plugins/xair/main.js index 152dd84..25e70d4 100644 --- a/plugins/xair/main.js +++ b/plugins/xair/main.js @@ -1,16 +1,19 @@ -exports.defaultName = 'X Air Mixer'; -exports.connectionType = 'UDPsocket'; -exports.heartbeatInterval = 10000; -exports.searchOptions = { - type: 'UDPsocket', - searchBuffer: Buffer.from([0x2f, 0x78, 0x69, 0x6e, 0x66, 0x6f]), - devicePort: 10024, - listenPort: 0, - validateResponse(msg, info) { - return msg.toString().indexOf('/xinfo') === 0; - }, -}; -exports.defaultPort = 10024; +exports.config = { + defaultName: "X Air Mixer", + connectionType: "UDPsocket", + heartbeatInterval: 10000, + defaultPort: 10024, + mayChangePort: false, + searchOptions: { + type: 'UDPsocket', + searchBuffer: Buffer.from([0x2f, 0x78, 0x69, 0x6e, 0x66, 0x6f]), + devicePort: 10024, + listenPort: 0, + validateResponse(msg, info) { + return msg.toString().indexOf('/xinfo') === 0; + } + } +} exports.ready = function ready(device) { const d = device; diff --git a/src/device.js b/src/device.js index 7ad45e7..cf16a4f 100644 --- a/src/device.js +++ b/src/device.js @@ -49,6 +49,7 @@ function registerDevice(newDevice) { port: newDevice.port, addresses: newDevice.addresses, data: {}, + fields: {}, pinIndex: false, lastDrawn: 0, lastHeartbeat: 0, @@ -65,6 +66,13 @@ function registerDevice(newDevice) { }, }; + if(PLUGINS.all[newDevice.type].config.fields){ + PLUGINS.all[newDevice.type].config.fields.forEach(field => { + devices[id].fields[field.key] = field.value; + }); + } + + VIEW.addDeviceToList(devices[id]); initDeviceConnection(id); return true; @@ -89,8 +97,8 @@ function initDeviceConnection(id) { const { type } = devices[id]; const plugins = PLUGINS.all; - if (plugins[type].connectionType.includes('osc')) { - if (plugins[type].connectionType.includes('udp')) { + if (plugins[type].config.connectionType.includes('osc')) { + if (plugins[type].config.connectionType.includes('udp')) { device.connection = new osc.UDPPort({ localAddress: '0.0.0.0', remoteAddress: device.addresses[0], @@ -124,8 +132,8 @@ function initDeviceConnection(id) { device.send = (address, args) => { device.connection.send({ address, args }); }; - device.plugin = plugins[type]; - } else if (plugins[type].connectionType === 'TCPsocket') { + + } else if (plugins[type].config.connectionType === 'TCPsocket') { device.connection = new net.Socket(); device.connection.connect( @@ -155,7 +163,7 @@ function initDeviceConnection(id) { // log("SOCK OUT", data); device.connection.write(data); }; - } else if (plugins[type].connectionType === 'UDPsocket') { + } else if (plugins[type].config.connectionType === 'UDPsocket') { device.connection = udp.createSocket('udp4'); device.connection.bind({ port: plugins[type].defaultPort }, () => { @@ -177,7 +185,7 @@ function initDeviceConnection(id) { } ); }; - } else if (plugins[type].connectionType === 'multicast') { + } else if (plugins[type].config.connectionType === 'multicast') { device.connection = udp.createSocket('udp4'); device.connection.bind(device.port, () => { @@ -191,6 +199,7 @@ function initDeviceConnection(id) { device.send = (data) => {}; } + device.plugin = plugins[type]; return true; } diff --git a/src/index.css b/src/index.css index 7f7cd0f..d363a87 100644 --- a/src/index.css +++ b/src/index.css @@ -56,10 +56,6 @@ a { user-select: text; -webkit-app-region: drag; z-index: 1000; - /*border-bottom: black 1px solid;*/ - /*background: linear-gradient(#3f3f3f 0%, #343434 100%);*/ - /*background-color: #363636;*/ - /*background-color: rgba(0, 0, 0, 0.4);*/ } #view-buttons-bar button { background-color: rgba(255, 255, 255, 0.07); @@ -123,11 +119,15 @@ a { width: 210px; pointer-events: none; } - -#device-tools { +#device-inspector{ position: absolute; + bottom: 0px; + width: 100%; +} +#device-tools { + /* position: absolute; bottom: 210px; - left: 0px; + left: 0px;*/ width: 100%; height: 30px; padding-right: 10px; @@ -142,10 +142,7 @@ a { } #device-settings { - position: absolute; - left: 0px; - bottom: 0px; - height: 200px; + height: 300px; padding: 5px; color: white; font-weight: 300; @@ -165,7 +162,7 @@ a { display: none; } #device-settings h3 { - padding-top: 20px; + padding-top: 90px; text-align: center; font-weight: 300; } @@ -220,6 +217,7 @@ input { color: #fff; font-size: 16px; font-weight: 500; + cursor: text; } select { display: block; @@ -250,6 +248,7 @@ input[type='checkbox'] { width: 14px; padding: 2px; -webkit-appearance: none; + cursor: default; } input[type='checkbox']:checked { background-color: #616064; @@ -267,6 +266,10 @@ input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; } +input:disabled{ + color: gray; + cursor: not-allowed; +} button, select.button { box-sizing: content-box; diff --git a/src/plugins.js b/src/plugins.js index 794fcd6..24e2305 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -47,17 +47,33 @@ module.exports.init = function init(callback) { ); - if (p.heartbeatTimeout === undefined || p.heartbeatTimeout < 50) { - if(p.heartbeatInterval){ - p.heartbeatTimeout = p.heartbeatInterval * 1.5; + //if (p.heartbeatTimeout === undefined || p.heartbeatTimeout < 50) { + if(p.config.heartbeatTimeout){ + p.heartbeatTimeout = p.config.heartbeatInterval * 1.5; }else{ p.heartbeatTimeout = 10000; } - } + //} - if (p.heartbeatInterval === undefined || p.heartbeatInterval < 50) { - p.heartbeatInterval = 5000; - } + if(p.config.heartbeatInterval){ + p.heartbeatInterval = Math.max(50, p.config.heartbeatInterval); + }else{ + p.heartbeatInterval = 5000; + } + + // p.fields = {}; + // if(p.config.fields){ + // p.config.fields.forEach(field => { + // p.fields[field.key] = field.value; + // }); + // } + + + + + // if (p.heartbeatInterval === undefined || p.heartbeatInterval < 50) { + // p.heartbeatInterval = 5000; + // } } }); diff --git a/src/search.js b/src/search.js index 955355a..72b599d 100644 --- a/src/search.js +++ b/src/search.js @@ -20,10 +20,12 @@ function getServers() { const addresses = interfaces[key]; for (let i = addresses.length; i--; ) { const address = addresses[i]; - if (address.family === 'IPv4' && !address.internal) { + + if (address.family === 'IPv4' && !address.internal && address.address.substring(0, 3)!="169") { const subnet = ip.subnet(address.address, address.netmask); let current = ip.toLong(subnet.firstAddress); const last = ip.toLong(subnet.lastAddress) - 1; + console.log(`range ${subnet.firstAddress} - ${subnet.lastAddress}`); while (current++ < last) result.push(ip.fromLong(current)); } } @@ -60,21 +62,21 @@ function searchAll() { const plugin = PLUGINS.all[p]; try { - const t = plugin.searchOptions.type; + const t = plugin.config.searchOptions.type; if(t === 'TCPport'){ if (TCPFlag) { - searchTCP(p, plugin); + searchTCP(p, plugin.config); } }else if(t === 'Bonjour'){ - searchBonjour(p, plugin); + searchBonjour(p, plugin.config); }else if(t === 'UDPsocket'){ - searchUDP(p, plugin); + searchUDP(p, plugin.config); }else if(t === 'multicast'){ - searchMulticast(p, plugin); + searchMulticast(p, plugin.config); } @@ -96,7 +98,7 @@ function searchAll() { } ipcRenderer.send('enableSearchAll', ''); - }, 5000); + }, 10000); }; module.exports.searchAll = searchAll; diff --git a/src/view.js b/src/view.js index 9d3613f..323755a 100644 --- a/src/view.js +++ b/src/view.js @@ -197,6 +197,35 @@ function switchDevice(id) { document.getElementById('device-settings-port').value = activeDevice.port || ''; document.getElementById('device-settings-pin').checked = activeDevice.pinIndex; + if(activeDevice.plugin.config.mayChangePort){ + document.getElementById('device-settings-port').disabled = false; + }else{ + document.getElementById('device-settings-port').disabled = true; + } + + document.getElementById('device-settings-fields').innerHTML = ""; + + if(activeDevice.plugin.config.fields){ + let fields = activeDevice.plugin.config.fields; + + fields.forEach(field => { + let $elem = document.createElement("input"); + $elem.type = "text"; + $elem.value = activeDevice.fields[field.key];// || field.value; + $elem.name = field.key; + $elem.onchange = function(e){ + activeDevice.fields[field.key] = $elem.value; + field.action(activeDevice); + } + + if(field.type=="textinput"){ + let rowHTML =`${field.label}:`; + document.getElementById('device-settings-fields').insertAdjacentHTML("beforeend", rowHTML); + document.getElementById(field.key).appendChild($elem); + } + }); + } + ipcRenderer.send('enableDeviceDropdown', ''); ipcRenderer.send('setDevicePin', !DEVICE.all[id].pinIndex === false); }; @@ -303,8 +332,8 @@ function populatePluginLists() { Object.keys(PLUGINS.all).forEach((pluginType) => { const plugin = PLUGINS.all[pluginType]; - addSelect += ``; - typeSelect += ``; + addSelect += ``; + typeSelect += ``; }); document.getElementById('device-settings-plugin-dropdown').innerHTML = typeSelect;