Browse Source

feat: 尝试链接姿态传感器

master
WIN-87ES3P38OPV\EDY 7 days ago
parent
commit
3cc201d62f
  1. 35
      main.js
  2. 455
      package-lock.json
  3. 3
      package.json
  4. 19
      preload.js
  5. 393
      sensor.js

35
main.js

@ -10,9 +10,20 @@ @@ -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() { @@ -27,9 +38,14 @@ function readCache() {
// 写入缓存文件
function writeCache(data) {
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 = () => { @@ -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(() => {

455
package-lock.json generated

@ -8,6 +8,9 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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",

3
package.json

@ -12,5 +12,8 @@ @@ -12,5 +12,8 @@
"license": "ISC",
"devDependencies": {
"electron": "^43.2.0"
},
"dependencies": {
"serialport": "^13.0.0"
}
}

19
preload.js

@ -56,12 +56,29 @@ const networkUtil = { @@ -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)
}
})
/**
* 应用缓存对象数据持久化到本地文件
*/

393
sensor.js

@ -0,0 +1,393 @@ @@ -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 }
Loading…
Cancel
Save