feat: add diagnostics page and functionality, enhance transport settings in index.html
- Introduced a new diagnostics page (diagnostics.html) with real-time monitoring for Art-Net, sACN, MQTT, and KNX. - Added JavaScript functionality (diagnostics.js) to handle diagnostics data and render updates. - Enhanced index.html with new transport settings including timeline length, beat snap options, and diagnostics button. - Updated styles.css to accommodate new diagnostics layout and transport settings. - Added splash screen (splash.html) for improved user experience during startup. - Included new assets: Time4Midi.psd and updated executable files for version 0.3.6.
This commit is contained in:
+100
-4
@@ -69,6 +69,28 @@ function addStatusApi(target, handler) {
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeDiagnosticsChannelValues(values) {
|
||||
if (!Array.isArray(values)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return values.slice(0, 512).map((value) => {
|
||||
const safeValue = Math.round(Number(value) || 0);
|
||||
return Math.max(0, Math.min(255, safeValue));
|
||||
});
|
||||
}
|
||||
|
||||
function recordDiagnostics(record) {
|
||||
try {
|
||||
ipcRenderer.send("diagnostics:record", {
|
||||
...record,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
} catch (_error) {
|
||||
// Ignore diagnostics forwarding issues to avoid blocking output.
|
||||
}
|
||||
}
|
||||
|
||||
const artnet = {
|
||||
...createStatusEmitter(),
|
||||
sendSocket: null,
|
||||
@@ -297,7 +319,13 @@ function closeUdpSocket(target, key, signatureKey) {
|
||||
}
|
||||
|
||||
async function ensureUdpSocket(target, key, signatureKey, label, config, options = {}) {
|
||||
const signature = `${config.localAddress || ""}|${options.broadcast ? "1" : "0"}`;
|
||||
const signature = [
|
||||
config.localAddress || "",
|
||||
options.broadcast ? "1" : "0",
|
||||
options.multicast ? "1" : "0",
|
||||
options.multicastInterface || "",
|
||||
Number.isFinite(Number(options.multicastTtl)) ? String(Number(options.multicastTtl)) : ""
|
||||
].join("|");
|
||||
if (target[key] && target[signatureKey] === signature) {
|
||||
return target[key];
|
||||
}
|
||||
@@ -308,8 +336,14 @@ async function ensureUdpSocket(target, key, signatureKey, label, config, options
|
||||
target[signatureKey] = signature;
|
||||
|
||||
const bindAddress = config.localAddress || "0.0.0.0";
|
||||
const requiresBinding = Boolean(config.localAddress || options.broadcast);
|
||||
const requiresBinding = Boolean(
|
||||
config.localAddress
|
||||
|| options.broadcast
|
||||
|| options.multicast
|
||||
|| options.forceBind
|
||||
);
|
||||
if (!requiresBinding) {
|
||||
emitStatus(target, "");
|
||||
return socket;
|
||||
}
|
||||
|
||||
@@ -346,6 +380,14 @@ async function ensureUdpSocket(target, key, signatureKey, label, config, options
|
||||
if (options.broadcast) {
|
||||
socket.setBroadcast(true);
|
||||
}
|
||||
if (options.multicast) {
|
||||
if (Number.isFinite(Number(options.multicastTtl))) {
|
||||
socket.setMulticastTTL(Number(options.multicastTtl));
|
||||
}
|
||||
if (options.multicastInterface) {
|
||||
socket.setMulticastInterface(options.multicastInterface);
|
||||
}
|
||||
}
|
||||
|
||||
emitStatus(target, "");
|
||||
return socket;
|
||||
@@ -453,6 +495,7 @@ async function sendArtNetDmx(payload) {
|
||||
|
||||
const universe = Math.max(0, Math.min(32767, Number(payload.universe) || 0));
|
||||
const values = Array.isArray(payload.values) ? payload.values : [];
|
||||
const diagnosticsValues = normalizeDiagnosticsChannelValues(values);
|
||||
const length = Math.max(2, Math.min(512, values.length || 512));
|
||||
const evenLength = length % 2 === 0 ? length : length + 1;
|
||||
const packet = Buffer.alloc(18 + evenLength);
|
||||
@@ -487,6 +530,12 @@ async function sendArtNetDmx(payload) {
|
||||
return;
|
||||
}
|
||||
emitStatus(artnet, "");
|
||||
recordDiagnostics({
|
||||
kind: "dmx-frame",
|
||||
transport: "artnet",
|
||||
universe,
|
||||
values: diagnosticsValues
|
||||
});
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
@@ -550,8 +599,12 @@ async function sendOsc(payload) {
|
||||
|
||||
function configureSacn(nextConfig) {
|
||||
const previousLocalAddress = sacn.config.localAddress;
|
||||
const previousMulticast = sacn.config.multicast;
|
||||
sacn.config = normalizeSacnConfig(nextConfig);
|
||||
if (previousLocalAddress !== sacn.config.localAddress) {
|
||||
if (
|
||||
previousLocalAddress !== sacn.config.localAddress
|
||||
|| previousMulticast !== sacn.config.multicast
|
||||
) {
|
||||
closeUdpSocket(sacn, "socket", "socketSignature");
|
||||
}
|
||||
emitStatus(sacn, "");
|
||||
@@ -588,6 +641,7 @@ async function sendSacnDmx(payload) {
|
||||
|
||||
const universe = clampSacnUniverse(payload.universe);
|
||||
const values = Array.isArray(payload.values) ? payload.values : [];
|
||||
const diagnosticsValues = normalizeDiagnosticsChannelValues(values);
|
||||
const slotCount = Math.max(1, Math.min(512, values.length || 512));
|
||||
const packetLength = 126 + slotCount;
|
||||
const propertyValueCount = slotCount + 1;
|
||||
@@ -624,7 +678,19 @@ async function sendSacnDmx(payload) {
|
||||
|
||||
sacn.sequence = sacn.sequence >= 255 ? 1 : sacn.sequence + 1;
|
||||
const host = sacnDestination(universe);
|
||||
const socket = await ensureUdpSocket(sacn, "socket", "socketSignature", "sACN", sacn.config);
|
||||
const socket = await ensureUdpSocket(
|
||||
sacn,
|
||||
"socket",
|
||||
"socketSignature",
|
||||
"sACN",
|
||||
sacn.config,
|
||||
{
|
||||
multicast: sacn.config.multicast,
|
||||
multicastInterface: sacn.config.localAddress || "",
|
||||
multicastTtl: 32,
|
||||
forceBind: sacn.config.multicast
|
||||
}
|
||||
);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
socket.send(packet, 0, packet.length, sacn.config.port, host, (error) => {
|
||||
@@ -634,6 +700,12 @@ async function sendSacnDmx(payload) {
|
||||
return;
|
||||
}
|
||||
emitStatus(sacn, "");
|
||||
recordDiagnostics({
|
||||
kind: "dmx-frame",
|
||||
transport: "sacn",
|
||||
universe,
|
||||
values: diagnosticsValues
|
||||
});
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
@@ -707,6 +779,11 @@ function publishMqtt(payload) {
|
||||
return;
|
||||
}
|
||||
emitStatus(mqttState, "");
|
||||
recordDiagnostics({
|
||||
kind: "mqtt-publish",
|
||||
topic: String(payload.topic || ""),
|
||||
value: String(payload.value ?? "")
|
||||
});
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
@@ -1104,6 +1181,12 @@ async function sendKnx(payload) {
|
||||
return;
|
||||
}
|
||||
emitStatus(knxState, "");
|
||||
recordDiagnostics({
|
||||
kind: "knx-write",
|
||||
groupAddress: String(payload.groupAddress || "1/1/1"),
|
||||
dpt,
|
||||
value
|
||||
});
|
||||
resolve(true);
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -1134,6 +1217,19 @@ contextBridge.exposeInMainWorld("time4midi", {
|
||||
importFile: (defaultPath) => importMappingLibrary(defaultPath),
|
||||
exportFile: (defaultPath, data) => exportMappingLibrary(defaultPath, data)
|
||||
},
|
||||
diagnostics: {
|
||||
openWindow: () => ipcRenderer.invoke("diagnostics:openWindow"),
|
||||
getSnapshot: () => ipcRenderer.invoke("diagnostics:getSnapshot"),
|
||||
subscribe: (handler) => {
|
||||
const listener = (_event, record) => {
|
||||
handler(record);
|
||||
};
|
||||
ipcRenderer.on("diagnostics:update", listener);
|
||||
return () => {
|
||||
ipcRenderer.removeListener("diagnostics:update", listener);
|
||||
};
|
||||
}
|
||||
},
|
||||
network: {
|
||||
listInterfaces: () => listNetworkInterfaces()
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user