diff --git a/main.js b/main.js index 637d7c7..2b21e48 100644 --- a/main.js +++ b/main.js @@ -10,9 +10,20 @@ const { app, BrowserWindow, ipcMain } = require('electron') const path = require('path') const fs = require('fs') +const { WitMotionSensor } = require('./sensor') // 缓存文件路径(存储在应用用户数据目录下) -const cacheFile = path.join(app.getPath('userData'), 'app-cache.json') +const cacheDir = app.getPath('userData') +const cacheFile = path.join(cacheDir, 'app-cache.json') + +// 确保缓存目录存在 +function ensureCacheDir() { + try { + if (!fs.existsSync(cacheDir)) { + fs.mkdirSync(cacheDir, { recursive: true }) + } + } catch { /* 忽略 */ } +} // 读取缓存文件 function readCache() { @@ -27,9 +38,14 @@ function readCache() { // 写入缓存文件 function writeCache(data) { - fs.writeFileSync(cacheFile, JSON.stringify(data, null, 2), 'utf-8') + try { + ensureCacheDir() + fs.writeFileSync(cacheFile, JSON.stringify(data, null, 2), 'utf-8') + } catch { /* 忽略写入错误 */ } } +let sensor = null + const createWindow = () => { const win = new BrowserWindow({ width: 800, @@ -43,6 +59,23 @@ const createWindow = () => { }) win.loadFile('./dist/index.html') + + // 窗口创建后连接传感器 + sensor = new WitMotionSensor({ + onData: (data) => { + if (!win.isDestroyed()) { + win.webContents.send('sensor-data', data) + } + }, + onLog: (level, msg) => { + console.log(`[sensor] ${msg}`) + // 也发送到渲染进程,方便静态页面查看日志 + if (win && !win.isDestroyed()) { + try { win.webContents.send('sensor-log', { level, msg }) } catch {} + } + } + }) + sensor.connect() } app.whenReady().then(() => { @@ -61,4 +94,4 @@ app.whenReady().then(() => { }) createWindow() -}) \ No newline at end of file +}) diff --git a/package-lock.json b/package-lock.json index e3fb2d3..94ba018 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "photovoltaic-pc", "version": "1.0.0", "license": "ISC", + "dependencies": { + "serialport": "^13.0.0" + }, "devDependencies": { "electron": "^43.2.0" } @@ -41,6 +44,227 @@ "undici": "^7.24.4" } }, + "node_modules/@serialport/binding-mock": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@serialport/binding-mock/-/binding-mock-10.2.2.tgz", + "integrity": "sha512-HAFzGhk9OuFMpuor7aT5G1ChPgn5qSsklTFOTUX72Rl6p0xwcSVsRtG/xaGp6bxpN7fI9D/S8THLBWbBgS6ldw==", + "dependencies": { + "@serialport/bindings-interface": "^1.2.1", + "debug": "^4.3.3" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@serialport/bindings-cpp": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@serialport/bindings-cpp/-/bindings-cpp-13.0.0.tgz", + "integrity": "sha512-r25o4Bk/vaO1LyUfY/ulR6hCg/aWiN6Wo2ljVlb4Pj5bqWGcSRC4Vse4a9AcapuAu/FeBzHCbKMvRQeCuKjzIQ==", + "hasInstallScript": true, + "dependencies": { + "@serialport/bindings-interface": "1.2.2", + "@serialport/parser-readline": "12.0.0", + "debug": "4.4.0", + "node-addon-api": "8.3.0", + "node-gyp-build": "4.8.4" + }, + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/bindings-cpp/node_modules/@serialport/parser-delimiter": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-12.0.0.tgz", + "integrity": "sha512-gu26tVt5lQoybhorLTPsH2j2LnX3AOP2x/34+DUSTNaUTzu2fBXw+isVjQJpUBFWu6aeQRZw5bJol5X9Gxjblw==", + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/bindings-cpp/node_modules/@serialport/parser-readline": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-12.0.0.tgz", + "integrity": "sha512-O7cywCWC8PiOMvo/gglEBfAkLjp/SENEML46BXDykfKP5mTPM46XMaX1L0waWU6DXJpBgjaL7+yX6VriVPbN4w==", + "dependencies": { + "@serialport/parser-delimiter": "12.0.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/bindings-cpp/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@serialport/bindings-interface": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@serialport/bindings-interface/-/bindings-interface-1.2.2.tgz", + "integrity": "sha512-CJaUd5bLvtM9c5dmO9rPBHPXTa9R2UwpkJ0wdh9JCYcbrPWsKz+ErvR0hBLeo7NPeiFdjFO4sonRljiw4d2XiA==", + "engines": { + "node": "^12.22 || ^14.13 || >=16" + } + }, + "node_modules/@serialport/parser-byte-length": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-byte-length/-/parser-byte-length-13.0.0.tgz", + "integrity": "sha512-32yvqeTAqJzAEtX5zCrN1Mej56GJ5h/cVFsCDPbF9S1ZSC9FWjOqNAgtByseHfFTSTs/4ZBQZZcZBpolt8sUng==", + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-cctalk": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-cctalk/-/parser-cctalk-13.0.0.tgz", + "integrity": "sha512-RErAe57g9gvnlieVYGIn1xymb1bzNXb2QtUQd14FpmbQQYlcrmuRnJwKa1BgTCujoCkhtaTtgHlbBWOxm8U2uA==", + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-delimiter": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-13.0.0.tgz", + "integrity": "sha512-Qqyb0FX1avs3XabQqNaZSivyVbl/yl0jywImp7ePvfZKLwx7jBZjvL+Hawt9wIG6tfq6zbFM24vzCCK7REMUig==", + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-inter-byte-timeout": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-inter-byte-timeout/-/parser-inter-byte-timeout-13.0.0.tgz", + "integrity": "sha512-a0w0WecTW7bD2YHWrpTz1uyiWA2fDNym0kjmPeNSwZ2XCP+JbirZt31l43m2ey6qXItTYVuQBthm75sPVeHnGA==", + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-packet-length": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-packet-length/-/parser-packet-length-13.0.0.tgz", + "integrity": "sha512-60ZDDIqYRi0Xs2SPZUo4Jr5LLIjtb+rvzPKMJCohrO6tAqSDponcNpcB1O4W21mKTxYjqInSz+eMrtk0LLfZIg==", + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/@serialport/parser-readline": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-13.0.0.tgz", + "integrity": "sha512-dov3zYoyf0dt1Sudd1q42VVYQ4WlliF0MYvAMA3MOyiU1IeG4hl0J6buBA2w4gl3DOCC05tGgLDN/3yIL81gsA==", + "dependencies": { + "@serialport/parser-delimiter": "13.0.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-ready": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-ready/-/parser-ready-13.0.0.tgz", + "integrity": "sha512-JNUQA+y2Rfs4bU+cGYNqOPnNMAcayhhW+XJZihSLQXOHcZsFnOa2F9YtMg9VXRWIcnHldHYtisp62Etjlw24bw==", + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-regex": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-regex/-/parser-regex-13.0.0.tgz", + "integrity": "sha512-m7HpIf56G5XcuDdA3DB34Z0pJiwxNRakThEHjSa4mG05OnWYv0IG8l2oUyYfuGMowQWaVnQ+8r+brlPxGVH+eA==", + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-slip-encoder": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-slip-encoder/-/parser-slip-encoder-13.0.0.tgz", + "integrity": "sha512-fUHZEExm6izJ7rg0A1yjXwu4sOzeBkPAjDZPfb+XQoqgtKAk+s+HfICiYn7N2QU9gyaeCO8VKgWwi+b/DowYOg==", + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-spacepacket": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-spacepacket/-/parser-spacepacket-13.0.0.tgz", + "integrity": "sha512-DoXJ3mFYmyD8X/8931agJvrBPxqTaYDsPoly9/cwQSeh/q4EjQND9ySXBxpWz5WcpyCU4jOuusqCSAPsbB30Eg==", + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/stream": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@serialport/stream/-/stream-13.0.0.tgz", + "integrity": "sha512-F7xLJKsjGo2WuEWMSEO1SimRcOA+WtWICsY13r0ahx8s2SecPQH06338g28OT7cW7uRXI7oEQAk62qh5gHJW3g==", + "dependencies": { + "@serialport/bindings-interface": "1.2.2", + "debug": "4.4.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/stream/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/@types/node": { "version": "24.13.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", @@ -54,7 +278,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "dependencies": { "ms": "^2.1.3" }, @@ -107,8 +330,25 @@ "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/node-addon-api": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.3.0.tgz", + "integrity": "sha512-8VOpLHFrOQlAH+qA0ZzuGRlALRA6/LVh8QJldbrC4DY0hXoMP0l4Acq8TzFC018HztWiRqyCEj2aTWY2UvnJUg==", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } }, "node_modules/progress": { "version": "2.0.3", @@ -131,6 +371,49 @@ "node": ">=10" } }, + "node_modules/serialport": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/serialport/-/serialport-13.0.0.tgz", + "integrity": "sha512-PHpnTd8isMGPfFTZNCzOZp9m4mAJSNWle9Jxu6BPTcWq7YXl5qN7tp8Sgn0h+WIGcD6JFz5QDgixC2s4VW7vzg==", + "dependencies": { + "@serialport/binding-mock": "10.2.2", + "@serialport/bindings-cpp": "13.0.0", + "@serialport/parser-byte-length": "13.0.0", + "@serialport/parser-cctalk": "13.0.0", + "@serialport/parser-delimiter": "13.0.0", + "@serialport/parser-inter-byte-timeout": "13.0.0", + "@serialport/parser-packet-length": "13.0.0", + "@serialport/parser-readline": "13.0.0", + "@serialport/parser-ready": "13.0.0", + "@serialport/parser-regex": "13.0.0", + "@serialport/parser-slip-encoder": "13.0.0", + "@serialport/parser-spacepacket": "13.0.0", + "@serialport/stream": "13.0.0", + "debug": "4.4.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/serialport/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/sumchecker": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", @@ -182,6 +465,127 @@ "undici": "^7.24.4" } }, + "@serialport/binding-mock": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@serialport/binding-mock/-/binding-mock-10.2.2.tgz", + "integrity": "sha512-HAFzGhk9OuFMpuor7aT5G1ChPgn5qSsklTFOTUX72Rl6p0xwcSVsRtG/xaGp6bxpN7fI9D/S8THLBWbBgS6ldw==", + "requires": { + "@serialport/bindings-interface": "^1.2.1", + "debug": "^4.3.3" + } + }, + "@serialport/bindings-cpp": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@serialport/bindings-cpp/-/bindings-cpp-13.0.0.tgz", + "integrity": "sha512-r25o4Bk/vaO1LyUfY/ulR6hCg/aWiN6Wo2ljVlb4Pj5bqWGcSRC4Vse4a9AcapuAu/FeBzHCbKMvRQeCuKjzIQ==", + "requires": { + "@serialport/bindings-interface": "1.2.2", + "@serialport/parser-readline": "12.0.0", + "debug": "4.4.0", + "node-addon-api": "8.3.0", + "node-gyp-build": "4.8.4" + }, + "dependencies": { + "@serialport/parser-delimiter": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-12.0.0.tgz", + "integrity": "sha512-gu26tVt5lQoybhorLTPsH2j2LnX3AOP2x/34+DUSTNaUTzu2fBXw+isVjQJpUBFWu6aeQRZw5bJol5X9Gxjblw==" + }, + "@serialport/parser-readline": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-12.0.0.tgz", + "integrity": "sha512-O7cywCWC8PiOMvo/gglEBfAkLjp/SENEML46BXDykfKP5mTPM46XMaX1L0waWU6DXJpBgjaL7+yX6VriVPbN4w==", + "requires": { + "@serialport/parser-delimiter": "12.0.0" + } + }, + "debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "requires": { + "ms": "^2.1.3" + } + } + } + }, + "@serialport/bindings-interface": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@serialport/bindings-interface/-/bindings-interface-1.2.2.tgz", + "integrity": "sha512-CJaUd5bLvtM9c5dmO9rPBHPXTa9R2UwpkJ0wdh9JCYcbrPWsKz+ErvR0hBLeo7NPeiFdjFO4sonRljiw4d2XiA==" + }, + "@serialport/parser-byte-length": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-byte-length/-/parser-byte-length-13.0.0.tgz", + "integrity": "sha512-32yvqeTAqJzAEtX5zCrN1Mej56GJ5h/cVFsCDPbF9S1ZSC9FWjOqNAgtByseHfFTSTs/4ZBQZZcZBpolt8sUng==" + }, + "@serialport/parser-cctalk": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-cctalk/-/parser-cctalk-13.0.0.tgz", + "integrity": "sha512-RErAe57g9gvnlieVYGIn1xymb1bzNXb2QtUQd14FpmbQQYlcrmuRnJwKa1BgTCujoCkhtaTtgHlbBWOxm8U2uA==" + }, + "@serialport/parser-delimiter": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-13.0.0.tgz", + "integrity": "sha512-Qqyb0FX1avs3XabQqNaZSivyVbl/yl0jywImp7ePvfZKLwx7jBZjvL+Hawt9wIG6tfq6zbFM24vzCCK7REMUig==" + }, + "@serialport/parser-inter-byte-timeout": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-inter-byte-timeout/-/parser-inter-byte-timeout-13.0.0.tgz", + "integrity": "sha512-a0w0WecTW7bD2YHWrpTz1uyiWA2fDNym0kjmPeNSwZ2XCP+JbirZt31l43m2ey6qXItTYVuQBthm75sPVeHnGA==" + }, + "@serialport/parser-packet-length": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-packet-length/-/parser-packet-length-13.0.0.tgz", + "integrity": "sha512-60ZDDIqYRi0Xs2SPZUo4Jr5LLIjtb+rvzPKMJCohrO6tAqSDponcNpcB1O4W21mKTxYjqInSz+eMrtk0LLfZIg==" + }, + "@serialport/parser-readline": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-13.0.0.tgz", + "integrity": "sha512-dov3zYoyf0dt1Sudd1q42VVYQ4WlliF0MYvAMA3MOyiU1IeG4hl0J6buBA2w4gl3DOCC05tGgLDN/3yIL81gsA==", + "requires": { + "@serialport/parser-delimiter": "13.0.0" + } + }, + "@serialport/parser-ready": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-ready/-/parser-ready-13.0.0.tgz", + "integrity": "sha512-JNUQA+y2Rfs4bU+cGYNqOPnNMAcayhhW+XJZihSLQXOHcZsFnOa2F9YtMg9VXRWIcnHldHYtisp62Etjlw24bw==" + }, + "@serialport/parser-regex": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-regex/-/parser-regex-13.0.0.tgz", + "integrity": "sha512-m7HpIf56G5XcuDdA3DB34Z0pJiwxNRakThEHjSa4mG05OnWYv0IG8l2oUyYfuGMowQWaVnQ+8r+brlPxGVH+eA==" + }, + "@serialport/parser-slip-encoder": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-slip-encoder/-/parser-slip-encoder-13.0.0.tgz", + "integrity": "sha512-fUHZEExm6izJ7rg0A1yjXwu4sOzeBkPAjDZPfb+XQoqgtKAk+s+HfICiYn7N2QU9gyaeCO8VKgWwi+b/DowYOg==" + }, + "@serialport/parser-spacepacket": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@serialport/parser-spacepacket/-/parser-spacepacket-13.0.0.tgz", + "integrity": "sha512-DoXJ3mFYmyD8X/8931agJvrBPxqTaYDsPoly9/cwQSeh/q4EjQND9ySXBxpWz5WcpyCU4jOuusqCSAPsbB30Eg==" + }, + "@serialport/stream": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@serialport/stream/-/stream-13.0.0.tgz", + "integrity": "sha512-F7xLJKsjGo2WuEWMSEO1SimRcOA+WtWICsY13r0ahx8s2SecPQH06338g28OT7cW7uRXI7oEQAk62qh5gHJW3g==", + "requires": { + "@serialport/bindings-interface": "1.2.2", + "debug": "4.4.0" + }, + "dependencies": { + "debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "requires": { + "ms": "^2.1.3" + } + } + } + }, "@types/node": { "version": "24.13.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", @@ -195,7 +599,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "requires": { "ms": "^2.1.3" } @@ -226,8 +629,17 @@ "ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node-addon-api": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.3.0.tgz", + "integrity": "sha512-8VOpLHFrOQlAH+qA0ZzuGRlALRA6/LVh8QJldbrC4DY0hXoMP0l4Acq8TzFC018HztWiRqyCEj2aTWY2UvnJUg==" + }, + "node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==" }, "progress": { "version": "2.0.3", @@ -241,6 +653,37 @@ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "dev": true }, + "serialport": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/serialport/-/serialport-13.0.0.tgz", + "integrity": "sha512-PHpnTd8isMGPfFTZNCzOZp9m4mAJSNWle9Jxu6BPTcWq7YXl5qN7tp8Sgn0h+WIGcD6JFz5QDgixC2s4VW7vzg==", + "requires": { + "@serialport/binding-mock": "10.2.2", + "@serialport/bindings-cpp": "13.0.0", + "@serialport/parser-byte-length": "13.0.0", + "@serialport/parser-cctalk": "13.0.0", + "@serialport/parser-delimiter": "13.0.0", + "@serialport/parser-inter-byte-timeout": "13.0.0", + "@serialport/parser-packet-length": "13.0.0", + "@serialport/parser-readline": "13.0.0", + "@serialport/parser-ready": "13.0.0", + "@serialport/parser-regex": "13.0.0", + "@serialport/parser-slip-encoder": "13.0.0", + "@serialport/parser-spacepacket": "13.0.0", + "@serialport/stream": "13.0.0", + "debug": "4.4.0" + }, + "dependencies": { + "debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "requires": { + "ms": "^2.1.3" + } + } + } + }, "sumchecker": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", diff --git a/package.json b/package.json index ae850da..56db820 100644 --- a/package.json +++ b/package.json @@ -12,5 +12,8 @@ "license": "ISC", "devDependencies": { "electron": "^43.2.0" + }, + "dependencies": { + "serialport": "^13.0.0" } } diff --git a/preload.js b/preload.js index e24cc4f..97d7ba7 100644 --- a/preload.js +++ b/preload.js @@ -56,12 +56,29 @@ const networkUtil = { /** * 应用传感器对象 + * 静态页面设置 appSensor.onValueCallback = function(obj) { ... } + * obj 包含: { method, sensor, value } */ +const sensorCallbacks = { onValueCallback: null } + const appSensor = { version: '1.0.0', - name: 'appSensor' + name: 'appSensor', + get onValueCallback() { + return sensorCallbacks.onValueCallback + }, + set onValueCallback(fn) { + sensorCallbacks.onValueCallback = fn + } } +// 监听主进程发送的传感器数据 +ipcRenderer.on('sensor-data', (_event, data) => { + if (sensorCallbacks.onValueCallback) { + sensorCallbacks.onValueCallback(data) + } +}) + /** * 应用缓存对象(数据持久化到本地文件) */ diff --git a/sensor.js b/sensor.js new file mode 100644 index 0000000..1107e41 --- /dev/null +++ b/sensor.js @@ -0,0 +1,393 @@ +/** + * 姿态传感器通信模块 (Modbus RTU) + * 协议文档: https://wit-motion.yuque.com/wumwnr/ltst03/oex3ay + * 产品: HWT6053-485 (高精度Modbus协议) + * 数据格式: 8N1, 从站地址默认 0x50 + * 指令必须在 10S 内完成,否则自动上锁,需先解锁 + */ +const { SerialPort } = require('serialport') + +// ======================== CRC-16 Modbus ======================== +const crc16Table = new Uint16Array(256) +for (let i = 0; i < 256; i++) { + let crc = i + for (let j = 0; j < 8; j++) { + crc = (crc & 1) ? (0xA001 ^ (crc >> 1)) : (crc >> 1) + } + crc16Table[i] = crc +} + +/** + * CRC-16 Modbus 计算 + * @param {Buffer} data + * @returns {Buffer} [CRCH, CRCL] — 高字节在前(文档要求 CRCH 在前) + */ +function crc16Modbus(data) { + let crc = 0xFFFF + for (const b of data) { + crc = crc16Table[(crc ^ b) & 0xFF] ^ (crc >> 8) + } + return Buffer.from([(crc >> 8) & 0xFF, crc & 0xFF]) // [CRCH, CRCL] +} + +// ======================== 常量 ======================== +const DEV_ADDR = 0x50 // 从站地址(默认) + +// ======================== Modbus 命令构建 ======================== + +/** 解锁指令 (写寄存器 0x69, 值 0xB588) */ +function buildUnlockCmd() { + const buf = Buffer.from([DEV_ADDR, 0x06, 0x00, 0x69, 0xB5, 0x88]) + return Buffer.concat([buf, crc16Modbus(buf)]) +} + +/** 保存指令 (写寄存器 0x00, 值 0x0000) */ +function buildSaveCmd() { + const buf = Buffer.from([DEV_ADDR, 0x06, 0x00, 0x00, 0x00, 0x00]) + return Buffer.concat([buf, crc16Modbus(buf)]) +} + +/** + * Modbus 读保持寄存器命令 (功能码 0x03) + * @param {number} startAddr - 起始寄存器地址 + * @param {number} count - 读取寄存器数量 + */ +function buildReadRegistersCmd(startAddr, count) { + const buf = Buffer.from([DEV_ADDR, 0x03, (startAddr >> 8) & 0xFF, startAddr & 0xFF, (count >> 8) & 0xFF, count & 0xFF]) + return Buffer.concat([buf, crc16Modbus(buf)]) +} + +// ======================== Modbus 响应解析 ======================== + +/** + * 解析 Modbus 读寄存器响应 + * 响应格式: 地址(1B) | 功能码(1B) | 字节数(1B) | 数据(NB) | CRC16(2B) + * 返回原始 16-bit 寄存器值数组 + */ +function parseModbusResponse(buf) { + if (buf.length < 5) return null + + // 校验地址和功能码 + if (buf[0] !== DEV_ADDR || buf[1] !== 0x03) return null + + // CRC 校验 + const crcRcv = buf.slice(buf.length - 2) + const crcCal = crc16Modbus(buf.slice(0, buf.length - 2)) + if (crcRcv[0] !== crcCal[0] || crcRcv[1] !== crcCal[1]) { + console.log('[sensor] CRC校验失败: 收到=' + crcRcv.toString('hex').toUpperCase() + ' 计算=' + crcCal.toString('hex').toUpperCase()) + return null + } + + const byteCount = buf[2] + if (buf.length !== 5 + byteCount) { + console.log('[sensor] 长度不匹配: 期望=' + (5 + byteCount) + ' 收到=' + buf.length) + return null + } + + const data = [] + for (let i = 0; i < byteCount; i += 2) { + data.push((buf[3 + i] << 8) | buf[3 + i + 1]) + } + return data +} + +/** + * 将 16-bit 有符号值转为 int16 + */ +function toInt16(val) { + return val >= 0x8000 ? val - 0x10000 : val +} + +/** + * 将 32-bit 值(高位+低位组合)转为有符号 int32 + */ +function toInt32(hi, lo) { + let val = (hi << 16) | (lo & 0xFFFF) + return val >= 0x80000000 ? val - 0x100000000 : val +} + +// ======================== 传感器类 ======================== +class WitMotionSensor { + /** + * @param {Object} options + * @param {Function} options.onData - 回调函数,接收 { method, sensor, value } + * @param {Function} options.onLog - 日志回调 (level, msg) + */ + constructor(options = {}) { + this.onData = options.onData || (() => {}) + this.onLog = options.onLog || ((level, msg) => console.log(`[sensor] ${msg}`)) + this.port = null + this.timer = null + this.connected = false + + // Modbus 响应缓冲 + this._modbusBuf = Buffer.alloc(0) + + // 设备类型 + this.sensorName = 'HWT6053' + } + + /** 日志辅助 */ + _log(msg) { this.onLog('info', msg) } + _warn(msg) { this.onLog('warn', msg) } + _error(msg) { this.onLog('error', msg) } + + /** 扫描并连接传感器 */ + async connect() { + try { + this._log('开始扫描串口设备...') + const ports = await SerialPort.list() + this._log(`检测到 ${ports.length} 个串口:`) + for (const p of ports) { + this._log(` ${p.path} VID=${p.vendorId || 'N/A'} PID=${p.productId || 'N/A'} 制造商=${p.manufacturer || 'N/A'}`) + } + + // 优先找 CH340 / CP210x / FTDI 芯片的串口 + let sensorPort = ports.find(p => + p.vendorId && ['1a86', '10c4', '0403'].includes(p.vendorId.toLowerCase()) + ) + if (!sensorPort) { + sensorPort = ports.find(p => p.path && /^COM\d+$/i.test(p.path)) + } + if (!sensorPort) { + this._error('未找到串口设备') + return false + } + + this._log(`选定串口: ${sensorPort.path}`) + + // 按优先级尝试波特率 + const baudRates = [9600, 115200] // 默认 9600,试试 115200 + for (const baud of baudRates) { + const ok = await this._tryOpen(sensorPort.path, baud) + if (ok) return true + } + + this._error('所有波特率均失败,请检查串口连接') + return false + } catch (err) { + this._error('扫描失败: ' + err.message) + return false + } + } + + /** 尝试以指定波特率打开串口 */ + _tryOpen(path, baudRate) { + return new Promise((resolve) => { + this._log(`尝试 ${path} @ ${baudRate}...`) + + const port = new SerialPort({ + path, + baudRate, + dataBits: 8, + parity: 'none', + stopBits: 1, + autoOpen: false, + dtr: false, + rts: false + }) + + port.open((err) => { + if (err) { + this._warn(`${path} @ ${baudRate} 打开失败: ${err.message}`) + resolve(false) + return + } + + this._log(`=== 已连接: ${path} @ ${baudRate} ===`) + this.port = port + this.connected = true + this._modbusBuf = Buffer.alloc(0) + this._setupListeners() + + // 连接后先解锁并查询数据 + this._initialSetup() + + resolve(true) + }) + }) + } + + /** 初始配置:解锁 + 延时后验证 + 启动轮询 */ + _initialSetup() { + if (!this.port) return + + // 1) 解锁 + this._log('步骤1: 发送解锁指令...') + try { this.port.write(buildUnlockCmd()) } catch (e) { this._error('解锁发送失败: ' + e.message) } + + // 2) 500ms 后发读命令验证连接 + setTimeout(() => { + this._log('步骤2: 发送读指令验证连接...') + this._queryAll() + }, 500) + + // 3) 1.5s 后如果收到数据则启动轮询,否则重试解锁 + setTimeout(() => { + if (this.connected) { + this._log('步骤3: 启动定时轮询 (每200ms)...') + // 再发一次解锁(确保后续写操作不超时) + try { this.port.write(buildUnlockCmd()) } catch {} + this._startPolling() + } + }, 1500) + } + + /** 查询所有传感器数据(一次性读取 0x34 ~ 0x43 共 16 个寄存器) */ + _queryAll() { + if (!this.port || !this.connected) return + try { + // 一次读取: AX~TEMP (寄存器 0x34~0x43 = 52~67 = 16个寄存器) + // 包含: 加速度(3) + 角速度(3) + 磁场(3) + 角度(6) + 温度(1) = 16 + const cmd = buildReadRegistersCmd(0x34, 16) + this.port.write(cmd) + } catch (e) { + this._error('查询发送失败: ' + e.message) + } + } + + /** 启动定时轮询 */ + _startPolling() { + if (!this.connected) return + this._queryAll() + if (this.timer) clearInterval(this.timer) + this.timer = setInterval(() => this._queryAll(), 200) + } + + /** 注册串口事件监听 */ + _setupListeners() { + let dataCount = 0 + this.port.on('data', (chunk) => { + dataCount++ + if (dataCount <= 3) { + this._log(`收到原始数据 #${dataCount}: ${chunk.length} 字节 [${chunk.toString('hex').toUpperCase()}]`) + } + this._onData(chunk) + }) + this.port.on('error', (err) => this._error('串口错误: ' + err.message)) + this.port.on('close', (err) => { + this._warn(`串口已断开${err ? ': ' + err.message : ''}`) + this.connected = false + if (this.timer) { clearInterval(this.timer); this.timer = null } + }) + } + + /** 数据接收处理 */ + _onData(chunk) { + this._modbusBuf = Buffer.concat([this._modbusBuf, chunk]) + + // 尝试从缓冲中解析 Modbus 帧 + while (this._modbusBuf.length >= 5) { + // 查找帧头 (设备地址) + const idx = this._modbusBuf.indexOf(DEV_ADDR) + if (idx === -1) { this._modbusBuf = Buffer.alloc(0); break } + if (idx > 0) { + this._log(`跳过 ${idx} 个非帧头字节`) + this._modbusBuf = this._modbusBuf.slice(idx) + } + + if (this._modbusBuf.length < 5) break + + // 功能码必须是 0x03 + if (this._modbusBuf[1] !== 0x03) { + this._log(`非 0x03 响应, 功能码=0x${this._modbusBuf[1].toString(16)}`) + this._modbusBuf = this._modbusBuf.slice(1) + continue + } + + const byteCount = this._modbusBuf[2] + const expectedLen = 3 + 1 + byteCount + 2 // addr + func + byteCount + data + crc = 5 + byteCount + if (this._modbusBuf.length < expectedLen) break // 等待更多数据 + + const frame = this._modbusBuf.slice(0, expectedLen) + this._modbusBuf = this._modbusBuf.slice(expectedLen) + + // 解析 + const regs = parseModbusResponse(frame) + if (regs) { + this._log(`Modbus解析成功: ${regs.length} 个寄存器: [${regs.map(r => '0x' + r.toString(16).padStart(4, '0').toUpperCase()).join(', ')}]`) + this._emitModbusData(regs) + } else { + this._log(`Modbus解析失败, 原始帧: [${frame.toString('hex').toUpperCase()}]`) + } + } + + // 防止无限增长 + if (this._modbusBuf.length > 512) { + this._log('缓冲溢出,重置') + this._modbusBuf = Buffer.alloc(0) + } + } + + /** 发射 Modbus 解析结果,根据寄存器数量推断数据类型 */ + _emitModbusData(regs) { + // 寄存器布局: [AX, AY, AZ, GX, GY, GZ, HX, HY, HZ, LRoll, HRoll, LPitch, HPitch, LYaw, HYaw, TEMP] + // 索引: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 + const len = regs.length + + if (len >= 16) { + // 16 个寄存器:完整数据 + const ax = toInt16(regs[0]), ay = toInt16(regs[1]), az = toInt16(regs[2]) + const gx = toInt16(regs[3]), gy = toInt16(regs[4]), gz = toInt16(regs[5]) + const hx = toInt16(regs[6]), hy = toInt16(regs[7]), hz = toInt16(regs[8]) + const lroll = regs[9], hroll = regs[10] + const lpitch = regs[11], hpitch = regs[12] + const lyaw = regs[13], hyaw = regs[14] + const temp = toInt16(regs[15]) + + // 加速度 = 原始值/32768 * 16g + const accelVal = { + ax: +(ax / 32768 * 16).toFixed(4), + ay: +(ay / 32768 * 16).toFixed(4), + az: +(az / 32768 * 16).toFixed(4) + } + this._log('→ 加速度: ' + JSON.stringify(accelVal)) + this.onData({ method: 'acceleration', sensor: this.sensorName, value: accelVal }) + + // 角速度 = 原始值/32768 * 2000°/s + const gyroVal = { + wx: +(gx / 32768 * 2000).toFixed(2), + wy: +(gy / 32768 * 2000).toFixed(2), + wz: +(gz / 32768 * 2000).toFixed(2) + } + this._log('→ 角速度: ' + JSON.stringify(gyroVal)) + this.onData({ method: 'angularVelocity', sensor: this.sensorName, value: gyroVal }) + + // 磁场 + const magVal = { hx, hy, hz } + this._log('→ 磁场: ' + JSON.stringify(magVal)) + this.onData({ method: 'magnetic', sensor: this.sensorName, value: magVal }) + + // 角度 = (高16位<<16 | 低16位) / 1000° + const roll = toInt32(hroll, lroll) + const pitch = toInt32(hpitch, lpitch) + const yaw = toInt32(hyaw, lyaw) + const angleVal = { + roll: +(roll / 1000).toFixed(2), + pitch: +(pitch / 1000).toFixed(2), + yaw: +(yaw / 1000).toFixed(2) + } + this._log('→ 角度: ' + JSON.stringify(angleVal)) + this.onData({ method: 'angle', sensor: this.sensorName, value: angleVal }) + + // 温度 = 原始值/100 ℃ + const tempVal = { temp: +(temp / 100).toFixed(2) } + this._log('→ 温度: ' + JSON.stringify(tempVal)) + this.onData({ method: 'temperature', sensor: this.sensorName, value: tempVal }) + + } else { + this._log('未知寄存器数量: ' + len) + } + } + + /** 断开连接 */ + disconnect() { + this.connected = false + if (this.timer) { clearInterval(this.timer); this.timer = null } + if (this.port && this.port.isOpen) { + try { this.port.close() } catch {} + } + } +} + +module.exports = { WitMotionSensor }