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:
jhartworks
2026-06-17 14:57:38 +02:00
parent b65b0461bc
commit b5c6d33c2d
14 changed files with 2315 additions and 100 deletions
+100 -4
View File
@@ -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()
},