diff --git a/index.html b/index.html index 7000668..296d0c2 100644 --- a/index.html +++ b/index.html @@ -49,17 +49,33 @@ Addr: - + - Port: + Port: - + Pin: + + Local: + + + + + + diff --git a/plugins/artnet/main.js b/plugins/artnet/main.js index 459dfd0..5ccfde7 100644 --- a/plugins/artnet/main.js +++ b/plugins/artnet/main.js @@ -1,8 +1,8 @@ exports.config = { defaultName: 'Art-Net', connectionType: 'UDPsocket', - defaultPort: 6454, - mayChangePort: false, + remotePort: 6454, + mayChangePorts: false, heartbeatInterval: 5000, heartbeatTimeout: 15000, searchOptions: { diff --git a/plugins/digico/fader.ejs b/plugins/digico/fader.ejs new file mode 100644 index 0000000..38c444e --- /dev/null +++ b/plugins/digico/fader.ejs @@ -0,0 +1,42 @@ + + <% if(fader && fader.meter>0){ %> + + <%=index%> + + <% }else{ %> + <%=index%> + <% } %> + + <% if(type=="Control_Groups"){ %> + + <%= fader ? fader.name : "-" %> + + <% }else if(type=="Input_Channels"){ %> + + <%= fader ? (fader.Channel_Input ? fader.Channel_Input.name: "") : "" %> + + <% }else if(type=="Group_Outputs"){ %> + + <%= fader ? (fader.Buss_Trim ? fader.Buss_Trim.name: "") : "" %> + + <% }else if(type=="Aux_Outputs"){ %> + + <%= fader ? (fader.Buss_Trim ? fader.Buss_Trim.name: "") : "" %> + + <% } %> + + + + <% if(fader){ %> + <% let percent = (fader.fader-10)*1.5+100 %> + + <% if(fader.fader>-100){ %> + <%= fader ? Math.round(fader.fader*10.0)/10.0 : '-' %>db + <% } %> + + <% }else{ %> + - + <% } %> + + + \ No newline at end of file diff --git a/plugins/digico/icon.afphoto b/plugins/digico/icon.afphoto new file mode 100644 index 0000000..ff2c905 Binary files /dev/null and b/plugins/digico/icon.afphoto differ diff --git a/plugins/digico/icon.png b/plugins/digico/icon.png new file mode 100644 index 0000000..4a5276a Binary files /dev/null and b/plugins/digico/icon.png differ diff --git a/plugins/digico/info.html b/plugins/digico/info.html new file mode 100644 index 0000000..9d0f401 --- /dev/null +++ b/plugins/digico/info.html @@ -0,0 +1,17 @@ +Cue View emulates to be the DiGiCo SD iPad app. +
    +
  1. On the SD console open Master ScreenSetupExternal Control
  2. +
  3. Enable External Control by pressing the button at the top of the panel
  4. +
  5. Press the add device button and select DiGiCo Pad
  6. +
  7. Enter a Device Name for Cue View and then enter the IP Address of this computer
  8. +
  9. Enter Send and Receive Port numbers
  10. + +
  11. Tick the Enable column for this device
  12. +
  13. + Press the Load button in the bottom right corner of the panel and select the commands button for the + relevant console +
  14. +
diff --git a/plugins/digico/main.js b/plugins/digico/main.js new file mode 100644 index 0000000..c541510 --- /dev/null +++ b/plugins/digico/main.js @@ -0,0 +1,192 @@ +const _ = require('lodash'); +const fs = require('fs'); +const path = require('path'); + +exports.config = { + defaultName: 'DiGiCo SD', + connectionType: 'osc-udp', + remotePort: 8000, + localPort: 8001, + mayChangePorts: true, + heartbeatInterval: 2000, + heartbeatTimeout: 11000, + searchOptions: { + type: 'UDPsocket', + // send "/Console/Name/?" this is what the iPad sends to check connection + searchBuffer: Buffer.from([ + 0x2f, 0x43, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x2f, 0x4e, 0x61, 0x6d, 0x65, 0x2f, 0x3f, 0x00, 0x2c, 0x00, 0x00, + 0x00, + ]), + devicePort: 8000, + listenPort: 9000, + validateResponse(msg, info) { + console.log(msg); + console.log(msg.toString()); + return msg.toString().includes('/Console'); + }, + }, +}; + +exports.ready = function ready(_device) { + const device = _device; + device.data = new DiGiCo(); + + device.send('/Console/Name/?'); + device.send('/Console/Session/Filename/?'); + device.send('/Console/Channels/?'); + device.send('/Snapshots/Current_Snapshot/?'); + device.send('/Meters/clear'); +}; +exports.data = function data(_device, oscData) { + const device = _device; + this.deviceInfoUpdate(device, 'status', 'ok'); + + const properties = oscData.address.split('/'); + properties.shift(); + + if (properties[0] !== 'Meters') { + setObjectProperty(device.data, properties, oscData.args); + // console.log(properties); + } + + if (properties[0] === 'Console' && properties[1] === 'Control_Groups') { + for (let i = 1; i <= device.data.Console.Control_Groups; i++) { + device.send(`/Control_Groups/${i}/?`); + } + device.draw(); + } else if (properties[0] === 'Console' && properties[1] === 'Input_Channels') { + for (let i = 1; i <= device.data.Console.Input_Channels; i++) { + device.send(`/Input_Channels/${i}/Channel_Input/name/?`); + device.send(`/Input_Channels/${i}/mute/?`); + device.send(`/Input_Channels/${i}/solo/?`); + device.send(`/Input_Channels/${i}/fader/?`); + device.send(`/Meters/request/1${i}`, [ + { type: 's', value: `/Input_Channels/${i}/Channel_Input/post_meter/left` }, + ]); + } + device.draw(); + } else if (properties[0] === 'Console' && properties[1] === 'Group_Outputs') { + for (let i = 1; i <= device.data.Console.Group_Outputs; i++) { + device.send(`/Group_Outputs/${i}/mute/?`); + device.send(`/Group_Outputs/${i}/solo/?`); + device.send(`/Group_Outputs/${i}/fader/?`); + device.send(`/Group_Outputs/${i}/Buss_Trim/name/?`); + device.send(`/Meters/request/3${i}`, [{ type: 's', value: `/Group_Outputs/${i}/fader_meter/left` }]); + } + device.draw(); + } else if (properties[0] === 'Console' && properties[1] === 'Aux_Outputs') { + for (let i = 1; i <= device.data.Console.Aux_Outputs; i++) { + device.send(`/Aux_Outputs/${i}/mute/?`); + device.send(`/Aux_Outputs/${i}/solo/?`); + device.send(`/Aux_Outputs/${i}/fader/?`); + device.send(`/Aux_Outputs/${i}/Buss_Trim/name/?`); + device.send(`/Meters/request/4${i}`, [{ type: 's', value: `/Aux_Outputs/${i}/fader_meter/left` }]); + } + device.draw(); + } else if (properties[0] === 'Console' && properties[1] === 'Name') { + this.deviceInfoUpdate(device, 'defaultName', device.data.Console.Name); + } else if (['Control_Groups', 'Input_Channels', 'Group_Outputs', 'Aux_Outputs'].includes(properties[0])) { + device.update('fader', { + type: properties[0], + channel: properties[1], + }); + } else if (properties[0] === 'Snapshots') { + device.update('updateSnapshot', { + snapshots: device.data.Snapshots, + }); + } else if (properties[0] === 'Meters' && properties[1] === 'values') { + for (let i = 0; i < oscData.args.length; i += 2) { + const channelType = oscData.args[i].toString()[0]; + const channelNumber = oscData.args[i].toString().substring(1); + const meterValue = BigInt(oscData.args[i + 1]) / -40000n + 100n; + + if (channelType === '1' && device.data.Input_Channels[channelNumber]) { + device.data.Input_Channels[channelNumber].meter = meterValue; + device.update('fader', { + type: 'Input_Channels', + channel: channelNumber, + }); + } else if (channelType === '3' && device.data.Group_Outputs[channelNumber]) { + device.data.Group_Outputs[channelNumber].meter = meterValue; + device.update('fader', { + type: 'Group_Outputs', + channel: channelNumber, + }); + } else if (channelType === '4' && device.data.Aux_Outputs[channelNumber]) { + device.data.Aux_Outputs[channelNumber].meter = meterValue; + device.update('fader', { + type: 'Aux_Outputs', + channel: channelNumber, + }); + } + } + } +}; + +exports.heartbeat = function heartbeat(device) { + device.send('/Console/Name/?'); + console.log(device.data); + + // Need to do a /Meters/clear when quitting Cue View or closing the View +}; + +class DiGiCo { + constructor() { + this.Console = {}; + this.Input_Channels = {}; + this.Aux_Outputs = {}; + this.Control_Groups = {}; + this.Group_Outputs = {}; + this.Matrix_Outputs = {}; + this.Layout = {}; + } +} + +/* + This function takes somethings like this /Console/Session/Filename with a osc args like ['session.ses'] + and turns it into a nested object like. An easy way to set properties converting OSC paths to properties + { + Console: { + Session: { + Filename: 'session.ses' + } + } + } + +*/ +function setObjectProperty(_object, _properties, value) { + let object = _object; + const properties = _properties; + + for (let i = 0; i < properties.length - 1; i++) { + const property = properties[i]; + if (object[property] === undefined) { + object[property] = {}; + } + object = object[property]; + } + const property = properties[properties.length - 1]; + if (Array.isArray(value)) { + // if this is an array of length one just set the property to the contents so ['string'] becomes 'string' + if (value.length === 1) { + object[property] = value[0]; + return; + } + } + object[property] = value; +} + +const faderTemplate = _.template(fs.readFileSync(path.join(__dirname, `/fader.ejs`))); +exports.update = function update(device, doc, updateType, data) { + if (updateType === 'fader') { + const $elem = doc.getElementById(`${data.type}-${data.channel}`); + $elem.outerHTML = faderTemplate({ + type: data.type, + fader: device.data[data.type][data.channel], + index: data.channel, + }); + } else if (updateType === 'snapshot') { + const $elem = doc.getElementById(`snapshot`); + $elem.textContent = `${data.snapshots.Current_Snapshot}`; + } +}; diff --git a/plugins/digico/styles.css b/plugins/digico/styles.css new file mode 100644 index 0000000..4a36e34 --- /dev/null +++ b/plugins/digico/styles.css @@ -0,0 +1,76 @@ +.column { + float: left; + margin: 5px; +} +.section { + border: #333 3px solid; + border-bottom: none; + border-top-left-radius: 10px; + border-top-right-radius: 10px; + padding: 10px; + margin-bottom: 30px; +} +.section h3 { + font-weight: 300; + margin-top: 0px; + color: #aaa !important; +} +table { + border-collapse: collapse; + display: block; +} +.fader:nth-child(even) { + background: #1a1a1a; +} +.fader:nth-child(odd) { + background: #1f1f1f; +} +.fader td { + text-align: center; + width: 30px; + height: 20px; + border: black 1px solid; +} +.fader .fader-channel { + color: #777; + font-size: 10px; +} +.fader .fader-name { + width: 80px; + color: black; + font-family: monospace; +} +.fader .lcd-control-group { + background-color: rgb(66, 132, 255); +} +.fader .lcd-channel-input { + background-color: rgb(135, 175, 235); +} +.fader .lcd-group-output { + background-color: rgb(241, 71, 48); +} +.fader .lcd-aux-output { + background-color: rgb(197, 0, 187); +} +.fader .fader-name.state-1 { + background-color: rgb(0, 185, 0); +} +.fader .fader-mute { + width: 10px; + background-color: #444; +} +.fader .fader-mute.state-1 { + background-color: red; +} +.fader .fader-fader { + text-align: left; + width: 70px; + padding: 0px 10px; + color: rgb(68, 68, 68); +} + +#snapshot { + font-size: 36px; + font-weight: bold; + font-family: 'Arial Black', 'Impact'; +} diff --git a/plugins/digico/template.ejs b/plugins/digico/template.ejs new file mode 100644 index 0000000..25667c2 --- /dev/null +++ b/plugins/digico/template.ejs @@ -0,0 +1,56 @@ +
+

<%= listName %>

+

<%= data.Console.Session.Filename %>

+
+ +<% +const fs = require('fs'); +let _ = require('lodash'); +const path = require('path'); + + let faderTemplate = _.template(fs.readFileSync(path.join(__dirname, `/plugins/digico/fader.ejs`))); +%> + +
+
+

snapshot

+
+ +
+

control groups

+ + <% for(let i=1; i<=data.Console.Control_Groups; i++){ %> + <%= faderTemplate({type: "Control_Groups", fader: data.Control_Groups[i], index: i}) %> + <% } %> +
+
+ +
+

group outputs

+ + <% for(let i=1; i<=data.Console.Group_Outputs; i++){ %> + <%= faderTemplate({type: "Group_Outputs", fader: data.Group_Outputs[i], index: i}) %> + <% } %> +
+
+ +
+

aux outputs

+ + <% for(let i=1; i<=data.Console.Aux_Outputs; i++){ %> + <%= faderTemplate({type: "Aux_Outputs", fader: data.Aux_Outputs[i], index: i}) %> + <% } %> +
+
+
+ +
+

input channels

+ + <% for(let i=1; i<=data.Console.Input_Channels; i++){ %> + <%= faderTemplate({type: "Input_Channels", fader: data.Input_Channels[i], index: i}) %> + <% } %> +
+
+ + diff --git a/plugins/pjlink/main.js b/plugins/pjlink/main.js index e1e227b..d701049 100644 --- a/plugins/pjlink/main.js +++ b/plugins/pjlink/main.js @@ -3,8 +3,8 @@ const md5 = require('md5'); exports.config = { defaultName: 'PJLink Projector', connectionType: 'TCPsocket', - defaultPort: 4352, - mayChangePort: false, + remotePort: 4352, + mayChangePorts: false, heartbeatInterval: 5000, heartbeatTimeout: 15000, searchOptions: { diff --git a/plugins/qlab/main.js b/plugins/qlab/main.js index 7a8d4e6..d242e6f 100644 --- a/plugins/qlab/main.js +++ b/plugins/qlab/main.js @@ -5,8 +5,10 @@ const path = require('path'); exports.config = { defaultName: 'QLab', connectionType: 'osc', - defaultPort: 53000, - mayChangePort: true, + remotePort: 53000, + localPort: 53001, + mayChangePorts: true, + mayChangeLocalPort: false, heartbeatInterval: 100, heartbeatTimeout: 5000, searchOptions: { diff --git a/plugins/sacn/main.js b/plugins/sacn/main.js index 4274185..0fe9eb5 100644 --- a/plugins/sacn/main.js +++ b/plugins/sacn/main.js @@ -3,8 +3,8 @@ const _ = require('lodash'); exports.config = { defaultName: 'sACN', connectionType: 'multicast', - defaultPort: 5568, - mayChangePort: false, + remotePort: 5568, + mayChangePorts: false, heartbeatInterval: 5000, heartbeatTimeout: 15000, searchOptions: { diff --git a/plugins/shure/main.js b/plugins/shure/main.js index e4f539b..05897b6 100644 --- a/plugins/shure/main.js +++ b/plugins/shure/main.js @@ -3,8 +3,8 @@ const Channel = require('./channel'); exports.config = { defaultName: 'Shure Wireless', connectionType: 'TCPsocket', - defaultPort: 2202, - mayChangePort: false, + remotePort: 2202, + mayChangePorts: false, heartbeatInterval: 5000, heartbeatTimeout: 10000, searchOptions: { diff --git a/plugins/watchout/main.js b/plugins/watchout/main.js index 340aa9f..d6abc35 100644 --- a/plugins/watchout/main.js +++ b/plugins/watchout/main.js @@ -1,8 +1,8 @@ exports.config = { defaultName: 'Dataton Watchout', connectionType: 'TCPsocket', - defaultPort: 3040, - mayChangePort: false, + remotePort: 3040, + mayChangePorts: false, heartbeatInterval: 250, heartbeatTimeout: 5000, searchOptions: { diff --git a/plugins/x32/main.js b/plugins/x32/main.js index 5021abd..6d28b87 100644 --- a/plugins/x32/main.js +++ b/plugins/x32/main.js @@ -1,8 +1,8 @@ exports.config = { defaultName: 'X32 Mixer', connectionType: 'osc-udp', - defaultPort: 10023, - mayChangePort: false, + remotePort: 10023, + mayChangePorts: false, heartbeatInterval: 9000, heartbeatTimeout: 11000, searchOptions: { diff --git a/plugins/xair/main.js b/plugins/xair/main.js index ffc70f3..482360b 100644 --- a/plugins/xair/main.js +++ b/plugins/xair/main.js @@ -1,8 +1,8 @@ exports.config = { defaultName: 'X Air Mixer', connectionType: 'UDPsocket', - defaultPort: 10024, - mayChangePort: false, + remotePort: 10024, + mayChangePorts: false, heartbeatInterval: 9000, heartbeatTimeout: 15000, searchOptions: { diff --git a/preload.js b/preload.js index 5e5aeca..cb7f165 100644 --- a/preload.js +++ b/preload.js @@ -59,6 +59,11 @@ window.init = function init() { DEVICE.changeActivePort(e.target.value); }; + document.getElementById('device-settings-rx-port').onchange = function portChange(e) { + e.stopPropagation(); + DEVICE.changeActiveRxPort(e.target.value); + }; + document.getElementById('device-settings-pin').onchange = function pinChange(e) { e.stopPropagation(); if (e.target.checked) { @@ -101,7 +106,7 @@ window.init = function init() { { type: e.target.value, defaultName: 'New Device', - port: undefined, + remotePort: PLUGINS.all[e.target.value].config.remotePort || '', addresses: [], }, 'fromAddButton' diff --git a/src/assets/css/index.css b/src/assets/css/index.css index f1884b4..c3b675a 100644 --- a/src/assets/css/index.css +++ b/src/assets/css/index.css @@ -182,7 +182,8 @@ a { #device-settings #device-settings-plugin-dropdown { width: 170px; } -#device-settings #device-settings-port { +#device-settings #device-settings-port, +#device-settings #device-settings-rx-port { width: 70px; } #network-indicator-dot { diff --git a/src/assets/css/plugin_default.css b/src/assets/css/plugin_default.css index 3a82e48..81eb742 100644 --- a/src/assets/css/plugin_default.css +++ b/src/assets/css/plugin_default.css @@ -43,7 +43,9 @@ header h2 { header h2 { color: #b6b6b6; } - +h3 { + font-weight: 300; +} table.cv-table { border: none; table-layout: fixed; diff --git a/src/device.js b/src/device.js index bb907db..c73fe7f 100644 --- a/src/device.js +++ b/src/device.js @@ -27,6 +27,16 @@ function registerDevice(newDevice, discoveryMethod) { return false; } + // set the local port for the new device + let newLocalPort = newDevice.localPort; + if (!newLocalPort) { + if (PLUGINS.all[newDevice.type].config.localPort) { + newLocalPort = PLUGINS.all[newDevice.type].config.localPort; + } else { + newLocalPort = newDevice.remotePort; + } + } + const id = newDevice.id || uuid(); devices[id] = { id, @@ -35,7 +45,8 @@ function registerDevice(newDevice, discoveryMethod) { type: newDevice.type, displayName: newDevice.displayName, defaultName: newDevice.defaultName, - port: newDevice.port, + remotePort: newDevice.remotePort, + localPort: newLocalPort, addresses: newDevice.addresses, data: {}, templates: {}, @@ -82,11 +93,11 @@ function initDeviceConnection(id) { infoUpdate(device, 'status', 'new'); - if (device.port === undefined) { - device.port = device.plugin.config.defaultPort; + if (device.remotePort === undefined) { + device.remotePort = device.plugin.config.defaultPort; } - if (device.port === undefined || device.addresses.length === 0) { + if (device.remotePort === undefined || device.addresses.length === 0) { return true; } try { @@ -103,13 +114,14 @@ function initDeviceConnection(id) { if (plugins[type].config.connectionType.includes('udp')) { device.connection = new osc.UDPPort({ localAddress: '0.0.0.0', + localPort: device.localPort, remoteAddress: device.addresses[0], - remotePort: device.port, + remotePort: device.remotePort, }); } else { device.connection = new osc.TCPSocketPort({ address: device.addresses[0], - port: device.port, + port: device.remotePort, }); } device.connection.open(); @@ -144,7 +156,7 @@ function initDeviceConnection(id) { device.connection = new net.Socket(); device.connection.connect( { - port: device.port, + port: device.remotePort, host: device.addresses[0], }, () => {} @@ -176,7 +188,7 @@ function initDeviceConnection(id) { } else if (plugins[type].config.connectionType === 'UDPsocket') { device.connection = udp.createSocket('udp4'); - device.connection.bind({ port: plugins[type].config.defaultPort }, () => { + device.connection.bind({ port: plugins[type].config.remotePort }, () => { plugins[type].ready(device); device.connection.on('message', (msg, info) => { @@ -198,7 +210,7 @@ function initDeviceConnection(id) { } else if (plugins[type].config.connectionType === 'multicast') { device.connection = udp.createSocket('udp4'); - device.connection.bind(device.port, () => { + device.connection.bind(device.remotePort, () => { plugins[type].ready(device); device.connection.on('message', (msg, info) => { @@ -287,7 +299,7 @@ module.exports.changeActiveIP = function changeActiveIP(newIP) { module.exports.changeActivePort = function changeActivePort(newPort) { const device = VIEW.getActiveDevice(); - device.port = newPort; + device.remotePort = newPort; initDeviceConnection(device.id); VIEW.draw(device); diff --git a/src/saveSlots.js b/src/saveSlots.js index 2ef2273..ac30072 100644 --- a/src/saveSlots.js +++ b/src/saveSlots.js @@ -62,7 +62,8 @@ module.exports.loadDevices = function loadDevices() { type: savedDevices[i].type, displayName: savedDevices[i].displayName, defaultName: savedDevices[i].defaultName, - port: savedDevices[i].port, + remotePort: savedDevices[i].remotePort, + localPort: savedDevices[i].localPort, addresses: savedDevices[i].addresses, id: savedDevices[i].id, fields: savedDevices[i].fields, @@ -110,7 +111,8 @@ module.exports.saveAll = function saveAll() { type: device.type, displayName: device.displayName, defaultName: device.defaultName, - port: device.port, + remotePort: device.remotePort, + localPort: device.localPort, id: device.id, fields: device.fields, }; diff --git a/src/search.js b/src/search.js index a713dbe..27bfcbc 100644 --- a/src/search.js +++ b/src/search.js @@ -130,7 +130,7 @@ function searchBonjour(pluginType, pluginConfig) { { type: pluginType, defaultName: e.name, - port: e.port, + remotePort: e.port, addresses: validAddresses, }, 'fromSearch' @@ -157,7 +157,7 @@ function TCPtest(ipAddr, pluginType, pluginConfig) { { type: pluginType, defaultName: pluginConfig.defaultName, - port: pluginConfig.defaultPort, + remotePort: pluginConfig.remotePort, addresses: [ipAddr], }, 'fromSearch' @@ -215,7 +215,7 @@ function searchUDP(pluginType, pluginConfig) { { type: pluginType, defaultName: pluginConfig.defaultName, - port: pluginConfig.defaultPort, + remotePort: pluginConfig.remotePort, addresses: [info.address], }, 'fromSearch' @@ -247,7 +247,7 @@ function searchMulticast(pluginType, pluginConfig) { { type: pluginType, defaultName: pluginConfig.defaultName, - port: pluginConfig.defaultPort, + remotePort: pluginConfig.remotePort, addresses: [info.address], }, 'fromSearch' diff --git a/src/view.js b/src/view.js index 3c750dc..1c144b1 100644 --- a/src/view.js +++ b/src/view.js @@ -190,13 +190,30 @@ function switchDevice(id) { document.getElementById('device-settings-plugin-dropdown').value = activeDevice.type; document.getElementById('device-settings-name').value = activeDevice.displayName || activeDevice.defaultName || ''; document.getElementById('device-settings-ip').value = activeDevice.addresses[0] || ''; - document.getElementById('device-settings-port').value = activeDevice.port || ''; + document.getElementById('device-settings-port').value = activeDevice.remotePort || ''; document.getElementById('device-settings-pin').checked = activeDevice.pinIndex; - if (activeDevice.plugin.config.mayChangePort) { + if (activeDevice.plugin.config.mayChangePorts) { document.getElementById('device-settings-port').disabled = false; + document.getElementById('device-settings-rx-port').disabled = false; } else { document.getElementById('device-settings-port').disabled = true; + document.getElementById('device-settings-rx-port').disabled = true; + } + + if (activeDevice.plugin.config.mayChangeLocalPort === false) { + document.getElementById('device-settings-rx-port').disabled = true; + } else { + document.getElementById('device-settings-rx-port').disabled = false; + } + + if (activeDevice.plugin.config.localPort) { + // document.getElementById('device-settings-port-label').textContent = 'Remote\nPort:'; + document.getElementById('device-settings-rx-port-row').style.display = 'table-row'; + document.getElementById('device-settings-rx-port').value = activeDevice.localPort || ''; + } else { + document.getElementById('device-settings-port-label').textContent = 'Port:'; + document.getElementById('device-settings-rx-port-row').style.display = 'none'; } updateFields();