/* Open Battery Information — Electron main process. Loads the OBI-1 page and provides native serial access through the `serialport` package (no browser Web Serial needed). The renderer talks to the serial port over IPC via the preload bridge and a small navigator.serial shim. Everything stays local; no telemetry. */ const { app, BrowserWindow, Menu, ipcMain, shell } = require('electron'); const path = require('path'); process.on('uncaughtException', (err) => { console.error('[main] uncaughtException:', err); }); process.on('unhandledRejection', (reason) => { console.error('[main] unhandledRejection:', reason); }); let SerialPort = null; try { ({ SerialPort } = require('serialport')); console.log('[main] serialport loaded'); } catch (err) { console.error('[main] serialport failed to load:', err.message); } const SITE_DIR = path.join(__dirname, 'site'); const openPorts = new Map(); // port path -> { port, sender } let pickerPromise = null; /* ---------- serialport helpers ---------- */ function toPortInfo(p) { return { path: p.path, vendorId: p.vendorId ? parseInt(p.vendorId, 16) : 0, productId: p.productId ? parseInt(p.productId, 16) : 0, manufacturer: p.manufacturer || '', serialNumber: p.serialNumber || '', }; } function openPort(info, baudRate) { return new Promise((resolve, reject) => { const port = new SerialPort( { path: info.path, baudRate: baudRate || 9600 }, (err) => (err ? reject(err) : resolve(port)) ); port.on('error', () => {}); }); } /* ---------- IPC: serial ---------- */ ipcMain.handle('serial:list', async () => { const list = await SerialPort.list(); return list.map(toPortInfo); }); ipcMain.handle('serial:requestPort', async (event) => { const win = BrowserWindow.fromWebContents(event.sender); const info = await showPortPicker(win); if (!info) throw new Error('NotFoundError: No port selected'); return info; }); ipcMain.handle('serial:open', async (event, info, baudRate) => { const existing = openPorts.get(info.path); if (existing && existing.port.isOpen) return { ok: true }; const port = await openPort(info, baudRate); port.on('data', (buf) => { const entry = openPorts.get(info.path); if (entry && entry.sender && !entry.sender.isDestroyed()) { entry.sender.send('serial:data', info.path, buf); } }); port.on('close', () => openPorts.delete(info.path)); openPorts.set(info.path, { port, sender: event.sender }); return { ok: true }; }); ipcMain.handle('serial:setSignals', async (_event, info, dtr) => { const entry = openPorts.get(info.path); if (!entry) throw new Error('Serial port is not open'); await new Promise((resolve, reject) => entry.port.set({ dtr: !!dtr }, (err) => (err ? reject(err) : resolve())) ); return { ok: true }; }); ipcMain.handle('serial:write', async (_event, info, data) => { const entry = openPorts.get(info.path); if (!entry) throw new Error('Serial port is not open'); await new Promise((resolve, reject) => entry.port.write(Buffer.from(data), (err) => (err ? reject(err) : resolve())) ); return { ok: true }; }); ipcMain.handle('serial:close', async (_event, info) => { const entry = openPorts.get(info.path); if (!entry) return { ok: true }; openPorts.delete(info.path); await new Promise((resolve) => entry.port.close(() => resolve())); return { ok: true }; }); /* ---------- serial port picker ---------- */ const escapeHtml = (s) => String(s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]) ); const PICKER_HTML = `
Choose the Arduino running the OBI firmware.