Merge pull request #78 from stagehacks/plugin-atem

This has passed tests and has been cleaned up, send it!
This commit is contained in:
2023-04-04 10:51:36 -05:00
committed by GitHub
16 changed files with 4841 additions and 116 deletions
+4143 -115
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -30,6 +30,7 @@
"prettier": "^2.8.7"
},
"dependencies": {
"atem-connection": "^3.1.3",
"bonjour": "^3.5.0",
"electron-updater": "^5.3.0",
"lodash": "^4.17.21",
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

+11
View File
@@ -0,0 +1,11 @@
<h3>ATEM Configuration</h3>
<ul>
<li>Set the IP Address, Subnet Mask, and Gateway of the ATEM using the <em>ATEM Setup utility</em></li>
<li>The ATEM must be connected to a computer using a USB cable to change its Network settings</li>
</ul>
<h3>ATEM Software Download</h3>
<button href="https://www.blackmagicdesign.com/support/family/atem-live-production-switchers">
Blackmagic Support
</button>
(Look for <em>ATEM Switchers Update</em>)
+240
View File
@@ -0,0 +1,240 @@
const { TransitionStyle, TransitionSelection } = require('atem-connection/dist/enums');
let timerFrameRate;
exports.config = {
defaultName: 'ATEM',
connectionType: 'atem',
defaultPort: 9910,
mayChangePort: false,
searchOptions: {
type: 'UDPScan',
searchBuffer: Buffer.from([
0x10, 0x14, 0x53, 0xab, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3a, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00,
]),
listenPort: 0,
devicePort: 9910,
validateResponse(msg, info) {
// This is a tad bit hacky but works from what I can see.
return Buffer.compare(msg.slice(0, 4), Buffer.from([16, 20, 83, 171])) === 0;
},
},
};
exports.ready = function ready(_device) {
console.log('atem ready');
const device = _device;
device.data = device.connection.state;
device.draw();
// TODO: this is a little hacky but it works? gotta be a way to hook into device.draw() to trigger update
setInterval(() => {
device.update('inputs', device.data);
device.update('fadeToBlack', device.data);
device.update(`transitionPosition`, device.data);
device.update('downstreamKeyers', device.data);
device.update('upstreamKeyers', device.data);
device.update('transitionProperties', device.data);
device.update('deviceInfo', device.data);
}, 1000);
};
exports.update = function update(device, _document, updateType, data) {
const document = _document;
if (updateType.includes('transitionPosition')) {
for (let i = 0; i < data.video.mixEffects.length; i++) {
const mixEffect = data.video.mixEffects[i];
const tbarId = `me-${i}-tbar-div`;
const tbarHandleId = `me-${i}-tbar-handle-div`;
if (document.getElementById(tbarId)) {
document.getElementById(tbarId).style.height = `${mixEffect.transitionPosition.handlePosition / 100}%`;
}
if (document.getElementById(tbarHandleId)) {
document.getElementById(tbarHandleId).style.bottom = `${mixEffect.transitionPosition.handlePosition / 100}%`;
}
document.getElementById(`me-${i}-transition-rate`).textContent = framesToTime(
mixEffect.transitionPosition.remainingFrames
);
if (mixEffect.transitionPosition.inTransition) {
document.getElementById(`me-${i}-auto`).classList.add('atem-red');
} else {
document.getElementById(`me-${i}-auto`).classList.remove('atem-red');
}
}
} else if (updateType.includes('downstreamKeyers')) {
for (let i = 0; i < data.video.downstreamKeyers.length; i++) {
const dsk = data.video.downstreamKeyers[i];
if (dsk.isAuto) {
document.getElementById(`dsk-${i}-auto`).classList.add('atem-red');
} else {
document.getElementById(`dsk-${i}-auto`).classList.remove('atem-red');
}
if (dsk.onAir) {
document.getElementById(`dsk-${i}-onair`).classList.add('atem-red');
} else {
document.getElementById(`dsk-${i}-onair`).classList.remove('atem-red');
}
if (dsk.properties.tie) {
document.getElementById(`dsk-${i}-tie`).classList.add('atem-yellow');
} else {
document.getElementById(`dsk-${i}-tie`).classList.remove('atem-yellow');
}
document.getElementById(`dsk-${i}-rate`).textContent = framesToTime(dsk.remainingFrames);
}
} else if (updateType.includes('fadeToBlack')) {
for (let i = 0; i < data.video.mixEffects.length; i++) {
const fadeToBlack = data.video.mixEffects[i].fadeToBlack;
const ftbRateId = `me-${i}-ftb-rate`;
const ftbId = `me-${i}-ftb`;
document.getElementById(ftbRateId).textContent = framesToTime(fadeToBlack.remainingFrames);
if (fadeToBlack.isFullyBlack) {
if (document.getElementById(ftbId).classList.contains('atem-red')) {
document.getElementById(ftbId).classList.remove('atem-red');
} else {
document.getElementById(ftbId).classList.add('atem-red');
}
} else if (fadeToBlack.inTransition) {
document.getElementById(ftbId).classList.add('atem-red');
} else {
document.getElementById(ftbId).classList.remove('atem-red');
}
}
} else if (
updateType.includes('programInput') ||
updateType.includes('previewInput') ||
updateType.includes('inputs')
) {
for (let i = 0; i < data.video.mixEffects.length; i++) {
const mixEffect = data.video.mixEffects[0];
Object.keys(device.data.inputs)
.map((key) => Number(key))
.forEach((inputId) => {
if (mixEffect.programInput === inputId) {
document.getElementById(`me-${i}-program-input-${inputId}`).classList.add('atem-red');
document.getElementById(`me-${i}-input-${inputId}`).classList.add('atem-red');
} else {
document.getElementById(`me-${i}-program-input-${inputId}`).classList.remove('atem-red');
document.getElementById(`me-${i}-input-${inputId}`).classList.remove('atem-red');
}
if (mixEffect.previewInput === inputId) {
document.getElementById(`me-${i}-preview-input-${inputId}`).classList.add('atem-green');
document.getElementById(`me-${i}-input-${inputId}`).classList.add('atem-green');
} else {
document.getElementById(`me-${i}-preview-input-${inputId}`).classList.remove('atem-green');
document.getElementById(`me-${i}-input-${inputId}`).classList.remove('atem-green');
}
});
}
} else if (updateType.includes('transitionProperties')) {
for (let i = 0; i < data.video.mixEffects.length; i++) {
const transitionProperties = data.video.mixEffects[i].transitionProperties;
Object.keys(TransitionStyle)
.filter((key) => !isNaN(Number(key)))
.map((key) => Number(key))
.forEach((style) => {
if (style === transitionProperties.style) {
document.getElementById(`me-${i}-transition-style-${style}`).classList.add('atem-yellow');
} else {
document.getElementById(`me-${i}-transition-style-${style}`).classList.remove('atem-yellow');
}
if (style === transitionProperties.nextStyle) {
document.getElementById(`me-${i}-transition-style-${style}`).classList.add('atem-yellow');
} else {
document.getElementById(`me-${i}-transition-style-${style}`).classList.remove('atem-yellow');
}
});
Object.keys(TransitionSelection)
.filter((key) => !isNaN(Number(key)))
.map((key) => Number(key))
.forEach((selection) => {
if (transitionProperties.selection.includes(selection)) {
document.getElementById(`me-${i}-transition-selection-${selection}`).classList.add('atem-yellow');
} else {
document.getElementById(`me-${i}-transition-selection-${selection}`).classList.remove('atem-yellow');
}
if (transitionProperties.nextSelection.includes(selection)) {
document.getElementById(`me-${i}-transition-selection-${selection}`).classList.add('atem-yellow');
} else {
document.getElementById(`me-${i}-transition-selection-${selection}`).classList.remove('atem-yellow');
}
});
}
} else if (updateType.includes('upstreamKeyers')) {
for (let i = 0; i < data.video.mixEffects.length; i++) {
const upstreamKeyers = data.video.mixEffects[i].upstreamKeyers;
upstreamKeyers.forEach((upstreamKeyer) => {
if (upstreamKeyer.onAir) {
document.getElementById(`me-${i}-key-${upstreamKeyer.upstreamKeyerId + 1}-onair`).classList.add('atem-red');
} else {
document
.getElementById(`me-${i}-key-${upstreamKeyer.upstreamKeyerId + 1}-onair`)
.classList.remove('atem-red');
}
});
}
} else if (updateType.includes('transitionPreview')) {
for (let i = 0; i < data.video.mixEffects.length; i++) {
const mixEffect = data.video.mixEffects[i];
if (mixEffect.transitionPreview) {
document.getElementById(`me-${i}-transition-preview`).classList.add('atem-red');
} else {
document.getElementById(`me-${i}-transition-preview`).classList.remove('atem-red');
}
}
} else if (updateType.includes('deviceInfo')) {
// videoModes pulled from https://github.com/nrkno/sofie-atem-connection/blob/master/src/enums/index.ts#L238
if ([27, 26, 23, 25, 19, 13, 11, 7, 5].includes(data.settings.videoMode)) {
timerFrameRate = 30;
} else if ([24, 22, 18, 16, 12, 10, 6, 4].includes(data.settings.videoMode)) {
timerFrameRate = 25;
} else if ([21, 20, 15, 14, 9, 8].includes(data.settings.videoMode)) {
timerFrameRate = 24;
} else {
timerFrameRate = 30;
}
if (!device.displayName) {
this.deviceInfoUpdate(device, 'displayName', data.info.productIdentifier);
}
} else {
console.log('unhandled update');
console.log(updateType);
console.log(device.data);
}
};
exports.data = function data(_device, msg) {
const device = _device;
this.deviceInfoUpdate(device, 'status', 'ok');
device.data = msg.state;
msg.pathToChange.forEach((path) => {
device.update(path, device.data);
});
};
function framesToTime(total) {
if (timerFrameRate) {
const seconds = Math.floor(total / timerFrameRate);
const frames = total - seconds * timerFrameRate;
let framesString = String(frames);
if (frames < 10) {
framesString = `0${framesString}`;
} else if (framesString === '0') {
framesString = '00';
}
return `${seconds}:${framesString}`;
}
return '0:00';
}
+203
View File
@@ -0,0 +1,203 @@
body {
background-color: #282828;
}
h1 {
font-family: 'Open Sans', sans-serif;
}
h3 {
color: #6d6d6d !important;
margin: 30px 0px 10px 9px;
font-size: 0.9em;
font-weight: 400;
font-family: 'Open Sans', sans-serif;
}
.me-label {
margin-top: 0;
margin-bottom: 2px;
}
.atem-input {
width: 52px;
height: 52px;
background-image: url('img/button_white.png');
}
.source-wrapper {
display: flex;
flex-wrap: wrap;
background-color: #1f1f1f;
border: #1a1a1a 2px solid;
border-radius: 8px;
max-width: 416px;
padding: 12px;
}
.atem-red {
background-image: url('img/button_red.png');
box-shadow: 0px 0px 10px 1px #ff0000;
z-index: 10000;
}
.atem-green {
background-image: url('img/button_green.png');
box-shadow: 0px 0px 10px 1px #06c300;
z-index: 10000;
}
.atem-yellow {
background-image: url('img/button_yellow.png');
box-shadow: 0px 0px 10px 1px #c7ca00;
z-index: 10000;
}
.atem-disabled {
background-image: url('img/button_off.png');
color: #3a3a3a;
}
.atem-gray {
color: #575757;
}
.source-label {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
font-size: 11px;
font-weight: bold;
}
.transition-container {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
.transition-settings {
display: flex;
flex-direction: column;
justify-content: space-between;
margin-right: 30px;
}
.tbar-container {
display: flex;
flex-direction: column-reverse;
background-color: #1f1f1f;
border: 2px solid;
border-color: #1a1a1a;
border-radius: 6px;
width: 88px;
height: 429px;
position: relative;
margin-top: 58px;
margin-right: 60px;
}
.tbar-bg {
position: absolute;
background-image: url('img/tbar_bg.png');
width: 88px;
height: 429px;
}
.tbar-handle {
position: absolute;
background-image: url('img/tbar_handle.png');
width: 126px;
height: 50px;
left: -3px;
}
.tbar-div {
width: 100%;
background-color: #7aff58;
overflow: hidden;
}
.dsk {
width: 100%;
}
.dsk .source-wrapper {
display: flex;
flex-direction: column;
align-content: center;
width: 100%;
padding: 5px;
}
.dsk h3 {
text-align: center;
}
.fade-to-black-container {
display: flex;
flex-direction: column;
align-items: flex-end;
margin-right: 30px;
}
.fade-to-black .source-wrapper {
display: flex;
width: 100%;
padding: 5px;
}
.fade-to-black h3 {
text-align: center;
}
.rate-heading {
text-align: center;
font-size: small;
margin-bottom: 4px;
}
.dsk-rate,
.ftb-rate,
.transition-rate {
display: flex;
justify-content: center;
flex-direction: column;
}
.dsk-rate-label,
.ftb-rate-label,
.transition-rate-label {
color: rgb(235, 110, 0);
background-color: black;
border-radius: 3px;
padding: 6px 2px;
text-align: center;
}
.clear {
background: transparent;
border-color: transparent;
background-color: transparent;
}
.hide {
display: none;
}
.no-wrap {
flex-wrap: nowrap;
}
.show-small {
display: none;
}
@media screen and (min-width: 0px) and (max-width: 480px) {
.hide-small {
display: none;
}
.show-small {
display: block;
}
}
.float-left {
float: left;
}
+189
View File
@@ -0,0 +1,189 @@
<header>
<h1><%= listName %></h1>
</header>
<% if (data.video.mixEffects) { %>
<% Object.entries(data.video.mixEffects).forEach(([meIndex, state])=>{ %>
<div class="mixeffect_wrapper" id="me-<%= meIndex %>">
<h1 class="atem-gray me-label">ME<%= Number(meIndex) + 1 %></h1>
<div class="inputs-container show-small">
<h3 class="atem-gray">Preview/Program</h3>
<div class="source-wrapper">
<% Object.values(data.inputs).sort((a,b)=>a.internalPortType<b.internalPortType).forEach((input)=>{ %>
<% if ([0,1,2,3,4].includes(input.internalPortType)) { %>
<div id="me-<%= meIndex %>-input-<%= input.inputId %>" class="atem-input">
<div class="source-label"><%= input.shortName %></div>
</div>
<% } else {%>
<div id="me-<%= meIndex %>-input-<%= input.inputId %>" class="atem-input atem-disabled">
<div class="source-label"><%= input.shortName %></div>
</div>
<% }%>
<% }) %>
</div>
</div>
<div class="float-left" style="margin-right: 30px;">
<div class="program-container hide-small">
<h3 class="atem-gray">Program</h3>
<div class="source-wrapper">
<% Object.values(data.inputs).sort((a,b)=>a.internalPortType<b.internalPortType).forEach((input)=>{ %>
<% if ([0,1,2,3,4].includes(input.internalPortType)) { %>
<div id="me-<%= meIndex %>-program-input-<%= input.inputId %>" class="atem-input">
<div class="source-label"><%= input.shortName %></div>
</div>
<% } else {%>
<div id="me-<%= meIndex %>-program-input-<%= input.inputId %>" class="atem-input atem-disabled">
<div class="source-label"><%= input.shortName %></div>
</div>
<% }%>
<% }) %>
</div>
</div>
<div class="preview hide-small">
<h3 class="atem-gray">Preview</h3>
<div class="source-wrapper">
<% Object.values(data.inputs).forEach((input)=>{ %>
<% if ([0,1,2,3,4].includes(input.internalPortType)) { %>
<div id="me-<%= meIndex %>-preview-input-<%= input.inputId %>" class="atem-input">
<div class="source-label"><%= input.shortName %></div>
</div>
<% } else {%>
<div id="me-<%= meIndex %>-preview-input-<%= input.inputId %>" class="atem-input atem-disabled">
<div class="source-label"><%= input.shortName %></div>
</div>
<% }%>
<% }) %>
</div>
</div>
</div>
<div class="transition-container float-left">
<div class="transition-settings">
<div class="next-transition">
<h3>Next Transition</h3>
<div class="source-wrapper" style="width: 260px;">
<div class="atem-input clear"></div>
<div id="me-<%= meIndex %>-key-1-onair" class="atem-input">
<div class="source-label" style="flex-direction: column;">
<div>ON</div> <div>AIR</div>
</div>
</div>
<div id="me-<%= meIndex %>-key-2-onair" class="atem-input <% if(data.video.downstreamKeyers.length<2){ %> atem-disabled <% } %>">
<div class="source-label" style="flex-direction: column;">
<div>ON</div> <div>AIR</div>
</div>
</div>
<div id="me-<%= meIndex %>-key-3-onair" class="atem-input <% if(data.video.downstreamKeyers.length<3){ %> atem-disabled <% } %>">
<div class="source-label" style="flex-direction: column;">
<div>ON</div> <div>AIR</div>
</div>
</div>
<div id="me-<%= meIndex %>-key-4-onair" class="atem-input <% if(data.video.downstreamKeyers.length<4){ %> atem-disabled <% } %>">
<div class="source-label" style="flex-direction: column;">
<div>ON</div> <div>AIR</div>
</div>
</div>
<div id="me-<%= meIndex %>-transition-selection-1" class="atem-input">
<div class="source-label">BKGD</div>
</div>
<div id="me-<%= meIndex %>-transition-selection-2" class="atem-input">
<div class="source-label">KEY 1</div>
</div>
<div id="me-<%= meIndex %>-transition-selection-4" class="atem-input <% if(data.video.downstreamKeyers.length<2){ %> atem-disabled <% } %>">
<div class="source-label">KEY 2</div>
</div>
<div id="me-<%= meIndex %>-transition-selection-8" class="atem-input <% if(data.video.downstreamKeyers.length<3){ %> atem-disabled <% } %>">
<div class="source-label">KEY 3</div>
</div>
<div id="me-<%= meIndex %>-transition-selection-16" class="atem-input <% if(data.video.downstreamKeyers.length<4){ %> atem-disabled <% } %>">
<div class="source-label">KEY 4</div>
</div>
</div>
</div>
<div class="transition-style">
<h3>Transition Style</h3>
<div class="source-wrapper" style="width: 260px;">
<div id="me-<%= meIndex %>-transition-style-0" class="atem-input">
<div class="source-label">MIX</div>
</div>
<div id="me-<%= meIndex %>-transition-style-1" class="atem-input">
<div class="source-label">DIP</div>
</div>
<div id="me-<%= meIndex %>-transition-style-2" class="atem-input">
<div class="source-label">WIPE</div>
</div>
<div id="me-<%= meIndex %>-transition-style-4" class="atem-input">
<div class="source-label">STING</div>
</div>
<div id="me-<%= meIndex %>-transition-style-3" class="atem-input">
<div class="source-label">DVE</div>
</div>
<div id="me-<%= meIndex %>-transition-preview" class="atem-input">
<div class="source-label" style="flex-direction: column;">
<div>PREV</div> <div>TRANS</div>
</div>
</div>
<div class="atem-input clear"></div>
<div id="me-<%= meIndex %>-cut" class="atem-input">
<div class="source-label">CUT</div>
</div>
<div id="me-<%= meIndex %>-auto" class="atem-input">
<div class="source-label">AUTO</div>
</div>
<div class="transition-rate atem-input clear">
<div class="atem-gray rate-heading">Rate</div>
<div id="me-<%= meIndex %>-transition-rate" class="transition-rate-label">&nbsp;&nbsp;</div>
</div>
</div>
</div>
</div>
<div class="tbar-container">
<div class="tbar-bg"></div>
<div id="me-<%= meIndex %>-tbar-handle-div" class="tbar-handle"></div>
<div id="me-<%= meIndex %>-tbar-div" class="tbar-div" ></div>
</div>
<div class="fade-to-black-container">
<% if (data.video.downstreamKeyers) { %>
<% Object.entries(data.video.downstreamKeyers).forEach(([dskIndex, state])=>{ %>
<div class="dsk">
<h3 class="atem-gray">DSK<%= Number(dskIndex) + 1 %></h3>
<div class="source-wrapper">
<div id="dsk-<%= dskIndex %>-tie" class="atem-input">
<div class="source-label">TIE</div>
</div>
<div class="dsk-rate atem-input clear">
<div class="atem-gray rate-heading">Rate</div>
<div id="dsk-<%= dskIndex %>-rate" class="dsk-rate-label">&nbsp;&nbsp;</div>
</div>
<div id="dsk-<%= dskIndex %>-onair" class="atem-input">
<div class="source-label" style="flex-direction: column;">
<div>ON</div> <div>AIR</div>
</div>
</div>
<div id="dsk-<%= dskIndex %>-auto" class="atem-input">
<div class="source-label">AUTO</div>
</div>
</div>
</div>
<% }) %>
<% } %>
<div class="fade-to-black">
<h3 class="atem-gray">Fade to Black</h3>
<div class="source-wrapper">
<div class="ftb-rate atem-input clear">
<div class="atem-gray rate-heading">Rate</div>
<div id="me-<%= meIndex %>-ftb-rate" class="ftb-rate-label">&nbsp;&nbsp;</div>
</div>
<div id="me-<%= meIndex %>-ftb" class="atem-input">
<div class="source-label">FTB</div>
</div>
</div>
</div>
</div>
</div>
</div>
<% }) %>
<% } %>
+21 -1
View File
@@ -2,7 +2,7 @@ const { v4: uuid } = require('uuid');
const osc = require('osc');
const net = require('net');
const udp = require('dgram');
const { Atem } = require('atem-connection');
const PLUGINS = require('./plugins.js');
const VIEW = require('./view.js');
const SAVESLOTS = require('./saveSlots.js');
@@ -193,6 +193,26 @@ function initDeviceConnection(id) {
});
device.send = (data) => {};
} else if (plugins[type].config.connectionType === 'atem') {
device.connection = new Atem({
// this gets around the no workers nodejs error
disableMultithreaded: true,
});
device.connection.connect(device.addresses[0]);
device.connection.on('connected', () => {
infoUpdate(device, 'status', 'ok');
plugins[type].ready(device);
});
device.connection.on('stateChanged', (state, pathToChange) => {
device.lastMessage = Date.now();
plugins[type].data(device, {
pathToChange,
state,
});
infoUpdate(device, 'status', 'ok');
});
}
return true;
+33
View File
@@ -5,6 +5,7 @@ const net = require('net');
const os = require('os');
const ip = require('ip');
const { Netmask } = require('netmask');
const DEVICE = require('./device.js');
const PLUGINS = require('./plugins.js');
@@ -91,6 +92,8 @@ function searchAll() {
searchUDP(pluginType, plugin.config);
} else if (searchType === 'multicast') {
searchMulticast(pluginType, plugin.config);
} else if (searchType === 'UDPScan') {
searchUDPScan(pluginType, plugin.config);
}
} catch (err) {
console.error(`Unable to search for plugin ${pluginType}`);
@@ -167,6 +170,36 @@ function TCPtest(ipAddr, pluginType, pluginConfig) {
});
}
function searchUDPScan(pluginType, pluginConfig) {
for (let i = 0; i < Object.keys(validInterfaces).length; i++) {
const interfaceID = Object.keys(validInterfaces)[i];
const interfaceObj = validInterfaces[interfaceID];
interfaceObj.forEach((netInterface) => {
const udpSocket = dgram.createSocket('udp4');
udpSocket.bind(pluginConfig.searchOptions.listenPort, netInterface.address);
udpSocket.on('message', (msg, info) => {
if (pluginConfig.searchOptions.validateResponse(msg, info, DEVICE.all)) {
udpSocket.close();
DEVICE.registerDevice(
{
type: pluginType,
defaultName: pluginConfig.defaultName,
port: pluginConfig.defaultPort,
addresses: [info.address],
},
'fromSearch'
);
}
});
const interfaceBlock = new Netmask(netInterface.cidr);
interfaceBlock.forEach((address, long, index) => {
udpSocket.send(pluginConfig.searchOptions.searchBuffer, pluginConfig.searchOptions.devicePort, address);
});
});
}
}
function searchUDP(pluginType, pluginConfig) {
for (let i = 0; i < Object.keys(validInterfaces).length; i++) {
const interfaceID = Object.keys(validInterfaces)[i];