709 lines
24 KiB
JavaScript
709 lines
24 KiB
JavaScript
const { app, BrowserWindow, ipcMain, Tray, Menu, nativeImage } = require("electron");
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const { exec } = require("child_process");
|
|
const { execFile } = require("child_process");
|
|
const { spawn } = require("child_process");
|
|
|
|
// add ArduinoThread module and instance holder
|
|
const ArduinoThread = require("./arduino_thread");
|
|
let arduinoInstance = null;
|
|
|
|
// NEW: persistent python server helpers (globals)
|
|
let pythonServer = null;
|
|
let pythonServerStdoutBuffer = "";
|
|
let pythonServerReady = false;
|
|
|
|
let mainWindow;
|
|
let tray = null;
|
|
let isQuiting = false;
|
|
const tagsFilePath = path.join(__dirname, "config.json");
|
|
|
|
// load a single app/tray icon to reuse (tray.png expected next to index.html/main.js)
|
|
const iconPath = path.join(__dirname, "tray.png");
|
|
let appIcon;
|
|
try {
|
|
if (fs.existsSync(iconPath)) {
|
|
const img = nativeImage.createFromPath(iconPath);
|
|
// prefer a multi-size icon where possible; keep as-is if empty
|
|
appIcon = img && !img.isEmpty() ? img : undefined;
|
|
}
|
|
} catch (e) {
|
|
appIcon = undefined;
|
|
}
|
|
|
|
function readConfig() {
|
|
try {
|
|
if (fs.existsSync(tagsFilePath)) {
|
|
return JSON.parse(fs.readFileSync(tagsFilePath, "utf8"));
|
|
}
|
|
} catch (err) {
|
|
console.error("readConfig error:", err);
|
|
}
|
|
return {};
|
|
}
|
|
|
|
function writeConfig(obj) {
|
|
try {
|
|
fs.writeFileSync(tagsFilePath, JSON.stringify(obj, null, 2));
|
|
} catch (err) {
|
|
console.error("writeConfig error:", err);
|
|
}
|
|
}
|
|
|
|
// helper to run a command and return stdout
|
|
function execPromise(cmd) {
|
|
return new Promise((resolve, reject) => {
|
|
exec(cmd, { encoding: "utf8", windowsHide: true }, (err, stdout, stderr) => {
|
|
if (err) return reject(err);
|
|
resolve(stdout || "");
|
|
});
|
|
});
|
|
}
|
|
|
|
// --- NEW: tray helpers ---
|
|
function createTray() {
|
|
try {
|
|
if (tray) return;
|
|
// use the shared appIcon if available
|
|
tray = new Tray(appIcon || undefined);
|
|
const contextMenu = Menu.buildFromTemplate([
|
|
{
|
|
label: "Show",
|
|
click: () => {
|
|
if (mainWindow) {
|
|
mainWindow.show();
|
|
mainWindow.focus();
|
|
}
|
|
}
|
|
},
|
|
{
|
|
label: "Quit",
|
|
click: () => {
|
|
// user explicitly wants to quit
|
|
isQuiting = true;
|
|
app.quit();
|
|
}
|
|
}
|
|
]);
|
|
tray.setToolTip("Dreckshub");
|
|
tray.setContextMenu(contextMenu);
|
|
tray.on("double-click", () => {
|
|
if (mainWindow) {
|
|
mainWindow.show();
|
|
mainWindow.focus();
|
|
}
|
|
});
|
|
} catch (err) {
|
|
console.error("createTray failed:", err);
|
|
}
|
|
}
|
|
|
|
function destroyTray() {
|
|
try {
|
|
if (tray) {
|
|
tray.destroy();
|
|
tray = null;
|
|
}
|
|
} catch (err) {
|
|
console.error("destroyTray failed:", err);
|
|
}
|
|
}
|
|
|
|
/* Ensure tray state matches config.closeToTray */
|
|
function updateTrayState(enable) {
|
|
if (enable) {
|
|
createTray();
|
|
} else {
|
|
destroyTray();
|
|
}
|
|
}
|
|
|
|
app.whenReady().then(() => {
|
|
// set macOS dock icon early so the app uses tray.png instead of default electron icon
|
|
if (process.platform === "darwin" && appIcon) {
|
|
try { app.dock && app.dock.setIcon && app.dock.setIcon(appIcon); } catch (err) { /* ignore */ }
|
|
}
|
|
|
|
// restore saved window bounds if present
|
|
const cfg = readConfig();
|
|
const wb = cfg.windowBounds || null;
|
|
const winOptions = {
|
|
// start with the typical fixed size used previously
|
|
width: 1466,
|
|
height: 642,
|
|
// prevent user resizing
|
|
resizable: false,
|
|
// set window/taskbar icon (Windows/Linux)
|
|
icon: appIcon || undefined,
|
|
webPreferences: {
|
|
preload: path.join(__dirname, "preload.js"),
|
|
},
|
|
autoHideMenuBar: true,
|
|
};
|
|
|
|
if (wb && typeof wb.width === "number" && typeof wb.height === "number") {
|
|
winOptions.width = wb.width;
|
|
winOptions.height = wb.height;
|
|
if (typeof wb.x === "number" && typeof wb.y === "number") {
|
|
winOptions.x = wb.x;
|
|
winOptions.y = wb.y;
|
|
}
|
|
}
|
|
|
|
mainWindow = new BrowserWindow(winOptions);
|
|
// window is non-resizable; no minimum size call needed
|
|
|
|
mainWindow.loadFile("index.html");
|
|
|
|
// after content loads, apply maximized state if saved
|
|
mainWindow.webContents.once("did-finish-load", () => {
|
|
try {
|
|
if (wb && wb.isMaximized) {
|
|
mainWindow.maximize();
|
|
}
|
|
} catch (err) {
|
|
console.error("apply maximize failed:", err);
|
|
}
|
|
});
|
|
|
|
// create tray if setting enabled in config
|
|
updateTrayState(Boolean(cfg && cfg.closeToTray));
|
|
|
|
// save bounds when window is closing
|
|
mainWindow.on("close", (e) => {
|
|
try {
|
|
// If close-to-tray is enabled and user didn't explicitly choose to quit, hide instead of closing
|
|
const cfgNow = readConfig();
|
|
const useTray = Boolean(cfgNow && cfgNow.closeToTray);
|
|
if (useTray && !isQuiting) {
|
|
e.preventDefault();
|
|
if (mainWindow) mainWindow.hide();
|
|
return;
|
|
}
|
|
|
|
// allow closing — save bounds as before
|
|
// getBounds returns the un-maximized bounds; good for restoring windowed state
|
|
const bounds = mainWindow.getBounds();
|
|
const isMax = mainWindow.isMaximized();
|
|
const curCfg = readConfig();
|
|
curCfg.windowBounds = {
|
|
x: bounds.x,
|
|
y: bounds.y,
|
|
width: bounds.width,
|
|
height: bounds.height,
|
|
isMaximized: isMax,
|
|
};
|
|
writeConfig(curCfg);
|
|
} catch (err) {
|
|
console.error("save window bounds failed:", err);
|
|
}
|
|
});
|
|
|
|
mainWindow.on("closed", () => {
|
|
mainWindow = null;
|
|
});
|
|
});
|
|
|
|
ipcMain.on("save-tags", (event, data) => {
|
|
// merge incoming tag data with existing config so windowBounds (and future fields) are preserved
|
|
try {
|
|
const cfg = readConfig() || {};
|
|
// If data is an array => legacy behavior for slider0
|
|
if (Array.isArray(data)) {
|
|
cfg.slider0 = data;
|
|
} else if (data && typeof data === "object") {
|
|
// copy all keys provided (slider0, slider1, slider2, sliderAssignments, etc.)
|
|
Object.keys(data).forEach((k) => {
|
|
cfg[k] = data[k];
|
|
});
|
|
}
|
|
writeConfig(cfg);
|
|
|
|
// react to closeToTray changes immediately
|
|
try {
|
|
updateTrayState(Boolean(cfg.closeToTray));
|
|
} catch (err) {
|
|
console.error("updateTrayState failed after save-tags:", err);
|
|
}
|
|
} catch (err) {
|
|
console.error("save-tags merge failed:", err);
|
|
}
|
|
});
|
|
|
|
ipcMain.handle("load-tags", () => {
|
|
const cfg = readConfig() || {};
|
|
// ensure defaults for all three sliders for backward compatibility
|
|
if (!Array.isArray(cfg.slider0)) cfg.slider0 = [];
|
|
if (!Array.isArray(cfg.slider1)) cfg.slider1 = [];
|
|
if (!Array.isArray(cfg.slider2)) cfg.slider2 = [];
|
|
return cfg;
|
|
});
|
|
|
|
ipcMain.handle("list-serial-ports", async () => {
|
|
try {
|
|
// Windows: prefer .NET SerialPort.GetPortNames() via PowerShell (reliable, returns COM names)
|
|
if (process.platform === "win32") {
|
|
try {
|
|
const psCmd = `powershell -NoProfile -Command "[System.IO.Ports.SerialPort]::GetPortNames() | ForEach-Object { $_ }"`;
|
|
const out = await execPromise(psCmd);
|
|
const lines = out.split(/\r?\n/).map(l => l.trim()).filter(Boolean);
|
|
if (lines.length > 0) {
|
|
// Ensure unique, return objects with label=value
|
|
const uniq = Array.from(new Set(lines));
|
|
return uniq.map(p => ({ value: p, label: p }));
|
|
}
|
|
|
|
// Fallback 1: Get-PnpDevice friendly names containing (COMx)
|
|
const psFallback = `powershell -NoProfile -Command "Get-PnpDevice -Status OK | Where-Object { $_.FriendlyName -match '\\(COM\\d+\\)' -or $_.Name -match '\\(COM\\d+\\)' } | ForEach-Object { ($_.FriendlyName -or $_.Name) }"`;
|
|
const out2 = await execPromise(psFallback);
|
|
const lines2 = out2.split(/\r?\n/).map(l => l.trim()).filter(Boolean);
|
|
const ports = [];
|
|
const re = /(COM\d+)/i;
|
|
for (const l of lines2) {
|
|
const m = l.match(re);
|
|
if (m) ports.push({ value: m[1], label: l });
|
|
}
|
|
if (ports.length > 0) {
|
|
// dedupe by value
|
|
const seen = new Set();
|
|
return ports.filter(p => {
|
|
if (seen.has(p.value)) return false;
|
|
seen.add(p.value);
|
|
return true;
|
|
});
|
|
}
|
|
|
|
// Fallback 2: 'mode' command parsing
|
|
const out3 = await execPromise("mode");
|
|
const lines3 = out3.split(/\r?\n/);
|
|
const reMode = /(COM\d+)/gi;
|
|
const portsMode = [];
|
|
for (const l of lines3) {
|
|
const m = l.match(reMode);
|
|
if (m) {
|
|
for (const token of m) portsMode.push({ value: token, label: token });
|
|
}
|
|
}
|
|
return portsMode;
|
|
} catch (err) {
|
|
console.error("list-serial-ports (win) primary/fallback failed:", err);
|
|
return [];
|
|
}
|
|
} else {
|
|
// macOS / Linux: scan common device files
|
|
try {
|
|
const out = await execPromise("ls /dev/tty.* /dev/ttyUSB* /dev/ttyACM* 2>/dev/null || true");
|
|
const lines = out.split(/\r?\n/).map(l => l.trim()).filter(Boolean);
|
|
return lines.map(p => ({ value: p, label: p }));
|
|
} catch (err) {
|
|
console.error("list-serial-ports (unix) failed:", err);
|
|
return [];
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error("list-serial-ports failed:", err);
|
|
return [];
|
|
}
|
|
});
|
|
|
|
// --- new IPC handlers to start/stop and forward serial updates ---
|
|
// Ensure we don't double-register handlers if the file is reloaded in dev reloads:
|
|
if (ipcMain.removeHandler) {
|
|
try { ipcMain.removeHandler("connect-com"); } catch (e) { /* ignore */ }
|
|
}
|
|
ipcMain.handle("connect-com", async (event, { port, baudRate = 9600 }) => {
|
|
try {
|
|
// if already connected, return info
|
|
if (arduinoInstance) {
|
|
return { ok: false, message: "Already connected" };
|
|
}
|
|
if (!port) return { ok: false, message: "No port specified" };
|
|
|
|
// create and wire instance (verbose disabled to avoid noisy slider logs)
|
|
arduinoInstance = new ArduinoThread(port, Number(baudRate) || 9600, 3, "config.json", false);
|
|
|
|
// Keep last normalized value per slider so we only act when the slider actually changed.
|
|
// Index = slider number (0..2). null means "unknown / not initialized".
|
|
let _lastSliderNormalized = [null, null, null];
|
|
const _minSliderDeltaForChange = 0.01; // 1% change required to be considered different
|
|
|
|
arduinoInstance.on("update", (values) => {
|
|
// forward to renderer
|
|
try {
|
|
if (mainWindow && mainWindow.webContents) {
|
|
mainWindow.webContents.send("serial-update", values);
|
|
}
|
|
} catch (e) { /* ignore */ }
|
|
|
|
// Also ensure the Python helper is invoked for each slider value.
|
|
// This is a fallback in case the ArduinoThread did not emit 'set-volume'.
|
|
try {
|
|
const cfg = readConfig() || {};
|
|
// values expected as integers (0..1023)
|
|
if (Array.isArray(values)) {
|
|
// ensure we have a session cache and refresher running
|
|
startSessionRefresher();
|
|
values.forEach((raw, idx) => {
|
|
const num = Number(raw);
|
|
if (!Number.isFinite(num)) return;
|
|
let normalized = Math.max(0, Math.min(1, num / 1023));
|
|
// snap near-zero to exact 0 to avoid tiny audible residuals
|
|
if (normalized < 0.02) normalized = 0;
|
|
|
|
// only continue if the slider value changed enough since last time
|
|
const prev = _lastSliderNormalized[idx];
|
|
const changed = prev === null || Math.abs(prev - normalized) > _minSliderDeltaForChange;
|
|
if (!changed) return; // skip unchanged slider
|
|
_lastSliderNormalized[idx] = normalized;
|
|
|
|
const key = `slider${idx}`;
|
|
const procs = Array.isArray(cfg[key]) ? cfg[key] : [];
|
|
if (!procs || procs.length === 0) return;
|
|
|
|
// match configured process names against running sessions (case-insensitive)
|
|
procs.forEach((proc) => {
|
|
if (!proc) return;
|
|
const procLow = String(proc).toLowerCase();
|
|
const matched = _runningSessions.some(s => {
|
|
return s === procLow || s.includes(procLow) || procLow.includes(s);
|
|
});
|
|
if (matched) {
|
|
// enqueue only when matched; DO NOT produce per-update verbose logs
|
|
runPythonSetVolume(proc, normalized, idx);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
} catch (err) {
|
|
console.error("update handler fallback failed:", err);
|
|
sendVolumeLog(`update handler fallback failed: ${String(err)}`, true);
|
|
}
|
|
});
|
|
arduinoInstance.on("raw", (raw) => {
|
|
try { if (mainWindow && mainWindow.webContents) mainWindow.webContents.send("serial-raw", raw); } catch (e) {}
|
|
});
|
|
arduinoInstance.on("error", (err) => {
|
|
try { if (mainWindow && mainWindow.webContents) mainWindow.webContents.send("serial-error", String(err)); } catch (e) {}
|
|
});
|
|
arduinoInstance.on("warn", (w) => {
|
|
try { if (mainWindow && mainWindow.webContents) mainWindow.webContents.send("serial-warn", w); } catch (e) {}
|
|
});
|
|
|
|
// --- new helper to call the python volume setter and emit logs to renderer ---
|
|
// send only concise important logs to renderer to avoid flooding DevTools
|
|
const _verboseVolumeLogs = false;
|
|
function sendVolumeLog(msg, force = false) {
|
|
try {
|
|
// keep main-process console for troubleshooting but avoid massive logs
|
|
if (_verboseVolumeLogs || force || /\b(error|failed|succeed|succeeded)\b/i.test(String(msg))) {
|
|
console.log(msg);
|
|
try { if (mainWindow && mainWindow.webContents) mainWindow.webContents.send("volume-log", String(msg)); } catch (e) {}
|
|
}
|
|
} catch (e) { /* ignore */ }
|
|
}
|
|
|
|
// cached running audio sessions (lowercase strings)
|
|
let _runningSessions = [];
|
|
let _sessionRefreshTimer = null;
|
|
const _sessionRefreshIntervalMs = 5000; // less frequent to avoid many spawns
|
|
|
|
// ask set_volume.py to list sessions (tries several invocation patterns)
|
|
async function refreshRunningSessions() {
|
|
const script = path.join(__dirname, "set_volume.py");
|
|
// Try invocation patterns; prefer "python <script>" as you run manually
|
|
const attempts = process.platform === "win32"
|
|
? [ ["python", script, "--list"], ["python3", script, "--list"], ["py", "-3", script, "--list"], ["py", script, "--list"] ]
|
|
: [ ["python3", script, "--list"], ["python", script, "--list"] ];
|
|
|
|
for (const att of attempts) {
|
|
const exe = att[0];
|
|
const args = att.slice(1);
|
|
try {
|
|
// removed verbose per-attempt logging to avoid noise
|
|
const out = await new Promise((resolve, reject) => {
|
|
const child = execFile(exe, args, { windowsHide: true }, (err, stdout, stderr) => {
|
|
if (err) return reject({ err, stderr });
|
|
resolve((stdout || "").toString());
|
|
});
|
|
child.on("error", (e) => reject({ err: e, stderr: "" }));
|
|
});
|
|
const lines = out.split(/\r?\n/).map(l => l.trim()).filter(Boolean);
|
|
_runningSessions = lines.map(l => l.toLowerCase());
|
|
// previously sent a refreshRunningSessions: found ... log here — removed to reduce noise
|
|
return;
|
|
} catch (e) {
|
|
// intentionally quiet on per-attempt failures; continue trying others
|
|
}
|
|
}
|
|
// all attempts failed — keep previous cache. Do not spam logs; only keep a very concise console hint.
|
|
// (If you want visible errors for troubleshooting, reintroduce a targeted sendVolumeLog or console.warn here.)
|
|
// sendVolumeLog("refreshRunningSessions: all interpreter attempts failed. Install Python or ensure 'python'/'py' is on PATH.");
|
|
}
|
|
|
|
function startSessionRefresher() {
|
|
if (_sessionRefreshTimer) return;
|
|
// initial immediate refresh
|
|
refreshRunningSessions();
|
|
_sessionRefreshTimer = setInterval(refreshRunningSessions, _sessionRefreshIntervalMs);
|
|
}
|
|
|
|
function stopSessionRefresher() {
|
|
if (_sessionRefreshTimer) {
|
|
clearInterval(_sessionRefreshTimer);
|
|
_sessionRefreshTimer = null;
|
|
}
|
|
}
|
|
|
|
// Queue + rate-limit for invoking set_volume.py to avoid process storming
|
|
const _volumeCallQueue = [];
|
|
let _volumeActive = 0;
|
|
const _volumeMaxConcurrent = 2; // small concurrency to avoid overload
|
|
const _perProcLast = {}; // procName -> { lastValue, lastTimeMillis }
|
|
const _minValueDelta = 0.01; // only call if value changed by > 1%
|
|
const _minIntervalMs = 0; // at least 200ms between calls per proc
|
|
|
|
function enqueueVolumeCall(procName, value, slider) {
|
|
if (!procName) return;
|
|
const now = Date.now();
|
|
const key = String(procName).toLowerCase();
|
|
const prev = _perProcLast[key] || { lastValue: null, lastTime: 0 };
|
|
const deltaOk = prev.lastValue === null || Math.abs(prev.lastValue - value) > _minValueDelta;
|
|
const timeOk = (now - prev.lastTime) >= _minIntervalMs;
|
|
// Only enqueue if value significantly changed or interval passed
|
|
if (!deltaOk && !timeOk) {
|
|
// skip noisy updates
|
|
return;
|
|
}
|
|
// optimistic update to prevent duplicate rapid enqueues
|
|
_perProcLast[key] = { lastValue: value, lastTime: now };
|
|
|
|
// dedupe: if a queued entry for same proc exists, replace its value with the newest
|
|
const existingIndex = _volumeCallQueue.findIndex(it => String(it.procName).toLowerCase() === key);
|
|
if (existingIndex >= 0) {
|
|
_volumeCallQueue[existingIndex].value = Number(value);
|
|
_volumeCallQueue[existingIndex].slider = slider || null;
|
|
} else {
|
|
_volumeCallQueue.push({ procName: String(procName), value: Number(value), slider: slider || null });
|
|
}
|
|
|
|
// cap queue size to avoid runaway growth
|
|
const maxQueue = 800;
|
|
if (_volumeCallQueue.length > maxQueue) {
|
|
_volumeCallQueue.splice(0, _volumeCallQueue.length - maxQueue);
|
|
}
|
|
|
|
processVolumeQueue();
|
|
}
|
|
|
|
// Replace processVolumeQueue() internals to prefer persistent python server
|
|
function processVolumeQueue() {
|
|
if (_volumeActive >= _volumeMaxConcurrent) return;
|
|
if (_volumeCallQueue.length === 0) return;
|
|
|
|
// Attempt to send to persistent python server first
|
|
(async () => {
|
|
const item = _volumeCallQueue.shift();
|
|
_volumeActive++;
|
|
|
|
// Ensure server is running (best-effort)
|
|
const haveServer = await ensurePythonServer();
|
|
if (haveServer && pythonServer && pythonServer.stdin && !pythonServer.killed) {
|
|
try {
|
|
const payload = JSON.stringify({ cmd: "set", process: item.procName, value: Number(item.value) }) + "\n";
|
|
pythonServer.stdin.write(payload, "utf8", (err) => {
|
|
if (err) {
|
|
console.error("pythonServer write error:", err);
|
|
}
|
|
});
|
|
// we assume the Python server will apply the change quickly; log concisely
|
|
sendVolumeLog(`sent to python-server: ${item.procName}=${item.value}`, false);
|
|
} catch (err) {
|
|
// fall back to spawning a process if write fails
|
|
sendVolumeLog(`python-server write failed, falling back: ${err}`, true);
|
|
// fallback to per-call attempts (existing execFile logic)
|
|
trySpawnFallback(item).catch(e => {
|
|
sendVolumeLog(`fallback spawn failed: ${e}`, true);
|
|
});
|
|
} finally {
|
|
_volumeActive = Math.max(0, _volumeActive - 1);
|
|
setImmediate(processVolumeQueue);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// If no server, use original spawn attempts (keeps previous behavior)
|
|
try {
|
|
await trySpawnFallback(item);
|
|
} catch (e) {
|
|
sendVolumeLog(`All interpreter attempts failed for '${item.procName}'.`, true);
|
|
} finally {
|
|
_volumeActive = Math.max(0, _volumeActive - 1);
|
|
setImmediate(processVolumeQueue);
|
|
}
|
|
})();
|
|
}
|
|
|
|
// Try to start a persistent python server (tries several interpreters).
|
|
function ensurePythonServer() {
|
|
if (pythonServer && !pythonServer.killed) return Promise.resolve(true);
|
|
const script = path.join(__dirname, "set_volume.py");
|
|
const interpreters = process.platform === "win32"
|
|
? ["python", "python3", "py"]
|
|
: ["python3", "python"];
|
|
let lastErr = null;
|
|
|
|
return new Promise((resolve) => {
|
|
(function tryInterp(i) {
|
|
if (i >= interpreters.length) {
|
|
console.error("Could not start python server:", lastErr);
|
|
pythonServer = null;
|
|
pythonServerReady = false;
|
|
return resolve(false);
|
|
}
|
|
const exe = interpreters[i];
|
|
try {
|
|
const child = spawn(exe, [script, "--server-stdin"], { stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
|
|
let started = false;
|
|
child.on("error", (err) => {
|
|
lastErr = err;
|
|
// try next interpreter
|
|
tryInterp(i + 1);
|
|
});
|
|
child.stdout.on("data", (chunk) => {
|
|
// accumulate or just log; we don't need to parse every response here
|
|
try {
|
|
const s = chunk.toString();
|
|
pythonServerStdoutBuffer += s;
|
|
// optionally parse complete lines if needed
|
|
} catch (e) { /* ignore */ }
|
|
});
|
|
child.stderr.on("data", (chunk) => {
|
|
console.error("python server stderr:", chunk.toString());
|
|
});
|
|
child.on("close", (code) => {
|
|
pythonServer = null;
|
|
pythonServerReady = false;
|
|
console.warn("python server closed, code=", code);
|
|
});
|
|
// treat as started
|
|
pythonServer = child;
|
|
pythonServerReady = true;
|
|
return resolve(true);
|
|
} catch (e) {
|
|
lastErr = e;
|
|
tryInterp(i + 1);
|
|
}
|
|
})(0);
|
|
});
|
|
}
|
|
|
|
// helper: existing per-call spawn attempts factored out so we can reuse it as fallback
|
|
async function trySpawnFallback(item) {
|
|
const script = path.join(__dirname, "set_volume.py");
|
|
const attempts = process.platform === "win32"
|
|
? [ ["python", script, String(item.procName), String(item.value)], ["python3", script, String(item.procName), String(item.value)], ["py", "-3", script, String(item.procName), String(item.value)], ["py", script, String(item.procName), String(item.value)] ]
|
|
: [ ["python3", script, String(item.procName), String(item.value)], ["python", script, String(item.procName), String(item.value)] ];
|
|
|
|
for (const att of attempts) {
|
|
const exe = att[0];
|
|
const args = att.slice(1);
|
|
try {
|
|
sendVolumeLog(`exec ${exe} ${args.join(" ")} -> ${item.procName} value=${item.value}`, false);
|
|
await new Promise((resolve, reject) => {
|
|
const child = execFile(exe, args, { windowsHide: true }, (err, stdout, stderr) => {
|
|
if (err) {
|
|
sendVolumeLog(`${exe} error: ${err.message}. stderr: ${stderr ? stderr.trim() : ""}`, true);
|
|
return reject(err);
|
|
}
|
|
return resolve();
|
|
});
|
|
child.on("error", (spawnErr) => {
|
|
sendVolumeLog(`${exe} spawn error: ${spawnErr.message}`, true);
|
|
reject(spawnErr);
|
|
});
|
|
});
|
|
sendVolumeLog(`set_volume succeeded for '${item.procName}'`, true);
|
|
return;
|
|
} catch (e) {
|
|
sendVolumeLog(`${exe} failed for '${item.procName}': ${String(e)}`, true);
|
|
// try next
|
|
}
|
|
}
|
|
throw new Error("All attempts failed");
|
|
}
|
|
|
|
// expose a simpler API used by other handlers
|
|
function runPythonSetVolume(procName, value, slider) {
|
|
enqueueVolumeCall(procName, value, slider);
|
|
}
|
|
|
|
// start session refresher once (do not start it repeatedly per update)
|
|
startSessionRefresher();
|
|
|
|
// throttle serial updates processed (avoid queueing on every single sample)
|
|
let _lastUpdateAt = 0;
|
|
const _updateThrottleMs = 120; // process ~16 updates/sec
|
|
|
|
// add listener to actually apply per-process volume changes via Python helper
|
|
arduinoInstance.on("set-volume", (info) => {
|
|
try {
|
|
// info: { process: procName, value: normalizedFloat, slider: sliderNumber }
|
|
const procName = info && info.process ? info.process : "";
|
|
const value = (typeof info.value === "number") ? info.value : 0;
|
|
const slider = typeof info.slider !== "undefined" ? info.slider : null;
|
|
if (!procName) {
|
|
sendVolumeLog(`set-volume received but no process name in info: ${JSON.stringify(info)}`);
|
|
return;
|
|
}
|
|
sendVolumeLog(`set-volume received -> process="${procName}" value=${value} slider=${slider}`);
|
|
// call Python helper (now queued/rate-limited)
|
|
runPythonSetVolume(procName, value, slider);
|
|
} catch (err) {
|
|
console.error("set-volume handler failed:", err);
|
|
sendVolumeLog(`set-volume handler failed: ${String(err)}`);
|
|
}
|
|
});
|
|
|
|
// no explicit start() needed — ArduinoThread constructor wires port and events
|
|
return { ok: true };
|
|
} catch (err) {
|
|
console.error("connect-com failed:", err);
|
|
arduinoInstance = null;
|
|
return { ok: false, message: String(err) };
|
|
}
|
|
});
|
|
// before registering disconnect, remove any previous handler
|
|
if (ipcMain.removeHandler) {
|
|
try { ipcMain.removeHandler("disconnect-com"); } catch (e) { /* ignore */ }
|
|
}
|
|
ipcMain.handle("disconnect-com", async () => {
|
|
try {
|
|
if (!arduinoInstance) return { ok: false, message: "Not connected" };
|
|
// stop session refresher if running
|
|
try { stopSessionRefresher(); } catch (e) { /* ignore */ }
|
|
try { arduinoInstance.stop(); } catch (e) { /* ignore */ }
|
|
arduinoInstance = null;
|
|
return { ok: true };
|
|
} catch (err) {
|
|
console.error("disconnect-com failed:", err);
|
|
return { ok: false, message: String(err) };
|
|
}
|
|
});
|
|
|
|
app.on("window-all-closed", () => {
|
|
// destroy tray if present
|
|
try { destroyTray(); } catch (e) { /* ignore */ }
|
|
|
|
if (process.platform !== "darwin") {
|
|
app.quit();
|
|
}
|
|
|
|
// destroy tray if present
|
|
try { destroyTray(); } catch (e) { /* ignore */ }
|
|
|
|
if (process.platform !== "darwin") {
|
|
app.quit();
|
|
}
|
|
});
|