光伏pc端应用
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

/*
* @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()
})
})