You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
608 lines
20 KiB
608 lines
20 KiB
const { app, BrowserWindow, clipboard, ipcMain, Menu, shell } = require('electron') |
|
const { execFile } = require('child_process') |
|
const path = require('path') |
|
const fs = require('fs') |
|
const { SensorController } = require('./src/business/sensor-controller') |
|
const { UsbSerialBridge } = require('./src/business/usb-serial-bridge') |
|
const pkg = require('./package.json') |
|
|
|
if (require('electron-squirrel-startup')) app.quit() |
|
|
|
const cacheDir = app.getPath('userData') |
|
const cacheFile = path.join(cacheDir, 'app-cache.json') |
|
const fileRoot = path.join(cacheDir, 'files') |
|
|
|
let mainWindow = null |
|
let usbBridge = null |
|
let sensorController = null |
|
let bluetoothSelectWindow = null |
|
const topBarState = { |
|
route: '', |
|
network: null, |
|
pwm: false, |
|
targetUrl: '' |
|
} |
|
const pwmPorts = new Set() |
|
|
|
function ensureDir(dir) { |
|
try { |
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }) |
|
} catch {} |
|
} |
|
|
|
function readCache() { |
|
try { |
|
if (fs.existsSync(cacheFile)) return JSON.parse(fs.readFileSync(cacheFile, 'utf-8')) |
|
} catch {} |
|
return {} |
|
} |
|
|
|
function writeCache(data) { |
|
try { |
|
ensureDir(cacheDir) |
|
fs.writeFileSync(cacheFile, JSON.stringify(data, null, 2), 'utf-8') |
|
} catch {} |
|
} |
|
|
|
function resolveAppFile(inputPath) { |
|
const raw = String(inputPath || '').trim() |
|
if (!raw) return fileRoot |
|
if (path.isAbsolute(raw) && /^[a-zA-Z]:[\\/]/.test(raw)) return raw |
|
const relative = raw.replace(/^[/\\]+/, '') |
|
return path.join(fileRoot, relative) |
|
} |
|
|
|
function fileList(dir) { |
|
try { |
|
const target = resolveAppFile(dir) |
|
if (!fs.existsSync(target)) return '' |
|
const stat = fs.statSync(target) |
|
if (stat.isFile()) return target |
|
return fs.readdirSync(target).map(name => path.join(target, name)).join('\n') |
|
} catch { |
|
return '' |
|
} |
|
} |
|
|
|
function emit(channel, ...args) { |
|
if (mainWindow && !mainWindow.isDestroyed()) { |
|
mainWindow.webContents.send(channel, ...args) |
|
} |
|
} |
|
|
|
function parseBluetoothDevices(raw) { |
|
const lines = String(raw || '').split(/\r?\n/).map(line => line.trim()).filter(Boolean) |
|
const devices = [] |
|
for (const line of lines) { |
|
const parts = line.split('\t') |
|
if (parts.length < 2) continue |
|
const [name, address = '', status = ''] = parts |
|
if (!name || name === 'Name') continue |
|
devices.push({ name, address, status }) |
|
} |
|
return devices |
|
} |
|
|
|
function listBluetoothDevices() { |
|
const script = [ |
|
'$ErrorActionPreference = "SilentlyContinue"', |
|
'Get-PnpDevice -Class Bluetooth |', |
|
'Where-Object { $_.FriendlyName -and $_.InstanceId } |', |
|
'ForEach-Object {', |
|
' $id = $_.InstanceId', |
|
' $addr = ""', |
|
' if ($id -match "DEV_([0-9A-Fa-f]{12})") {', |
|
' $raw = $Matches[1].ToUpper()', |
|
' $addr = (($raw -split "(.{2})" | Where-Object { $_ }) -join ":")', |
|
' }', |
|
' "$($_.FriendlyName)`t$addr`t$($_.Status)"', |
|
'}' |
|
].join('\n') |
|
return new Promise(resolve => { |
|
execFile('powershell.exe', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', script], { windowsHide: true }, (err, stdout) => { |
|
if (err) { |
|
resolve([]) |
|
return |
|
} |
|
resolve(parseBluetoothDevices(stdout)) |
|
}) |
|
}) |
|
} |
|
|
|
function bluetoothSelectHtml(namePrefix) { |
|
const prefix = String(namePrefix || '') |
|
return `<!doctype html> |
|
<html> |
|
<head> |
|
<meta charset="utf-8"> |
|
<title>蓝牙连接</title> |
|
<style> |
|
body{margin:0;font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;background:#f4f7fb;color:#1f2937} |
|
.bar{height:48px;display:flex;align-items:center;justify-content:space-between;padding:0 16px;background:#fff;border-bottom:1px solid #d8e0eb} |
|
.title{font-size:16px;font-weight:600} |
|
.actions{display:flex;gap:8px} |
|
button{border:1px solid #b9c7d8;background:#fff;color:#1f3a5f;border-radius:6px;padding:7px 10px;cursor:pointer} |
|
button.primary{background:#1677ff;border-color:#1677ff;color:#fff} |
|
.content{padding:12px} |
|
.hint{font-size:12px;color:#667085;margin-bottom:10px} |
|
.list{display:flex;flex-direction:column;gap:8px} |
|
.item{background:#fff;border:1px solid #d8e0eb;border-radius:8px;padding:10px 12px;display:flex;justify-content:space-between;gap:12px;align-items:center} |
|
.name{font-size:14px;font-weight:600} |
|
.meta{font-size:12px;color:#667085;margin-top:4px} |
|
.empty{padding:28px 12px;text-align:center;color:#667085;background:#fff;border:1px dashed #c8d2df;border-radius:8px} |
|
</style> |
|
</head> |
|
<body> |
|
<div class="bar"> |
|
<div class="title">蓝牙连接</div> |
|
<div class="actions"> |
|
<button id="settings">系统蓝牙设置</button> |
|
<button id="refresh" class="primary">刷新</button> |
|
</div> |
|
</div> |
|
<div class="content"> |
|
<div class="hint">选择已配对蓝牙设备${prefix ? `,过滤前缀:${prefix}` : ''}</div> |
|
<div id="list" class="list"><div class="empty">正在扫描已配对设备...</div></div> |
|
</div> |
|
<script> |
|
const prefix = ${JSON.stringify(prefix)} |
|
const list = document.getElementById('list') |
|
function render(devices) { |
|
const filtered = prefix ? devices.filter(d => String(d.name || '').startsWith(prefix)) : devices |
|
if (!filtered.length) { |
|
list.innerHTML = '<div class="empty">未找到已配对设备。请先在 Windows 蓝牙设置中配对设备,然后刷新。</div>' |
|
return |
|
} |
|
list.innerHTML = filtered.map((d, i) => '<div class="item"><div><div class="name">' + escapeHtml(d.name || '未知设备') + '</div><div class="meta">' + escapeHtml(d.address || d.status || '') + '</div></div><button class="primary" data-index="' + i + '">选择</button></div>').join('') |
|
Array.from(list.querySelectorAll('button[data-index]')).forEach(btn => { |
|
btn.onclick = () => window.bluetoothSelect.select(filtered[Number(btn.dataset.index)]) |
|
}) |
|
} |
|
function escapeHtml(value) { |
|
return String(value || '').replace(/[&<>"']/g, ch => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[ch])) |
|
} |
|
async function refresh() { |
|
list.innerHTML = '<div class="empty">正在扫描已配对设备...</div>' |
|
render(await window.bluetoothSelect.list()) |
|
} |
|
document.getElementById('refresh').onclick = refresh |
|
document.getElementById('settings').onclick = () => window.bluetoothSelect.openSettings() |
|
refresh() |
|
</script> |
|
</body> |
|
</html>` |
|
} |
|
|
|
async function openBluetoothSelectWindow(sender, namePrefix) { |
|
if (bluetoothSelectWindow && !bluetoothSelectWindow.isDestroyed()) { |
|
bluetoothSelectWindow.focus() |
|
return |
|
} |
|
|
|
bluetoothSelectWindow = new BrowserWindow({ |
|
width: 560, |
|
height: 620, |
|
parent: mainWindow && !mainWindow.isDestroyed() ? mainWindow : undefined, |
|
modal: false, |
|
title: '蓝牙连接', |
|
webPreferences: { |
|
preload: path.join(__dirname, 'src', 'business', 'bluetooth-select-preload.js'), |
|
contextIsolation: true, |
|
nodeIntegration: false, |
|
sandbox: false |
|
} |
|
}) |
|
|
|
ipcMain.once('bluetooth-select-device', (_event, device) => { |
|
const address = String(device?.address || '') |
|
const success = address.length > 0 ? 'true' : 'false' |
|
if (sender && !sender.isDestroyed()) { |
|
sender.send('bluetooth-device-selected', success, address) |
|
} |
|
if (bluetoothSelectWindow && !bluetoothSelectWindow.isDestroyed()) { |
|
bluetoothSelectWindow.close() |
|
} |
|
}) |
|
|
|
bluetoothSelectWindow.on('closed', () => { |
|
bluetoothSelectWindow = null |
|
}) |
|
await bluetoothSelectWindow.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(bluetoothSelectHtml(namePrefix))}`) |
|
} |
|
|
|
function hasSensor(sensorName) { |
|
return !!sensorController?.devices?.some(device => device.sensor === sensorName) |
|
} |
|
|
|
function routeSummary(route) { |
|
const raw = String(route || '#/') |
|
try { |
|
const [pathPart, queryPart = ''] = raw.split('?') |
|
const params = new URLSearchParams(queryPart) |
|
const picked = [] |
|
for (const key of ['id', 'taskId', 'pageType']) { |
|
const value = params.get(key) |
|
if (!value) continue |
|
const shortValue = value.length > 12 ? `${value.slice(0, 12)}...` : value |
|
picked.push(`${key}=${shortValue}`) |
|
} |
|
return picked.length ? `${pathPart}?${picked.join('&')}` : pathPart |
|
} catch { |
|
return raw.length > 60 ? `${raw.slice(0, 60)}...` : raw |
|
} |
|
} |
|
|
|
function connectionLabel(name, connected) { |
|
return `${name}${connected ? '✓' : '×'}` |
|
} |
|
|
|
function connectionItems() { |
|
return [ |
|
connectionLabel('网络', topBarState.network === true), |
|
connectionLabel('倾角', hasSensor('HWT6053')), |
|
connectionLabel('测距', hasSensor('SW_LDS20DA')), |
|
connectionLabel('全站仪', hasSensor('CHC_CGI_430')), |
|
connectionLabel('PWM', topBarState.pwm) |
|
] |
|
} |
|
|
|
function updateAppMenu() { |
|
const fullRoute = topBarState.route || '#/' |
|
const template = [ |
|
{ |
|
label: '连接', |
|
submenu: connectionItems().map(label => ({ |
|
label, |
|
enabled: false |
|
})) |
|
}, |
|
{ |
|
label: `当前页面 ${routeSummary(fullRoute)}`, |
|
submenu: [ |
|
{ |
|
label: fullRoute, |
|
enabled: false |
|
}, |
|
{ |
|
label: '复制当前页面链接', |
|
click: () => clipboard.writeText(fullRoute) |
|
} |
|
] |
|
}, |
|
{ |
|
label: `壳地址 ${topBarState.targetUrl ? '已设置' : '本地包'}`, |
|
submenu: [ |
|
{ |
|
label: topBarState.targetUrl || '本地包', |
|
enabled: false |
|
}, |
|
{ |
|
label: '切换到本地包', |
|
click: () => loadShellTarget('') |
|
}, |
|
{ |
|
label: '打开地址设置', |
|
click: () => emit('app-shell-open-url-dialog') |
|
} |
|
] |
|
}, |
|
{ |
|
label: '视图', |
|
submenu: [ |
|
{ role: 'reload', label: '刷新' }, |
|
{ role: 'toggleDevTools', label: '开发者工具' }, |
|
{ type: 'separator' }, |
|
{ role: 'resetZoom', label: '实际大小' }, |
|
{ role: 'zoomIn', label: '放大' }, |
|
{ role: 'zoomOut', label: '缩小' } |
|
] |
|
}, |
|
{ |
|
label: '窗口', |
|
submenu: [ |
|
{ role: 'minimize', label: '最小化' }, |
|
{ role: 'close', label: '关闭' } |
|
] |
|
} |
|
] |
|
Menu.setApplicationMenu(Menu.buildFromTemplate(template)) |
|
} |
|
|
|
function updateWindowTitle() { |
|
if (!mainWindow || mainWindow.isDestroyed()) return |
|
const connections = connectionItems().join(' ') |
|
const route = routeSummary(topBarState.route || '#/') |
|
mainWindow.setTitle(`光伏智能建造调度管控系统 | ${connections} | ${route}`) |
|
updateAppMenu() |
|
} |
|
|
|
function setPwmPort(deviceName, connected) { |
|
const port = String(deviceName || '') |
|
if (!port) return |
|
if (connected) pwmPorts.add(port) |
|
else pwmPorts.delete(port) |
|
topBarState.pwm = pwmPorts.size > 0 |
|
} |
|
|
|
function normalizeShellUrl(input) { |
|
const raw = String(input || '').trim() |
|
if (!raw) return '' |
|
try { |
|
const parsed = new URL(raw) |
|
if (parsed.protocol === 'http:' || parsed.protocol === 'https:') return parsed.toString() |
|
} catch {} |
|
return '' |
|
} |
|
|
|
function getStartupTarget() { |
|
const cache = readCache() |
|
return normalizeShellUrl(cache.shellTargetUrl || cache.appTargetUrl || '') |
|
} |
|
|
|
async function loadShellTarget(targetUrl) { |
|
if (!mainWindow || mainWindow.isDestroyed()) return |
|
const normalized = normalizeShellUrl(targetUrl) |
|
if (normalized) { |
|
topBarState.targetUrl = normalized |
|
writeCache({ ...readCache(), shellTargetUrl: normalized }) |
|
await mainWindow.loadURL(normalized) |
|
updateWindowTitle() |
|
return |
|
} |
|
topBarState.targetUrl = '' |
|
const localIndex = path.join(__dirname, 'dist', 'index.html') |
|
await mainWindow.loadFile(localIndex) |
|
updateWindowTitle() |
|
} |
|
|
|
function lastSensorValue(id, opId) { |
|
if (!sensorController) return null |
|
const device = sensorController.devices.find(d => d.id === id) || |
|
sensorController.devices.find(d => d.sensor === id) || |
|
sensorController.devices.find(d => d.sensor === String(id || '').split(':')[0]) |
|
if (!device) return null |
|
return sensorController.lastValues.get(`${device.sensor}:${opId}`) ?? null |
|
} |
|
|
|
function createSensorController() { |
|
sensorController = new SensorController({ |
|
onValue: data => emit('sensor-data', data), |
|
onDeviceAttached: device => { |
|
emit('sensor-attached', { id: device.id, name: device.name }) |
|
updateWindowTitle() |
|
}, |
|
onDeviceDetached: device => { |
|
emit('sensor-detached', { id: device.id, name: device.name }) |
|
updateWindowTitle() |
|
}, |
|
onLog: (level, msg) => emit('sensor-log', { level, msg }) |
|
}) |
|
sensorController.refresh() |
|
.then(() => updateWindowTitle()) |
|
.catch(err => console.error('[sensor] refresh failed:', err.message)) |
|
} |
|
|
|
function createWindow() { |
|
mainWindow = new BrowserWindow({ |
|
width: 1280, |
|
height: 800, |
|
fullscreen: false, |
|
webPreferences: { |
|
preload: path.join(__dirname, 'preload.js'), |
|
contextIsolation: true, |
|
nodeIntegration: false, |
|
sandbox: false |
|
} |
|
}) |
|
|
|
createSensorController() |
|
usbBridge = new UsbSerialBridge(mainWindow, { |
|
onDeviceDiscovered: payload => { |
|
if (payload?.probeId === 'remote_control') setPwmPort(payload.deviceName, true) |
|
if (sensorController) sensorController.refresh().catch(() => {}) |
|
updateWindowTitle() |
|
}, |
|
onDeviceDetached: device => { |
|
if (device?.probeId === 'remote_control') { |
|
setPwmPort(device.deviceName, false) |
|
} |
|
updateWindowTitle() |
|
} |
|
}) |
|
usbBridge.startDiscovery({ keepAlive: true }).catch(err => console.error('[usb] startup discovery failed:', err.message)) |
|
|
|
const startupTarget = getStartupTarget() |
|
if (startupTarget) { |
|
topBarState.targetUrl = startupTarget |
|
mainWindow.loadURL(startupTarget) |
|
} else { |
|
mainWindow.loadFile(path.join(__dirname, 'dist', 'index.html')) |
|
} |
|
updateWindowTitle() |
|
} |
|
|
|
function registerAppIpc() { |
|
ipcMain.on('app-clear-cache-sync', (event) => { |
|
writeCache({}) |
|
if (mainWindow && !mainWindow.isDestroyed()) mainWindow.reload() |
|
event.returnValue = true |
|
}) |
|
|
|
ipcMain.on('app-version-code-sync', (event) => { |
|
const major = Number(String(pkg.version || '1').split('.')[0]) || 1 |
|
event.returnValue = major |
|
}) |
|
|
|
ipcMain.on('get-cache-sync', (event, key) => { |
|
const cache = readCache() |
|
event.returnValue = key ? (cache[key] ?? null) : JSON.stringify(cache) |
|
}) |
|
|
|
ipcMain.on('set-cache-sync', (event, key, value) => { |
|
const cache = readCache() |
|
cache[key] = value |
|
writeCache(cache) |
|
event.returnValue = true |
|
}) |
|
|
|
ipcMain.handle('get-cache', (_event, key) => { |
|
const cache = readCache() |
|
return key ? (cache[key] ?? null) : cache |
|
}) |
|
|
|
ipcMain.handle('set-cache', (_event, key, value) => { |
|
const cache = readCache() |
|
cache[key] = value |
|
writeCache(cache) |
|
return true |
|
}) |
|
|
|
ipcMain.on('app-offline-page', () => emit('app-page-mode', 'offline')) |
|
ipcMain.on('app-online-page', () => emit('app-page-mode', 'online')) |
|
ipcMain.on('app-network-status', (_event, online) => { |
|
topBarState.network = !!online |
|
updateWindowTitle() |
|
}) |
|
ipcMain.on('app-route-changed', (_event, route) => { |
|
topBarState.route = String(route || '') |
|
updateWindowTitle() |
|
}) |
|
ipcMain.handle('app-shell-get-target-url', () => topBarState.targetUrl || getStartupTarget()) |
|
ipcMain.handle('app-shell-set-target-url', async (_event, targetUrl) => { |
|
await loadShellTarget(targetUrl) |
|
return topBarState.targetUrl |
|
}) |
|
ipcMain.on('app-shell-open-url', async (_event, targetUrl) => { |
|
await loadShellTarget(targetUrl) |
|
}) |
|
|
|
ipcMain.on('app-file-list-sync', (event, dir) => { |
|
event.returnValue = fileList(dir) |
|
}) |
|
|
|
ipcMain.on('app-file-write-string', (event, filePath, content, token) => { |
|
const target = resolveAppFile(filePath) |
|
fs.promises.mkdir(path.dirname(target), { recursive: true }) |
|
.then(() => fs.promises.writeFile(target, String(content ?? ''), 'utf-8')) |
|
.then(() => event.sender.send('app-file-write-callback', token, target, true, '')) |
|
.catch(err => event.sender.send('app-file-write-callback', token, target, false, err.message)) |
|
}) |
|
|
|
ipcMain.on('app-file-read-string', (event, filePath, token) => { |
|
const target = resolveAppFile(filePath) |
|
fs.promises.readFile(target, 'utf-8') |
|
.then(content => event.sender.send('app-file-read-callback', token, target, content)) |
|
.catch(() => event.sender.send('app-file-read-callback', token, target, null)) |
|
}) |
|
|
|
ipcMain.on('app-file-delete', (event, filePath, token) => { |
|
const target = resolveAppFile(filePath) |
|
fs.promises.unlink(target) |
|
.then(() => event.sender.send('app-file-delete-callback', token, target, true)) |
|
.catch(() => event.sender.send('app-file-delete-callback', token, target, false)) |
|
}) |
|
|
|
ipcMain.on('app-export-file', (event, filename, content, token) => { |
|
const target = resolveAppFile(filename || `export-${Date.now()}.txt`) |
|
fs.promises.mkdir(path.dirname(target), { recursive: true }) |
|
.then(() => fs.promises.writeFile(target, String(content ?? ''), 'utf-8')) |
|
.then(() => event.sender.send('app-file-write-callback', token || filename, target, true, '')) |
|
.catch(err => event.sender.send('app-file-write-callback', token || filename, target, false, err.message)) |
|
}) |
|
} |
|
|
|
function registerSensorIpc() { |
|
ipcMain.on('app-sensor-open-sync', (event) => { |
|
if (sensorController) sensorController.open().catch(() => {}) |
|
event.returnValue = true |
|
}) |
|
ipcMain.on('app-sensor-close-sync', (event) => { |
|
event.returnValue = sensorController ? sensorController.close() : true |
|
}) |
|
ipcMain.on('app-sensor-is-open-sync', (event) => { |
|
event.returnValue = sensorController ? sensorController.isOpen : false |
|
}) |
|
|
|
ipcMain.on('sensor-init-all-sync', (event) => { |
|
if (sensorController) sensorController.initAll().catch(() => {}) |
|
event.returnValue = true |
|
}) |
|
ipcMain.on('sensor-flush-sync', (event) => { |
|
if (sensorController) sensorController.refresh().catch(() => {}) |
|
event.returnValue = true |
|
}) |
|
ipcMain.on('sensor-list-sync', (event) => { |
|
event.returnValue = sensorController ? sensorController.list() : [] |
|
}) |
|
ipcMain.on('sensor-init-sync', (event, id) => { |
|
event.returnValue = sensorController ? sensorController.init(id) : false |
|
}) |
|
ipcMain.on('sensor-open-sync', (event) => { |
|
if (sensorController) sensorController.open().catch(() => {}) |
|
event.returnValue = true |
|
}) |
|
ipcMain.on('sensor-close-sync', (event) => { |
|
event.returnValue = sensorController ? sensorController.close() : true |
|
}) |
|
ipcMain.on('sensor-operation-sync', (event, id, opId) => { |
|
const cached = lastSensorValue(id, opId) |
|
if (sensorController) { |
|
sensorController.operation(id, opId).catch(() => {}) |
|
} |
|
event.returnValue = cached |
|
}) |
|
ipcMain.on('sensor-operation-async', (event, id, opId, args) => { |
|
if (!sensorController) { |
|
event.sender.send('sensor-operation-callback', id, opId, null) |
|
return |
|
} |
|
sensorController.operation(id, opId, args) |
|
.then(result => event.sender.send('sensor-operation-callback', id, opId, result)) |
|
.catch(err => event.sender.send('sensor-operation-callback', id, opId, err.message)) |
|
}) |
|
} |
|
|
|
function registerLocationAndBluetoothIpc() { |
|
ipcMain.on('app-location-is-support-sync', (event) => { event.returnValue = false }) |
|
ipcMain.on('app-location-is-open-sync', (event) => { event.returnValue = false }) |
|
ipcMain.on('app-location-open', (event) => { event.sender.send('app-location-opened', 1) }) |
|
ipcMain.on('app-location-close', () => {}) |
|
|
|
ipcMain.on('bluetooth-device-select', (event, namePrefix) => { |
|
openBluetoothSelectWindow(event.sender, namePrefix).catch(err => { |
|
console.error('[bluetooth] select window failed:', err.message) |
|
event.sender.send('bluetooth-device-selected', 'false', '') |
|
}) |
|
}) |
|
ipcMain.handle('bluetooth-select-list', () => listBluetoothDevices()) |
|
ipcMain.on('bluetooth-open-settings', () => { |
|
shell.openExternal('ms-settings:bluetooth') |
|
}) |
|
ipcMain.on('bluetooth-device-open', (event, mac) => { |
|
event.sender.send('bluetooth-device-opened', mac ? 'true' : 'false', mac || '') |
|
if (mac) event.sender.send('bluetooth-device-state-changed', mac, 'CONNECTED') |
|
}) |
|
ipcMain.on('bluetooth-device-write', () => {}) |
|
ipcMain.on('bluetooth-device-close', () => {}) |
|
} |
|
|
|
function registerIpc() { |
|
registerAppIpc() |
|
registerSensorIpc() |
|
registerLocationAndBluetoothIpc() |
|
} |
|
|
|
app.whenReady().then(() => { |
|
registerIpc() |
|
ensureDir(fileRoot) |
|
createWindow() |
|
|
|
app.on('before-quit', () => { |
|
if (sensorController) sensorController.close() |
|
if (usbBridge) usbBridge.disconnectAll() |
|
}) |
|
}) |
|
|
|
app.on('window-all-closed', () => { |
|
if (process.platform !== 'darwin') app.quit() |
|
})
|
|
|