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.
301 lines
9.2 KiB
301 lines
9.2 KiB
/* |
|
* @Author: 李皓 hao_li_work@163.com |
|
* @Date: 2026-07-28 15:22:20 |
|
* @LastEditors: 李皓 hao_li_work@163.com |
|
* @LastEditTime: 2026-07-28 16:59:56 |
|
* @FilePath: \pc\main.js |
|
* @Description: |
|
* @Version: 1.0.0 |
|
*/ |
|
const { app, BrowserWindow, ipcMain } = require('electron') |
|
const path = require('path') |
|
const fs = require('fs') |
|
const { SerialPort } = require('serialport') |
|
const { WitMotionSensor } = require('./sensor') |
|
const { UsbSerialBridge } = require('./usb-serial-bridge') |
|
const pkg = require('./package.json') |
|
|
|
const cacheDir = app.getPath('userData') |
|
const cacheFile = path.join(cacheDir, 'app-cache.json') |
|
const fileRoot = path.join(cacheDir, 'files') |
|
|
|
let mainWindow = null |
|
let sensor = null |
|
let usbBridge = null |
|
let sensorOpen = false |
|
let sensorDevices = [] |
|
let lastSensorValues = {} |
|
|
|
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 sensorOperations(id) { |
|
return [ |
|
{ id: 'read-version', name: '获取当前传感器版本' }, |
|
{ id: 'read-temp', name: '获取设备温度' }, |
|
{ id: 'read-euler', name: '获取设备角度' }, |
|
{ id: 'reset-euler', name: '重置角度传感器基准' }, |
|
{ id: 'read-heading-and-vertical', name: '获取朝向和垂直夹角' } |
|
].map(op => ({ ...op, sensorId: id })) |
|
} |
|
|
|
async function refreshSensorDevices() { |
|
try { |
|
const ports = await SerialPort.list() |
|
sensorDevices = ports |
|
.filter(p => p.path && /^COM\d+$/i.test(p.path)) |
|
.map(p => ({ |
|
id: `HWT6053:${p.path}`, |
|
name: 'HWT6053', |
|
port: p.path, |
|
baudRate: 9600, |
|
isOpen: sensorOpen, |
|
operations: sensorOperations(`HWT6053:${p.path}`) |
|
})) |
|
|
|
for (const device of sensorDevices) { |
|
if (mainWindow && !mainWindow.isDestroyed()) { |
|
mainWindow.webContents.send('sensor-attached', { id: device.id, name: device.name }) |
|
} |
|
} |
|
} catch (err) { |
|
console.error('[sensor] 刷新设备失败:', err.message) |
|
sensorDevices = [] |
|
} |
|
} |
|
|
|
function normalizeSensorData(data) { |
|
if (!data || data.sensor !== 'HWT6053') return data |
|
if (data.method === 'temperature') { |
|
const value = data.value && typeof data.value === 'object' ? data.value.temp : data.value |
|
return { sensor: 'HWT6053', method: 'read-temp', value: String(value ?? '') } |
|
} |
|
if (data.method === 'angle') { |
|
return { sensor: 'HWT6053', method: 'read-euler', value: data.value } |
|
} |
|
if (data.method === 'magnetic') { |
|
return { sensor: 'HWT6053', method: 'read-heading-and-vertical', value: data.value } |
|
} |
|
return data |
|
} |
|
|
|
function cacheSensorValue(data) { |
|
const normalized = normalizeSensorData(data) |
|
if (!normalized) return |
|
lastSensorValues[normalized.method] = normalized.value |
|
if (mainWindow && !mainWindow.isDestroyed()) { |
|
mainWindow.webContents.send('sensor-data', normalized) |
|
} |
|
} |
|
|
|
function ensureSensor() { |
|
if (sensor) return sensor |
|
sensor = new WitMotionSensor({ |
|
onData: cacheSensorValue, |
|
onLog: (level, msg) => { |
|
console.log(`[sensor] ${msg}`) |
|
if (mainWindow && !mainWindow.isDestroyed()) { |
|
try { mainWindow.webContents.send('sensor-log', { level, msg }) } catch {} |
|
} |
|
} |
|
}) |
|
return sensor |
|
} |
|
|
|
function openAppSensor() { |
|
sensorOpen = true |
|
refreshSensorDevices() |
|
ensureSensor().connect() |
|
return true |
|
} |
|
|
|
function closeAppSensor() { |
|
sensorOpen = false |
|
if (sensor) sensor.disconnect() |
|
sensor = null |
|
return true |
|
} |
|
|
|
function runSensorOperation(opId) { |
|
if (opId === 'read-version') return pkg.version || '1' |
|
if (opId === 'read-temp') return lastSensorValues['read-temp'] ?? null |
|
if (opId === 'read-euler') { |
|
const value = lastSensorValues['read-euler'] |
|
return value == null ? null : JSON.stringify(value) |
|
} |
|
if (opId === 'read-heading-and-vertical') { |
|
const value = lastSensorValues['read-heading-and-vertical'] |
|
return value == null ? null : JSON.stringify(value) |
|
} |
|
if (opId === 'reset-euler') return '0' |
|
return null |
|
} |
|
|
|
function createWindow() { |
|
mainWindow = new BrowserWindow({ |
|
width: 800, |
|
height: 600, |
|
webPreferences: { |
|
preload: path.join(__dirname, 'preload.js'), |
|
contextIsolation: true, |
|
nodeIntegration: false, |
|
sandbox: false |
|
} |
|
}) |
|
|
|
mainWindow.loadFile('./dist/index.html') |
|
usbBridge = new UsbSerialBridge(mainWindow) |
|
refreshSensorDevices() |
|
} |
|
|
|
function registerIpc() { |
|
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', () => { |
|
if (mainWindow && !mainWindow.isDestroyed()) mainWindow.webContents.send('app-page-mode', 'offline') |
|
}) |
|
|
|
ipcMain.on('app-online-page', () => { |
|
if (mainWindow && !mainWindow.isDestroyed()) mainWindow.webContents.send('app-page-mode', 'online') |
|
}) |
|
|
|
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-sensor-open-sync', (event) => { event.returnValue = openAppSensor() }) |
|
ipcMain.on('app-sensor-close-sync', (event) => { event.returnValue = closeAppSensor() }) |
|
ipcMain.on('app-sensor-is-open-sync', (event) => { event.returnValue = sensorOpen }) |
|
|
|
ipcMain.on('sensor-init-all-sync', (event) => { |
|
refreshSensorDevices() |
|
event.returnValue = true |
|
}) |
|
ipcMain.on('sensor-flush-sync', (event) => { |
|
refreshSensorDevices() |
|
event.returnValue = true |
|
}) |
|
ipcMain.on('sensor-list-sync', (event) => { |
|
event.returnValue = sensorDevices |
|
}) |
|
ipcMain.on('sensor-init-sync', (event, id) => { |
|
event.returnValue = sensorDevices.some(device => device.id === id) |
|
}) |
|
ipcMain.on('sensor-open-sync', (event) => { event.returnValue = openAppSensor() }) |
|
ipcMain.on('sensor-close-sync', (event) => { event.returnValue = closeAppSensor() }) |
|
ipcMain.on('sensor-operation-sync', (event, _id, opId) => { |
|
event.returnValue = runSensorOperation(opId) |
|
}) |
|
ipcMain.on('sensor-operation-async', (event, id, opId) => { |
|
const result = runSensorOperation(opId) |
|
event.sender.send('sensor-operation-callback', id, opId, result) |
|
}) |
|
|
|
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', () => {}) |
|
} |
|
|
|
app.whenReady().then(() => { |
|
registerIpc() |
|
ensureDir(fileRoot) |
|
createWindow() |
|
|
|
app.on('before-quit', () => { |
|
if (sensor) sensor.disconnect() |
|
if (usbBridge) usbBridge.disconnectAll() |
|
}) |
|
})
|
|
|