mirror of
https://github.com/stagehacks/Cue-View.git
synced 2026-08-03 21:07:48 +00:00
84c0921943
* Feature: DPA N-Series support Connects to N-Series receivers (e.g. N-DR1) via OSC over TCP. Displays audio level, RF strength (A/B), battery status and warnings for both channels. * Add Lato font (bundled, offline-safe) to DPA plugin * Add DPA plugin icon * Fix: TCP framing, correct meter scaling - Fix osc-tcp connection type being caught by osc SLIP handler in device.js - Add subscriptions for tx/active, tx/name, tx/batterystatus - Fix audio meter formula direction (was inverted) - Scale audiolevel and rfstrength by /10 (values are dB×10) * Fix: CSS
156 lines
5.2 KiB
JavaScript
156 lines
5.2 KiB
JavaScript
const osc = require('osc');
|
||
|
||
exports.config = {
|
||
defaultName: 'DPA N-Series',
|
||
connectionType: 'osc-tcp',
|
||
defaultPort: 1993,
|
||
mayChangePorts: true,
|
||
heartbeatInterval: 5000,
|
||
heartbeatTimeout: 10000,
|
||
searchOptions: {
|
||
type: 'TCPport',
|
||
// OSC /model message with SLIP framing: END + "/model\0\0" + ",\0\0\0" + END
|
||
searchBuffer: Buffer.from([
|
||
0xc0, 0x2f, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, 0xc0,
|
||
]),
|
||
testPort: 1993,
|
||
validateResponse(msg) {
|
||
const str = msg.toString();
|
||
return str.includes('/model') || str.includes('N-DR');
|
||
},
|
||
},
|
||
};
|
||
|
||
function makeChannel() {
|
||
return {
|
||
name: '–',
|
||
audioLevel: -80,
|
||
txActive: false,
|
||
txName: '',
|
||
battCapacity: null,
|
||
battTimeMin: null,
|
||
battHealth: null,
|
||
rfA: -130,
|
||
rfB: -130,
|
||
warnings: {},
|
||
};
|
||
}
|
||
|
||
function subscribeToAddress(device, address, intervalMs) {
|
||
const msgBlob = osc.writePacket({ address, args: [] });
|
||
device.send('/subscribe', [
|
||
{ type: 'i', value: intervalMs },
|
||
{ type: 'i', value: 0 },
|
||
{ type: 'b', value: msgBlob },
|
||
]);
|
||
}
|
||
|
||
exports.ready = function ready(_device) {
|
||
const device = _device;
|
||
device.data.model = '';
|
||
device.data.channels = [null, makeChannel(), makeChannel()];
|
||
device.data.warnings = {};
|
||
|
||
device.send('/settings/retrieve');
|
||
device.send('/model');
|
||
|
||
subscribeToAddress(device, '/ch/1/audiolevel', 100);
|
||
subscribeToAddress(device, '/ch/2/audiolevel', 100);
|
||
subscribeToAddress(device, '/advanced/1/antenna/rfstrength', 200);
|
||
subscribeToAddress(device, '/advanced/2/antenna/rfstrength', 200);
|
||
subscribeToAddress(device, '/ch/1/tx/active', 500);
|
||
subscribeToAddress(device, '/ch/2/tx/active', 500);
|
||
subscribeToAddress(device, '/ch/1/tx/name', 5000);
|
||
subscribeToAddress(device, '/ch/2/tx/name', 5000);
|
||
subscribeToAddress(device, '/ch/1/tx/batterystatus', 1000);
|
||
subscribeToAddress(device, '/ch/2/tx/batterystatus', 1000);
|
||
};
|
||
|
||
exports.data = function data(_device, message) {
|
||
const device = _device;
|
||
this.deviceInfoUpdate(device, 'status', 'ok');
|
||
|
||
const { address, args } = message;
|
||
if (!address) return;
|
||
|
||
const parts = address.split('/').filter(Boolean);
|
||
|
||
if (address === '/model') {
|
||
if (args && args[0]) {
|
||
device.data.model = args[0];
|
||
this.deviceInfoUpdate(device, 'defaultName', args[0]);
|
||
}
|
||
} else if (address === '/heartbeat') {
|
||
device.draw();
|
||
} else if (parts[0] === 'ch' && parts.length >= 3) {
|
||
const chNum = parseInt(parts[1], 10);
|
||
if (chNum < 1 || chNum > 2) return;
|
||
const ch = device.data.channels[chNum];
|
||
const sub = parts.slice(2).join('/');
|
||
|
||
if (sub === 'name') {
|
||
ch.name = (args && args[0]) ? args[0] : '–';
|
||
device.draw();
|
||
} else if (sub === 'audiolevel') {
|
||
ch.audioLevel = (args && args[0] !== undefined) ? args[0] / 10 : -80;
|
||
device.update('meterUpdate', { channels: device.data.channels });
|
||
} else if (sub === 'tx/active') {
|
||
ch.txActive = args && args[0] === 1;
|
||
device.draw();
|
||
} else if (sub === 'tx/name') {
|
||
ch.txName = (args && args[0]) ? args[0] : '';
|
||
device.draw();
|
||
} else if (sub === 'tx/batterystatus') {
|
||
if (args) {
|
||
ch.battCapacity = args[0] !== undefined ? args[0] : null;
|
||
ch.battTimeMin = args[1] !== undefined ? args[1] : null;
|
||
ch.battHealth = args[2] !== undefined ? args[2] : null;
|
||
}
|
||
device.draw();
|
||
}
|
||
} else if (parts[0] === 'advanced' && parts[2] === 'antenna' && parts[3] === 'rfstrength') {
|
||
const chNum = parseInt(parts[1], 10);
|
||
if (chNum < 1 || chNum > 2) return;
|
||
const ch = device.data.channels[chNum];
|
||
ch.rfA = (args && args[1] !== undefined) ? args[1] / 10 : -130;
|
||
ch.rfB = (args && args[2] !== undefined) ? args[2] / 10 : -130;
|
||
device.update('meterUpdate', { channels: device.data.channels });
|
||
} else if (parts[0] === 'warning') {
|
||
if (parts.length === 2) {
|
||
device.data.warnings[parts[1]] = args && args[0] === 1;
|
||
device.draw();
|
||
} else if (parts.length === 3) {
|
||
const chNum = parseInt(parts[1], 10);
|
||
if (chNum >= 1 && chNum <= 2) {
|
||
device.data.channels[chNum].warnings[parts[2]] = args && args[0] === 1;
|
||
device.draw();
|
||
}
|
||
}
|
||
}
|
||
};
|
||
|
||
exports.update = function update(device, doc, updateType, data) {
|
||
for (let i = 1; i <= 2; i++) {
|
||
const ch = data.channels[i];
|
||
|
||
// Audio level bar: -126 dBFS = empty, 0 dBFS = full
|
||
const audioOverlay = Math.max(0, Math.min(90, (1 - (ch.audioLevel + 126) / 126) * 90));
|
||
const $audio = doc.getElementById(`ch-${i}-audio`);
|
||
if ($audio) $audio.style.height = `${audioOverlay}px`;
|
||
const $audioText = doc.getElementById(`ch-${i}-audio-text`);
|
||
if ($audioText) $audioText.textContent = `${ch.audioLevel}`;
|
||
|
||
// RF bars: -130 to -50 dBm range (80 dBm span)
|
||
const rfANorm = Math.max(0, Math.min(1, (ch.rfA + 130) / 80));
|
||
const rfBNorm = Math.max(0, Math.min(1, (ch.rfB + 130) / 80));
|
||
const $rfA = doc.getElementById(`ch-${i}-rfa`);
|
||
if ($rfA) $rfA.style.height = `${(1 - rfANorm) * 90}px`;
|
||
const $rfB = doc.getElementById(`ch-${i}-rfb`);
|
||
if ($rfB) $rfB.style.height = `${(1 - rfBNorm) * 90}px`;
|
||
}
|
||
};
|
||
|
||
exports.heartbeat = function heartbeat(device) {
|
||
device.send('/settings/retrieve');
|
||
};
|