From 98832cd76583dbda6b0939eaff8260ae4c2ad62b Mon Sep 17 00:00:00 2001 From: Nico Date: Wed, 19 Nov 2025 23:13:06 +0100 Subject: [PATCH] Adjusting Project files for branch --- .gitignore | 3 +- arduino_thread.js | 170 -- index.html | 543 ------ main.js | 708 -------- package-lock.json | 4278 --------------------------------------------- package.json | 63 - preload.js | 27 - renderer.js | 814 --------- styles.css | 1221 ------------- ui.py | 189 ++ 10 files changed, 191 insertions(+), 7825 deletions(-) delete mode 100644 arduino_thread.js delete mode 100644 index.html delete mode 100644 main.js delete mode 100644 package-lock.json delete mode 100644 package.json delete mode 100644 preload.js delete mode 100644 renderer.js delete mode 100644 styles.css create mode 100644 ui.py diff --git a/.gitignore b/.gitignore index 42a7f05..de5c299 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ #Folders /node_modules/ +/.venv/ #Files -/config.json \ No newline at end of file +/config.json diff --git a/arduino_thread.js b/arduino_thread.js deleted file mode 100644 index b406ed6..0000000 --- a/arduino_thread.js +++ /dev/null @@ -1,170 +0,0 @@ -const { SerialPort } = require('serialport'); -const fs = require('fs'); -const EventEmitter = require('events'); -// add parser import -const { ReadlineParser } = require('@serialport/parser-readline'); - -class ArduinoThread extends EventEmitter { - constructor(comPort, baudRate = 9600, sliderCount = 3, yamlPath = 'config.json', verbose = false) { - super(); - - if (typeof comPort !== 'string') throw new Error('comPort must be a string'); - if (typeof baudRate !== 'number') throw new Error('baudRate must be a number'); - if (typeof sliderCount !== 'number') throw new Error('sliderCount must be a number'); - - this.port = new SerialPort({ path: comPort, baudRate, autoOpen: true }); - this.sliderCount = sliderCount; - this.yamlPath = yamlPath; // now points to config.json by default - this.verbose = !!verbose; - - this.sendBuffer = []; - this.updateList = Array(sliderCount).fill(0); - this.isRunning = true; - this.pendingMessage = null; - - // pipe through a newline-based parser so we always get full lines - try { - this.parser = this.port.pipe(new ReadlineParser({ delimiter: '\n' })); - this.parser.on('data', (line) => this._handleData(line)); - } catch (err) { - // fallback to raw data event if parser unavailable - this.port.on('data', (data) => this._handleData(data)); - } - - // keep error/close handlers on the port - this.port.on('error', (err) => { - this.emit('error', err); - if (this.verbose) console.error('Serial error:', err); - }); - this.port.on('close', () => { - this.isRunning = false; - if (this.verbose) console.log('Serial port closed'); - }); - } - - _handleData(data) { - // accept string or Buffer; trim newline/whitespace - const input = (typeof data === 'string') ? data.trim() : data.toString().trim(); - - // acknowledgement handling - if (input === 'OK' && this.pendingMessage) { - if (this.verbose) console.log(`Message confirmed: ${this.pendingMessage}`); - this.pendingMessage = null; - // drain buffer if present - if (this.sendBuffer.length > 0) { - const next = this.sendBuffer.shift(); - this._writeMessage(next); - } - return; - } - - const numbers = input.split('|'); - if (numbers.length === this.sliderCount) { - try { - const values = numbers.map((n) => parseInt(n, 10)); - values.forEach((val, i) => { - if (!Number.isFinite(val)) throw new Error('parse error'); - if (val !== this.updateList[i]) { - this._setVolume(val, i); - } - }); - this.updateList = values; - this.emit('update', values); // notify listeners (GUI) - if (this.verbose) console.log('Slider update ->', values); - } catch (err) { - this.emit('warn', { msg: 'Invalid slider data', raw: input }); - if (this.verbose) console.warn('Invalid slider data:', input); - } - } else { - // Not an update; emit for consumers - this.emit('raw', input); - if (this.verbose) console.warn('Unexpected input:', input); - } - } - - send(message) { - if (!message || typeof message !== 'string') return; - if (this.pendingMessage) { - // queue if another message is awaiting ACK - this.sendBuffer.push(message); - return; - } - this._writeMessage(message); - } - - _writeMessage(message) { - if (!this.port || !this.port.writable) { - const err = new Error('Serial port not writable'); - this.emit('error', err); - if (this.verbose) console.error(err); - return; - } - - this.pendingMessage = message; - this.port.write(message + '\n', (err) => { - if (err) { - this.emit('error', err); - if (this.verbose) console.error('Error writing:', err.message); - this.pendingMessage = null; - } else if (this.verbose) { - console.log('Wrote message:', message); - } - }); - - // fallback timeout if device never replies OK - setTimeout(() => { - if (this.pendingMessage) { - this.emit('warn', { msg: "Did not receive OK from device", message: this.pendingMessage }); - if (this.verbose) console.warn('Did not receive OK from device for', this.pendingMessage); - this.pendingMessage = null; - // try to drain next queued message if any - if (this.sendBuffer.length > 0) { - const next = this.sendBuffer.shift(); - this._writeMessage(next); - } - } - }, 100); // adjustable timeout - } - - _setVolume(value, sliderNumber) { - const normalized = value / 1023; - - if (!fs.existsSync(this.yamlPath)) { - this.emit('warn', { msg: 'Config file missing', path: this.yamlPath }); - if (this.verbose) console.warn('Config file missing:', this.yamlPath); - return; - } - - try { - const file = fs.readFileSync(this.yamlPath, 'utf8'); - const config = JSON.parse(file || '{}'); // parse JSON config.json - const processes = config[sliderNumber] || []; - - processes.forEach((proc) => { - // Emit an event so the application can perform the platform-specific change. - this.emit('set-volume', { process: proc, value: normalized, slider: sliderNumber }); - if (this.verbose) { - console.log(`Would set volume for "${proc}" to ${normalized.toFixed(2)} (slider ${sliderNumber})`); - } - }); - } catch (err) { - this.emit('error', err); - if (this.verbose) console.error('Config JSON error:', err); - } - } - - stop() { - this.isRunning = false; - try { - if (this.port && this.port.isOpen) this.port.close(); - } catch (e) { - if (this.verbose) console.warn('Error closing port', e); - } - } - - getValues() { - return this.updateList.slice(); - } -} - -module.exports = ArduinoThread; diff --git a/index.html b/index.html deleted file mode 100644 index 89ec91a..0000000 --- a/index.html +++ /dev/null @@ -1,543 +0,0 @@ - - - - - - Dreckshub - - - - - -
- - -
-
- -
-

Makropad — Main

-

Content for Makropad on the first tab.

-
- - - -
- -
- -
-
-
-
- - - -
- -
-
-
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
-
-
- - - -
- -
-
- -
-
-
-
- -
-
-
- - - -
- -
-
- -
-
-
- -
-
-
- - - -
- -
-
- -
-
-
- -
-
-
- - - -
-
-
-
-
-
- - - -
-
- - - - - - - diff --git a/main.js b/main.js deleted file mode 100644 index 4bb34cc..0000000 --- a/main.js +++ /dev/null @@ -1,708 +0,0 @@ -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