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
+214 -3
View File
@@ -4,14 +4,162 @@ const path = require("node:path");
app.commandLine.appendSwitch("enable-blink-features", "WebMidi");
let mainWindow = null;
let diagnosticsWindow = null;
let splashWindow = null;
const diagnosticsState = {
artnet: { universes: {} },
sacn: { universes: {} },
mqtt: { topics: {} },
knx: { groupAddresses: {} },
recentEvents: []
};
function rememberRecentEvent(entry) {
diagnosticsState.recentEvents.unshift(entry);
diagnosticsState.recentEvents = diagnosticsState.recentEvents.slice(0, 200);
}
function sameChannelValues(left, right) {
if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) {
return false;
}
for (let index = 0; index < left.length; index += 1) {
if (Number(left[index] || 0) !== Number(right[index] || 0)) {
return false;
}
}
return true;
}
function updateDmxDiagnostics(transport, record) {
const safeTransport = transport === "artnet" ? "artnet" : "sacn";
const previous = diagnosticsState[safeTransport].universes[String(record.universe)];
const nextValues = Array.isArray(record.values) ? record.values.slice(0, 512) : [];
const changed = !previous || !sameChannelValues(previous.values, nextValues);
diagnosticsState[safeTransport].universes[String(record.universe)] = {
universe: Number(record.universe) || 0,
values: nextValues,
updatedAt: record.timestamp
};
if (changed) {
rememberRecentEvent({
kind: "dmx-frame",
transport: safeTransport,
universe: Number(record.universe) || 0,
activeChannels: nextValues.filter((value) => Number(value) > 0).length,
timestamp: record.timestamp
});
}
}
function updateMqttDiagnostics(record) {
diagnosticsState.mqtt.topics[String(record.topic || "")] = {
topic: String(record.topic || ""),
value: String(record.value ?? ""),
updatedAt: record.timestamp
};
rememberRecentEvent({
kind: "mqtt-publish",
topic: String(record.topic || ""),
value: String(record.value ?? ""),
timestamp: record.timestamp
});
}
function updateKnxDiagnostics(record) {
const key = `${record.groupAddress || ""}:${record.dpt || ""}`;
diagnosticsState.knx.groupAddresses[key] = {
groupAddress: String(record.groupAddress || ""),
dpt: String(record.dpt || ""),
value: record.value,
updatedAt: record.timestamp
};
rememberRecentEvent({
kind: "knx-write",
groupAddress: String(record.groupAddress || ""),
dpt: String(record.dpt || ""),
value: record.value,
timestamp: record.timestamp
});
}
function applyDiagnosticsRecord(record) {
if (!record || typeof record !== "object") {
return;
}
if (record.kind === "dmx-frame") {
updateDmxDiagnostics(record.transport, record);
} else if (record.kind === "mqtt-publish") {
updateMqttDiagnostics(record);
} else if (record.kind === "knx-write") {
updateKnxDiagnostics(record);
}
}
function sendDiagnosticsUpdate(record) {
if (!diagnosticsWindow || diagnosticsWindow.isDestroyed()) {
return;
}
diagnosticsWindow.webContents.send("diagnostics:update", record);
}
function closeSplashWindow() {
if (!splashWindow || splashWindow.isDestroyed()) {
splashWindow = null;
return;
}
splashWindow.close();
splashWindow = null;
}
function createSplashWindow() {
if (splashWindow && !splashWindow.isDestroyed()) {
return splashWindow;
}
splashWindow = new BrowserWindow({
width: 520,
height: 420,
frame: false,
resizable: false,
minimizable: false,
maximizable: false,
movable: true,
show: true,
center: true,
backgroundColor: "#0f1117",
icon: path.join(__dirname, "time4midi.png"),
webPreferences: {
contextIsolation: true,
sandbox: false
}
});
splashWindow.on("closed", () => {
splashWindow = null;
});
splashWindow.loadFile(path.join(__dirname, "src", "splash.html"));
return splashWindow;
}
function createWindow() {
const win = new BrowserWindow({
mainWindow = new BrowserWindow({
width: 1600,
height: 980,
minWidth: 1200,
minHeight: 760,
backgroundColor: "#0f1117",
icon: path.join(__dirname, "time4midi.png"),
show: false,
webPreferences: {
contextIsolation: true,
sandbox: false,
@@ -19,7 +167,49 @@ function createWindow() {
}
});
win.loadFile(path.join(__dirname, "src", "index.html"));
mainWindow.on("closed", () => {
mainWindow = null;
});
mainWindow.once("ready-to-show", () => {
setTimeout(() => {
closeSplashWindow();
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.show();
}
}, 650);
});
mainWindow.loadFile(path.join(__dirname, "src", "index.html"));
}
function createDiagnosticsWindow() {
if (diagnosticsWindow && !diagnosticsWindow.isDestroyed()) {
diagnosticsWindow.focus();
return diagnosticsWindow;
}
diagnosticsWindow = new BrowserWindow({
width: 1100,
height: 820,
minWidth: 900,
minHeight: 640,
backgroundColor: "#0f1117",
icon: path.join(__dirname, "time4midi.png"),
title: "Time4Midi Diagnose",
webPreferences: {
contextIsolation: true,
sandbox: false,
preload: path.join(__dirname, "preload.js")
}
});
diagnosticsWindow.on("closed", () => {
diagnosticsWindow = null;
});
diagnosticsWindow.loadFile(path.join(__dirname, "src", "diagnostics.html"));
return diagnosticsWindow;
}
ipcMain.handle("project:save", async (_event, payload) => {
@@ -119,17 +309,38 @@ ipcMain.handle("mappingLibrary:pickSaveFile", async (_event, payload) => {
};
});
ipcMain.handle("diagnostics:openWindow", () => {
createDiagnosticsWindow();
return { ok: true };
});
ipcMain.handle("diagnostics:getSnapshot", () => {
return diagnosticsState;
});
ipcMain.on("diagnostics:record", (_event, record) => {
const normalizedRecord = {
...record,
timestamp: Number(record?.timestamp) || Date.now()
};
applyDiagnosticsRecord(normalizedRecord);
sendDiagnosticsUpdate(normalizedRecord);
});
app.whenReady().then(() => {
createSplashWindow();
createWindow();
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) {
if (!mainWindow || mainWindow.isDestroyed()) {
createSplashWindow();
createWindow();
}
});
});
app.on("window-all-closed", () => {
closeSplashWindow();
if (process.platform !== "darwin") {
app.quit();
}