Compare commits
10 Commits
c3c4eb64f0
...
e612c852e5
| Author | SHA1 | Date | |
|---|---|---|---|
| e612c852e5 | |||
| 39b5edb585 | |||
| 71345419d5 | |||
| fe841fb0ba | |||
| 1cd3badb2d | |||
|
|
b50cb503a8 | ||
|
|
8e06ca76ca | ||
|
|
783b901fe7 | ||
|
|
ae31462434 | ||
|
|
7bffe34f54 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -3,4 +3,5 @@ dist
|
||||
.DS_Store
|
||||
mav.parm
|
||||
mav.tlog
|
||||
mav.tlog.raw
|
||||
mav.tlog.raw
|
||||
deploy.sh
|
||||
@@ -6,7 +6,7 @@ A web-based tool for DroneCAN configuration and monitoring. This application pro
|
||||
|
||||
## Access
|
||||
|
||||
**Official Entry**: https://can.vimdrones.com
|
||||
**Official Entry**: https://can.ardupilot.org
|
||||
|
||||
**Backup Entry**: https://can.vimdrones.com
|
||||
|
||||
@@ -90,4 +90,4 @@ This project uses:
|
||||
- Webpack 5 for bundling
|
||||
- Babel for JavaScript transpilation
|
||||
|
||||
The development server runs on port 8080 with hot module replacement enabled.
|
||||
The development server runs on port 8080 with hot module replacement enabled.
|
||||
|
||||
226
package-lock.json
generated
226
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "dronecan-configurator",
|
||||
"version": "1.0.0",
|
||||
"name": "dronecan-webtools",
|
||||
"version": "1.0.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "dronecan-configurator",
|
||||
"version": "1.0.0",
|
||||
"name": "dronecan-webtools",
|
||||
"version": "1.0.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.26.7",
|
||||
@@ -20,6 +20,7 @@
|
||||
"buffer": "^6.0.3",
|
||||
"crypto-browserify": "^3.12.1",
|
||||
"html-webpack-plugin": "^5.6.3",
|
||||
"jspack": "^0.0.4",
|
||||
"long": "^5.2.4",
|
||||
"process": "^0.11.10",
|
||||
"react": "^18.3.1",
|
||||
@@ -52,14 +53,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.26.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
|
||||
"integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
||||
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": "^7.25.9",
|
||||
"@babel/helper-validator-identifier": "^7.27.1",
|
||||
"js-tokens": "^4.0.0",
|
||||
"picocolors": "^1.0.0"
|
||||
"picocolors": "^1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -320,18 +321,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
"version": "7.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
|
||||
"integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
||||
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
|
||||
"integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
|
||||
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -361,25 +362,25 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helpers": {
|
||||
"version": "7.26.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.7.tgz",
|
||||
"integrity": "sha512-8NHiL98vsi0mbPQmYAGWwfcFaOy4j2HY49fXJCfuDcdE7fMIsH9a7GdaeXpIBsbT7307WU8KCMp5pUVDNL4f9A==",
|
||||
"version": "7.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz",
|
||||
"integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/template": "^7.25.9",
|
||||
"@babel/types": "^7.26.7"
|
||||
"@babel/template": "^7.27.2",
|
||||
"@babel/types": "^7.28.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.26.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.7.tgz",
|
||||
"integrity": "sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w==",
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz",
|
||||
"integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.26.7"
|
||||
"@babel/types": "^7.28.0"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
@@ -1510,26 +1511,23 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.26.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.7.tgz",
|
||||
"integrity": "sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ==",
|
||||
"version": "7.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.2.tgz",
|
||||
"integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"regenerator-runtime": "^0.14.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz",
|
||||
"integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==",
|
||||
"version": "7.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
|
||||
"integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.25.9",
|
||||
"@babel/parser": "^7.25.9",
|
||||
"@babel/types": "^7.25.9"
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/parser": "^7.27.2",
|
||||
"@babel/types": "^7.27.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -1554,13 +1552,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.26.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.7.tgz",
|
||||
"integrity": "sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg==",
|
||||
"version": "7.28.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz",
|
||||
"integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.25.9",
|
||||
"@babel/helper-validator-identifier": "^7.25.9"
|
||||
"@babel/helper-string-parser": "^7.27.1",
|
||||
"@babel/helper-validator-identifier": "^7.27.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -2192,9 +2190,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/http-proxy": {
|
||||
"version": "1.17.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.15.tgz",
|
||||
"integrity": "sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ==",
|
||||
"version": "1.17.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.16.tgz",
|
||||
"integrity": "sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
@@ -3288,16 +3286,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/compression": {
|
||||
"version": "1.7.5",
|
||||
"resolved": "https://registry.npmjs.org/compression/-/compression-1.7.5.tgz",
|
||||
"integrity": "sha512-bQJ0YRck5ak3LgtnpKkiabX5pNF7tMUh1BSy2ZBOTh0Dim0BUu6aPPwByIns6/A5Prh8PufSPerMDUklpzes2Q==",
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz",
|
||||
"integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"compressible": "~2.0.18",
|
||||
"debug": "2.6.9",
|
||||
"negotiator": "~0.6.4",
|
||||
"on-headers": "~1.0.2",
|
||||
"on-headers": "~1.1.0",
|
||||
"safe-buffer": "5.2.1",
|
||||
"vary": "~1.1.2"
|
||||
},
|
||||
@@ -4342,9 +4340,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
||||
"version": "1.15.11",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -4789,9 +4787,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/http-proxy-middleware": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz",
|
||||
"integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==",
|
||||
"version": "2.0.9",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz",
|
||||
"integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/http-proxy": "^1.17.8",
|
||||
@@ -5301,6 +5299,11 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/jspack": {
|
||||
"version": "0.0.4",
|
||||
"resolved": "https://registry.npmjs.org/jspack/-/jspack-0.0.4.tgz",
|
||||
"integrity": "sha512-DC/lSTXYDDdYWzyY/9A1kMzp6Ov9mCRhZQ1cGg4te2w3y4/aKZTSspvbYN4LUsvSzMCb/H8z4TV9mYYW/bs3PQ=="
|
||||
},
|
||||
"node_modules/kind-of": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
||||
@@ -5701,9 +5704,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/on-headers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
|
||||
"integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
|
||||
"integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
@@ -5899,21 +5902,53 @@
|
||||
}
|
||||
},
|
||||
"node_modules/pbkdf2": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz",
|
||||
"integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==",
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.3.tgz",
|
||||
"integrity": "sha512-wfRLBZ0feWRhCIkoMB6ete7czJcnNnqRpcoWQBLqatqXXmelSRqfdDK4F3u9T2s2cXas/hQJcryI/4lAL+XTlA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"create-hash": "^1.1.2",
|
||||
"create-hmac": "^1.1.4",
|
||||
"ripemd160": "^2.0.1",
|
||||
"safe-buffer": "^5.0.1",
|
||||
"sha.js": "^2.4.8"
|
||||
"create-hash": "~1.1.3",
|
||||
"create-hmac": "^1.1.7",
|
||||
"ripemd160": "=2.0.1",
|
||||
"safe-buffer": "^5.2.1",
|
||||
"sha.js": "^2.4.11",
|
||||
"to-buffer": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
}
|
||||
},
|
||||
"node_modules/pbkdf2/node_modules/create-hash": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.1.3.tgz",
|
||||
"integrity": "sha512-snRpch/kwQhcdlnZKYanNF1m0RDlrCdSKQaH87w1FCFPVPNCQ/Il9QJKAX2jVBZddRdaHBMC+zXa9Gw9tmkNUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cipher-base": "^1.0.1",
|
||||
"inherits": "^2.0.1",
|
||||
"ripemd160": "^2.0.0",
|
||||
"sha.js": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pbkdf2/node_modules/hash-base": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hash-base/-/hash-base-2.0.2.tgz",
|
||||
"integrity": "sha512-0TROgQ1/SxE6KmxWSvXHvRj90/Xo1JvZShofnYF+f6ZsGtR4eES7WfrQzPalmyagfKZCXpVnitiRebZulWsbiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/pbkdf2/node_modules/ripemd160": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.1.tgz",
|
||||
"integrity": "sha512-J7f4wutN8mdbV08MJnXibYpCOPHR+yzy+iQ/AsjMv2j8cLavQ8VGagDFUwwTAdF8FmRKVeNpbTTEwNHCW1g94w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"hash-base": "^2.0.0",
|
||||
"inherits": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@@ -6323,12 +6358,6 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/regenerator-runtime": {
|
||||
"version": "0.14.1",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
|
||||
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/regenerator-transform": {
|
||||
"version": "0.15.2",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz",
|
||||
@@ -7159,6 +7188,26 @@
|
||||
"integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/to-buffer": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.1.tgz",
|
||||
"integrity": "sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"isarray": "^2.0.5",
|
||||
"safe-buffer": "^5.2.1",
|
||||
"typed-array-buffer": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/to-buffer/node_modules/isarray": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
|
||||
"integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
@@ -7215,6 +7264,20 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/typed-array-buffer": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
|
||||
"integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.3",
|
||||
"es-errors": "^1.3.0",
|
||||
"is-typed-array": "^1.1.14"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/underscore": {
|
||||
"version": "1.13.7",
|
||||
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz",
|
||||
@@ -7522,14 +7585,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-dev-server": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.0.tgz",
|
||||
"integrity": "sha512-90SqqYXA2SK36KcT6o1bvwvZfJFcmoamqeJY7+boioffX9g9C0wjjJRGUrQIuh43pb0ttX7+ssavmj/WN2RHtA==",
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.2.tgz",
|
||||
"integrity": "sha512-QcQ72gh8a+7JO63TAx/6XZf/CWhgMzu5m0QirvPfGvptOusAxG12w2+aua1Jkjr7hzaWDnJ2n6JFeexMHI+Zjg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/bonjour": "^3.5.13",
|
||||
"@types/connect-history-api-fallback": "^1.5.4",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/express-serve-static-core": "^4.17.21",
|
||||
"@types/serve-index": "^1.9.4",
|
||||
"@types/serve-static": "^1.15.5",
|
||||
"@types/sockjs": "^0.3.36",
|
||||
@@ -7542,7 +7606,7 @@
|
||||
"connect-history-api-fallback": "^2.0.0",
|
||||
"express": "^4.21.2",
|
||||
"graceful-fs": "^4.2.6",
|
||||
"http-proxy-middleware": "^2.0.7",
|
||||
"http-proxy-middleware": "^2.0.9",
|
||||
"ipaddr.js": "^2.1.0",
|
||||
"launch-editor": "^2.6.1",
|
||||
"open": "^10.0.3",
|
||||
@@ -7577,6 +7641,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-dev-server/node_modules/@types/express-serve-static-core": {
|
||||
"version": "4.19.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz",
|
||||
"integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"@types/qs": "*",
|
||||
"@types/range-parser": "*",
|
||||
"@types/send": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-merge": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dronecan-webtools",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.2",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
@@ -22,6 +22,7 @@
|
||||
"buffer": "^6.0.3",
|
||||
"crypto-browserify": "^3.12.1",
|
||||
"html-webpack-plugin": "^5.6.3",
|
||||
"jspack": "^0.0.4",
|
||||
"long": "^5.2.4",
|
||||
"process": "^0.11.10",
|
||||
"react": "^18.3.1",
|
||||
|
||||
56
src/App.js
56
src/App.js
@@ -18,20 +18,12 @@ import ConnectionIndicators from './ConnectionIndicators';
|
||||
import DnsIcon from '@mui/icons-material/Dns';
|
||||
import LanIcon from '@mui/icons-material/Lan';
|
||||
import CompactSidebar from './CompactSidebar';
|
||||
import DNAServerModal from './DNAServerModal';
|
||||
import DynamicNodeIdServer from './services/DynamicNodeIdServer';
|
||||
|
||||
window.mavlinkSession = new MavlinkSession();
|
||||
window.localNode = new dronecan.Node({name: "com.vimdrones.web_gui"});
|
||||
localNode.on('sendFrame', (messageId, data, len) => {
|
||||
const msg = new mavlink20.messages.can_frame(
|
||||
mavlinkSession.targetSystem, // target_system
|
||||
mavlinkSession.targetComponent, // target_component
|
||||
localNode.bus,
|
||||
len,
|
||||
messageId,
|
||||
data.toString('binary')
|
||||
);
|
||||
mavlinkSession.sendMavlinkMsg(msg);
|
||||
mavlinkSession.sendCanFrame(localNode.bus, messageId, data, len);
|
||||
});
|
||||
|
||||
|
||||
@@ -51,25 +43,26 @@ const App = () => {
|
||||
const [snackbarMessage, setSnackbarMessage] = useState('');
|
||||
const [snackbarSeverity, setSnackbarSeverity] = useState('info');
|
||||
const [selectedBus, setSelectedBus] = useState(0);
|
||||
const [dnaModalOpen, setDnaModalOpen] = useState(false);
|
||||
const [dnaServerActive, setDnaServerActive] = useState(false);
|
||||
|
||||
const openWindow = (windowTitle, windowPath, windowSize) => {
|
||||
if (subWindowRef[windowPath]) {
|
||||
if (subWindowRef[windowPath] && !subWindowRef[windowPath].closed) {
|
||||
subWindowRef[windowPath].focus();
|
||||
return;
|
||||
}
|
||||
const newWindow = window.open(windowPath, windowTitle, windowSize);
|
||||
if (newWindow) {
|
||||
subWindowRef[windowPath] = newWindow;
|
||||
setSubWindowRef(subWindowRef);
|
||||
setSubWindowRef((prev) => ({ ...prev, [windowPath]: newWindow }));
|
||||
|
||||
newWindow.addEventListener('beforeunload', () => {
|
||||
subWindowRef[windowPath] = null;
|
||||
setSubWindowRef(subWindowRef);
|
||||
setSubWindowRef((prev) => {
|
||||
const next = { ...prev };
|
||||
delete next[windowPath];
|
||||
return next;
|
||||
});
|
||||
});
|
||||
} else {
|
||||
console.error(`Main: Failed to open ${windowName}`);
|
||||
console.error(`Main: Failed to open ${windowTitle}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -114,18 +107,24 @@ const App = () => {
|
||||
setModalOpen(true);
|
||||
};
|
||||
|
||||
const handleCloseModal = (serverRunning) => {
|
||||
const handleCloseModal = () => {
|
||||
setModalOpen(false);
|
||||
setDnaServerActive(serverRunning);
|
||||
};
|
||||
|
||||
const handleOpenDnaModal = () => {
|
||||
setDnaModalOpen(true);
|
||||
};
|
||||
const handleToggleDnaServer = () => {
|
||||
if (!window.dnaServer) {
|
||||
window.dnaServer = new DynamicNodeIdServer(window.localNode);
|
||||
}
|
||||
|
||||
const handleCloseDnaModal = (serverRunning) => {
|
||||
setDnaModalOpen(false);
|
||||
setDnaServerActive(serverRunning);
|
||||
if (window.dnaServer.getStatus().isActive) {
|
||||
window.dnaServer.stop();
|
||||
setDnaServerActive(false);
|
||||
showMessage('DNA server stopped', 'info');
|
||||
} else {
|
||||
const success = window.dnaServer.start(1, 125);
|
||||
setDnaServerActive(success);
|
||||
showMessage(success ? 'DNA server started' : 'Failed to start DNA server', success ? 'success' : 'error');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -198,7 +197,7 @@ const App = () => {
|
||||
}} /> :
|
||||
<DnsIcon />
|
||||
}
|
||||
onClick={handleOpenDnaModal}
|
||||
onClick={handleToggleDnaServer}
|
||||
sx={dnaServerActive ? {
|
||||
borderColor: 'success.main',
|
||||
'&:hover': {
|
||||
@@ -285,11 +284,6 @@ const App = () => {
|
||||
selectedBus={selectedBus}
|
||||
onBusChange={handleBusChange}
|
||||
/>
|
||||
<DNAServerModal
|
||||
open={dnaModalOpen}
|
||||
onClose={handleCloseDnaModal}
|
||||
showMessage={showMessage.bind(this)}
|
||||
/>
|
||||
<Snackbar
|
||||
open={snackbarOpen}
|
||||
autoHideDuration={6000}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Dialog, DialogTitle, DialogContent, DialogActions,
|
||||
Dialog, DialogTitle, DialogContent,
|
||||
Button, FormControl, InputLabel, Select, MenuItem,
|
||||
Typography, Box, Divider, RadioGroup, FormControlLabel, Radio, Chip,
|
||||
TextField, Tabs, Tab, Paper, IconButton
|
||||
Typography, Box, Divider, Chip,
|
||||
TextField, Paper, IconButton
|
||||
} from '@mui/material';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import UsbIcon from '@mui/icons-material/Usb';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import Visibility from '@mui/icons-material/Visibility';
|
||||
import VisibilityOff from '@mui/icons-material/VisibilityOff';
|
||||
import WebSerial from './web_serial';
|
||||
|
||||
// Add this constant at the top of your file, outside the component
|
||||
@@ -62,6 +64,11 @@ const CONNECTION_TYPES = {
|
||||
WEBSOCKET: 'websocket'
|
||||
};
|
||||
|
||||
const SERIAL_PROTOCOLS = {
|
||||
MAVLINK: 'mavlink',
|
||||
SLCAN: 'slcan'
|
||||
};
|
||||
|
||||
// Add this constant inside the ConnectionSettingsModal.js file, outside the component
|
||||
const INTERFACE_BUS_LIST = [0, 1]; //BUS 1, BUS 2
|
||||
|
||||
@@ -74,10 +81,16 @@ const ConnectionSettingsModal = ({
|
||||
selectedBus, // New prop
|
||||
onBusChange // New prop
|
||||
}) => {
|
||||
// State for showing/hiding mavlink signing
|
||||
const [showMavlinkSigning, setShowMavlinkSigning] = useState(false);
|
||||
const handleToggleMavlinkSigning = () => {
|
||||
setShowMavlinkSigning((show) => !show);
|
||||
};
|
||||
// Port and connection management
|
||||
const [ports, setPorts] = useState([]);
|
||||
const [selectedPort, setSelectedPort] = useState(null);
|
||||
const [baudRate, setBaudRate] = useState(DEFAULT_BAUD_RATE);
|
||||
const [serialProtocol, setSerialProtocol] = useState(SERIAL_PROTOCOLS.MAVLINK);
|
||||
const [wsHost, setWsHost] = useState(DEFAULT_WS_HOST);
|
||||
const [wsPort, setWsPort] = useState(DEFAULT_WS_PORT);
|
||||
|
||||
@@ -86,13 +99,16 @@ const ConnectionSettingsModal = ({
|
||||
|
||||
// Track active connection
|
||||
const [activeConnection, setActiveConnection] = useState(null); // null, 'serial', or 'websocket'
|
||||
const [activeSerialProtocol, setActiveSerialProtocol] = useState(null);
|
||||
|
||||
// Add these state variables after your other state declarations
|
||||
const [ipError, setIpError] = useState('');
|
||||
const [hostError, setHostError] = useState('');
|
||||
const [portError, setPortError] = useState('');
|
||||
|
||||
// Add nodeId state to use the prop value
|
||||
const [nodeId, setNodeId] = useState(127);
|
||||
// Add mavlink signing state
|
||||
const [mavlinkSigning, setMavlinkSigning] = useState('');
|
||||
|
||||
// Add state for the forwarding interval
|
||||
const [forwardingInterval, setForwardingInterval] = useState(null);
|
||||
@@ -228,9 +244,10 @@ const ConnectionSettingsModal = ({
|
||||
if (activeConnection === 'serial') {
|
||||
// Disconnect using close()
|
||||
window.mavlinkSession.close();
|
||||
|
||||
|
||||
// Update the UI regardless of connection state
|
||||
setActiveConnection(null);
|
||||
setActiveSerialProtocol(null);
|
||||
onConnectionStatusChange(false);
|
||||
showMessage('Serial connection closed', 'info');
|
||||
|
||||
@@ -255,25 +272,28 @@ const ConnectionSettingsModal = ({
|
||||
}
|
||||
}
|
||||
|
||||
window.mavlinkSession.initWebSerialConnection(port, baudRate);
|
||||
window.mavlinkSession.initWebSerialConnection(port, baudRate, { protocol: serialProtocol });
|
||||
window.mavlinkSession.addWebSerialOpenHandler(() => {
|
||||
// Set Node ID and Bus for the local node
|
||||
window.localNode.setNodeId(parseInt(nodeId, 10));
|
||||
window.localNode.setBus(selectedBus);
|
||||
window.localNode.setBus(selectedBus);
|
||||
|
||||
if (serialProtocol === SERIAL_PROTOCOLS.MAVLINK) {
|
||||
const intervalId = setInterval(() => {
|
||||
if (window.mavlinkSession) {
|
||||
window.mavlinkSession.enableMavlinkCanForward(window.localNode.bus);
|
||||
}
|
||||
}, 1000);
|
||||
setForwardingInterval(intervalId);
|
||||
} else {
|
||||
setForwardingInterval(null);
|
||||
}
|
||||
|
||||
|
||||
// Start the mavlinkCanForward interval
|
||||
const intervalId = setInterval(() => {
|
||||
if (window.mavlinkSession) {
|
||||
window.mavlinkSession.enableMavlinkCanForward(window.localNode.bus);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
setForwardingInterval(intervalId);
|
||||
setActiveConnection('serial');
|
||||
setActiveSerialProtocol(serialProtocol);
|
||||
setConnectionInProgress(false);
|
||||
onConnectionStatusChange(true);
|
||||
showMessage('Serial connection established', 'success');
|
||||
showMessage(`Serial ${serialProtocol === SERIAL_PROTOCOLS.SLCAN ? 'SLCAN' : 'MAVLink'} connection established`, 'success');
|
||||
})
|
||||
|
||||
window.mavlinkSession.addWebSerialErrorHandler((error) => {
|
||||
@@ -315,34 +335,27 @@ const ConnectionSettingsModal = ({
|
||||
return '';
|
||||
}
|
||||
|
||||
// Check for IPv4 format
|
||||
if (input.includes('.')) {
|
||||
// If input matches IPv4 pattern, validate as IPv4, else treat as hostname/domain
|
||||
const ipv4Pattern = /^\d{1,3}(\.\d{1,3}){3}$/;
|
||||
if (ipv4Pattern.test(input)) {
|
||||
const octets = input.split('.');
|
||||
|
||||
// An IPv4 address must have exactly 4 octets
|
||||
if (octets.length !== 4) {
|
||||
return 'Invalid IPv4 format';
|
||||
}
|
||||
|
||||
// Each octet must be a number between 0 and 255
|
||||
for (const octet of octets) {
|
||||
const num = parseInt(octet, 10);
|
||||
if (isNaN(num) || num < 0 || num > 255 || octet !== num.toString()) {
|
||||
return 'Each part must be a number between 0-255';
|
||||
}
|
||||
}
|
||||
|
||||
// Valid IPv4
|
||||
return '';
|
||||
}
|
||||
|
||||
// Check for hostname format
|
||||
const hostnamePattern = /^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])(\.[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])*$/;
|
||||
|
||||
// Check for hostname or domain (including subdomains)
|
||||
// Allow domains like 'support.ardupilot.org', 'foo.local', etc.
|
||||
const hostnamePattern = /^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?$/;
|
||||
|
||||
if (hostnamePattern.test(input)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
|
||||
return 'Invalid IP address or hostname';
|
||||
};
|
||||
|
||||
@@ -377,11 +390,16 @@ const ConnectionSettingsModal = ({
|
||||
}
|
||||
};
|
||||
|
||||
// Handler for Mavlink Signing input
|
||||
const handleMavlinkSigningChange = (e) => {
|
||||
setMavlinkSigning(e.target.value);
|
||||
};
|
||||
|
||||
// Update the ws host/port change handlers
|
||||
const handleWsHostChange = (e) => {
|
||||
const value = e.target.value;
|
||||
setWsHost(value);
|
||||
setIpError(validateIpAddress(value));
|
||||
setHostError(validateIpAddress(value));
|
||||
};
|
||||
|
||||
const handleWsPortChange = (e) => {
|
||||
@@ -396,56 +414,52 @@ const ConnectionSettingsModal = ({
|
||||
if (activeConnection === 'websocket') {
|
||||
// Force close the connection
|
||||
window.mavlinkSession.close();
|
||||
|
||||
// Always update the UI state regardless of actual connection state
|
||||
setActiveConnection(null);
|
||||
setActiveSerialProtocol(null);
|
||||
onConnectionStatusChange(false);
|
||||
showMessage('WebSocket connection closed', 'info');
|
||||
|
||||
// Clear the forwarding interval
|
||||
if (forwardingInterval) {
|
||||
clearInterval(forwardingInterval);
|
||||
setForwardingInterval(null);
|
||||
}
|
||||
} else {
|
||||
// Set in-progress state before attempting to connect
|
||||
setConnectionInProgress(true);
|
||||
|
||||
// Validate both fields before connecting
|
||||
const hostError = validateIpAddress(wsHost);
|
||||
const portError = validatePort(wsPort);
|
||||
|
||||
setIpError(hostError);
|
||||
setPortError(portError);
|
||||
|
||||
// Only connect if both validations pass
|
||||
if (!hostError && !portError) {
|
||||
const hostErr = validateIpAddress(wsHost);
|
||||
const portErr = validatePort(wsPort);
|
||||
setHostError(hostErr);
|
||||
setPortError(portErr);
|
||||
if (!hostErr && !portErr) {
|
||||
if (activeConnection) {
|
||||
window.mavlinkSession.close();
|
||||
// Clear any existing interval
|
||||
if (forwardingInterval) {
|
||||
clearInterval(forwardingInterval);
|
||||
}
|
||||
}
|
||||
|
||||
// Use window.mavlinkSession consistently
|
||||
window.mavlinkSession.initWebSocketConnection(wsHost, parseInt(wsPort, 10));
|
||||
// Use wss if signing is present, else ws
|
||||
const wsProtocol = mavlinkSigning ? 'wss' : 'ws';
|
||||
// Compose the full URL for the websocket
|
||||
const wsUrl = `${wsProtocol}://${wsHost}:${wsPort}`;
|
||||
if (window.mavlinkSession.initWebSocketConnection.length === 1) {
|
||||
// If the function expects a URL
|
||||
window.mavlinkSession.initWebSocketConnection(wsUrl);
|
||||
} else {
|
||||
// Fallback to old signature (host, port)
|
||||
window.mavlinkSession.initWebSocketConnection(wsHost, parseInt(wsPort, 10), mavlinkSigning);
|
||||
}
|
||||
|
||||
window.mavlinkSession.addWebSocketOpenHandler(() => {
|
||||
console.log('WebSocket connection open');
|
||||
// Set Node ID and Bus for the local node
|
||||
window.localNode.setNodeId(parseInt(nodeId, 10));
|
||||
window.localNode.setBus(selectedBus);
|
||||
|
||||
// Start the mavlinkCanForward interval
|
||||
const intervalId = setInterval(() => {
|
||||
if (window.mavlinkSession) {
|
||||
window.mavlinkSession.enableMavlinkCanForward(window.localNode.bus);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
setForwardingInterval(intervalId);
|
||||
setActiveConnection('websocket');
|
||||
setActiveSerialProtocol(null);
|
||||
onConnectionStatusChange(true);
|
||||
setConnectionInProgress(false);
|
||||
showMessage('WebSocket connection established', 'success');
|
||||
@@ -453,20 +467,13 @@ const ConnectionSettingsModal = ({
|
||||
|
||||
window.mavlinkSession.addWebSocketErrorHandler((error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
|
||||
// Clear any existing interval
|
||||
if (forwardingInterval) {
|
||||
clearInterval(forwardingInterval);
|
||||
setForwardingInterval(null);
|
||||
}
|
||||
|
||||
setActiveConnection(null);
|
||||
onConnectionStatusChange(false);
|
||||
|
||||
// Reset in-progress state on error
|
||||
setConnectionInProgress(false);
|
||||
|
||||
// Show error message using App's showMessage function
|
||||
let errorMsg = 'Connection failed';
|
||||
if (error && error.message) {
|
||||
errorMsg = `Connection failed: ${error.message}`;
|
||||
@@ -478,14 +485,12 @@ const ConnectionSettingsModal = ({
|
||||
|
||||
window.mavlinkSession.webSocketConnect();
|
||||
} else {
|
||||
// If validation fails, reset the in-progress state
|
||||
setConnectionInProgress(false);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error with WebSocket connection:', error);
|
||||
showMessage(`WebSocket error: ${error.message || 'Unknown error'}`, 'error');
|
||||
// Reset in-progress state on any error
|
||||
setConnectionInProgress(false);
|
||||
}
|
||||
};
|
||||
@@ -519,9 +524,9 @@ const ConnectionSettingsModal = ({
|
||||
<Typography variant="h6">Adapter Settings</Typography>
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
{activeConnection && (
|
||||
<Chip
|
||||
label={`Connected via ${activeConnection}`}
|
||||
color="success"
|
||||
<Chip
|
||||
label={activeConnection === 'serial' && activeSerialProtocol ? `Connected via serial (${activeSerialProtocol === SERIAL_PROTOCOLS.SLCAN ? 'SLCAN' : 'MAVLink'})` : `Connected via ${activeConnection}`}
|
||||
color="success"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
/>
|
||||
@@ -594,7 +599,19 @@ const ConnectionSettingsModal = ({
|
||||
</FormControl>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
|
||||
<FormControl fullWidth size="small" disabled={activeConnection !== null}>
|
||||
<InputLabel>Serial Protocol</InputLabel>
|
||||
<Select
|
||||
value={serialProtocol}
|
||||
onChange={(e) => setSerialProtocol(e.target.value)}
|
||||
label="Serial Protocol"
|
||||
>
|
||||
<MenuItem value={SERIAL_PROTOCOLS.MAVLINK}>MAVLink tunnel</MenuItem>
|
||||
<MenuItem value={SERIAL_PROTOCOLS.SLCAN}>SLCAN / LAWICEL</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
{/* Port action buttons */}
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Button
|
||||
@@ -650,8 +667,8 @@ const ConnectionSettingsModal = ({
|
||||
disabled={activeConnection !== null}
|
||||
size="small"
|
||||
sx={{ flex: 3 }}
|
||||
error={!!ipError}
|
||||
helperText={ipError}
|
||||
error={!!hostError}
|
||||
helperText={hostError}
|
||||
/>
|
||||
<TextField
|
||||
label="Port"
|
||||
@@ -667,23 +684,23 @@ const ConnectionSettingsModal = ({
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Button
|
||||
onClick={handleWebSocketConnect}
|
||||
color={activeConnection === 'websocket' ? "error" : "primary"}
|
||||
variant="contained"
|
||||
disabled={
|
||||
connectionInProgress || // Disable when connection attempt is in progress
|
||||
!!ipError ||
|
||||
!!portError ||
|
||||
!wsHost ||
|
||||
!wsPort ||
|
||||
(activeConnection !== null && activeConnection !== 'websocket')
|
||||
}
|
||||
size="small"
|
||||
>
|
||||
{activeConnection === 'websocket' ? 'Disconnect' :
|
||||
connectionInProgress && !activeConnection ? 'Connecting...' : 'Connect'}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleWebSocketConnect}
|
||||
color={activeConnection === 'websocket' ? "error" : "primary"}
|
||||
variant="contained"
|
||||
disabled={
|
||||
connectionInProgress || // Disable when connection attempt is in progress
|
||||
!!hostError ||
|
||||
!!portError ||
|
||||
!wsHost ||
|
||||
!wsPort ||
|
||||
(activeConnection !== null && activeConnection !== 'websocket')
|
||||
}
|
||||
size="small"
|
||||
>
|
||||
{activeConnection === 'websocket' ? 'Disconnect' :
|
||||
connectionInProgress && !activeConnection ? 'Connecting...' : 'Connect'}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
@@ -691,7 +708,7 @@ const ConnectionSettingsModal = ({
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Bus Selection */}
|
||||
{/* Bus Selection and Mavlink Signing */}
|
||||
<Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
<TextField
|
||||
@@ -708,6 +725,29 @@ const ConnectionSettingsModal = ({
|
||||
error={validateNodeId(nodeId) !== ''}
|
||||
helperText={validateNodeId(nodeId)}
|
||||
/>
|
||||
<TextField
|
||||
label="Mavlink Signing"
|
||||
value={mavlinkSigning}
|
||||
onChange={handleMavlinkSigningChange}
|
||||
disabled={activeConnection !== null}
|
||||
size="small"
|
||||
placeholder="Secret Key"
|
||||
type={showMavlinkSigning ? 'text' : 'password'}
|
||||
sx={{ flex: 1 }}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<IconButton
|
||||
aria-label={showMavlinkSigning ? 'Hide secret' : 'Show secret'}
|
||||
onClick={handleToggleMavlinkSigning}
|
||||
edge="end"
|
||||
size="small"
|
||||
tabIndex={-1}
|
||||
>
|
||||
{showMavlinkSigning ? <VisibilityOff fontSize="small" /> : <Visibility fontSize="small" />}
|
||||
</IconButton>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -58,14 +58,119 @@ class FileServer {
|
||||
* @param {string} path - The virtual path to register the file under
|
||||
* @return {Promise} - Resolves when file is loaded
|
||||
*/
|
||||
/**
|
||||
* Parse Intel HEX format and convert to binary ArrayBuffer
|
||||
* @param {string} hexContent - The Intel HEX file content as string
|
||||
* @returns {ArrayBuffer} - Binary data
|
||||
*/
|
||||
parseIntelHex(hexContent) {
|
||||
const lines = hexContent.split(/\r?\n/);
|
||||
let binaryData = [];
|
||||
let minAddress = Infinity;
|
||||
let maxAddress = 0;
|
||||
let addressMap = {};
|
||||
let baseAddress = 0; // For extended address records
|
||||
|
||||
for (let line of lines) {
|
||||
line = line.trim();
|
||||
if (!line || !line.startsWith(':')) continue;
|
||||
|
||||
// Parse Intel HEX record
|
||||
const byteCount = parseInt(line.substring(1, 3), 16);
|
||||
const address = parseInt(line.substring(3, 7), 16);
|
||||
const recordType = parseInt(line.substring(7, 9), 16);
|
||||
const data = line.substring(9, 9 + (byteCount * 2));
|
||||
const checksum = parseInt(line.substring(9 + (byteCount * 2), 9 + (byteCount * 2) + 2), 16);
|
||||
|
||||
// Verify checksum
|
||||
let sum = byteCount + ((address >> 8) & 0xFF) + (address & 0xFF) + recordType;
|
||||
for (let i = 0; i < data.length; i += 2) {
|
||||
sum += parseInt(data.substring(i, i + 2), 16);
|
||||
}
|
||||
sum = (~sum + 1) & 0xFF;
|
||||
if (sum !== checksum) {
|
||||
throw new Error(`Checksum mismatch in line: ${line}`);
|
||||
}
|
||||
|
||||
if (recordType === 0x00) { // Data record
|
||||
const bytes = [];
|
||||
for (let i = 0; i < data.length; i += 2) {
|
||||
bytes.push(parseInt(data.substring(i, i + 2), 16));
|
||||
}
|
||||
|
||||
// Store data at address (with base address offset)
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
const addr = baseAddress + address + i;
|
||||
addressMap[addr] = bytes[i];
|
||||
minAddress = Math.min(minAddress, addr);
|
||||
maxAddress = Math.max(maxAddress, addr);
|
||||
}
|
||||
} else if (recordType === 0x04) { // Extended Linear Address
|
||||
// Upper 16 bits of address
|
||||
const upperAddress = parseInt(data, 16);
|
||||
baseAddress = upperAddress << 16;
|
||||
console.log(`Extended Linear Address: base = 0x${baseAddress.toString(16)}`);
|
||||
} else if (recordType === 0x02) { // Extended Segment Address
|
||||
// Segment address (multiply by 16)
|
||||
const segmentAddress = parseInt(data, 16);
|
||||
baseAddress = segmentAddress << 4;
|
||||
console.log(`Extended Segment Address: base = 0x${baseAddress.toString(16)}`);
|
||||
} else if (recordType === 0x01) { // End of file
|
||||
console.log('End of Intel HEX file');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (minAddress === Infinity) {
|
||||
throw new Error('No data found in Intel HEX file');
|
||||
}
|
||||
|
||||
// Convert to contiguous binary array
|
||||
const size = maxAddress - minAddress + 1;
|
||||
const buffer = new ArrayBuffer(size);
|
||||
const view = new Uint8Array(buffer);
|
||||
|
||||
// Fill with 0xFF (typical for flash memory)
|
||||
view.fill(0xFF);
|
||||
|
||||
// Copy data
|
||||
for (let addr = minAddress; addr <= maxAddress; addr++) {
|
||||
if (addressMap[addr] !== undefined) {
|
||||
view[addr - minAddress] = addressMap[addr];
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Intel HEX parsed: ${lines.length} lines, address range 0x${minAddress.toString(16)}-0x${maxAddress.toString(16)}, binary size: ${size} bytes`);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
loadFile(file, path = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Use the file name as the path if no path is provided
|
||||
const filePath = path || file.name;
|
||||
|
||||
// Check if this is a .hex file
|
||||
const isHexFile = file.name.toLowerCase().endsWith('.hex');
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const buffer = e.target.result;
|
||||
let buffer;
|
||||
|
||||
if (isHexFile) {
|
||||
// Parse Intel HEX format
|
||||
const hexContent = e.target.result;
|
||||
try {
|
||||
buffer = this.parseIntelHex(hexContent);
|
||||
} catch (error) {
|
||||
console.error('Error parsing Intel HEX file:', error);
|
||||
reject(new Error('Invalid Intel HEX format'));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Use raw binary data for .bin files
|
||||
buffer = e.target.result;
|
||||
}
|
||||
|
||||
this.files[filePath] = {
|
||||
name: file.name,
|
||||
size: buffer.byteLength,
|
||||
@@ -82,7 +187,12 @@ class FileServer {
|
||||
reject(error);
|
||||
};
|
||||
|
||||
reader.readAsArrayBuffer(file);
|
||||
// Read as text for .hex files, as binary for others
|
||||
if (isHexFile) {
|
||||
reader.readAsText(file);
|
||||
} else {
|
||||
reader.readAsArrayBuffer(file);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -17,11 +17,11 @@ const FirmwareUpdateModal = ({ open, onClose, targetNodeId }) => {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
// Validate file extension is .bin
|
||||
// Validate file extension is .bin or .hex
|
||||
const fileExtension = file.name.split('.').pop().toLowerCase();
|
||||
if (fileExtension !== 'bin') {
|
||||
if (fileExtension !== 'bin' && fileExtension !== 'hex') {
|
||||
setUpdateStatus('error');
|
||||
setStatusMessage('Invalid file type. Please select a .bin firmware file.');
|
||||
setStatusMessage('Invalid file type. Please select a .bin or .hex firmware file.');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ const FirmwareUpdateModal = ({ open, onClose, targetNodeId }) => {
|
||||
<DialogTitle>Firmware Update</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography variant="body2" gutterBottom>
|
||||
Please select the firmware file (.bin) to upload to node {targetNodeId}.
|
||||
Please select the firmware file (.bin|.hex) to upload to node {targetNodeId}.
|
||||
</Typography>
|
||||
|
||||
<Button
|
||||
@@ -156,7 +156,7 @@ const FirmwareUpdateModal = ({ open, onClose, targetNodeId }) => {
|
||||
<input
|
||||
type="file"
|
||||
hidden
|
||||
accept=".bin"
|
||||
accept=".bin,.hex"
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
</Button>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useRef } from 'react';
|
||||
import {
|
||||
Box, Button, Table, TableBody, TableCell, TableContainer,
|
||||
TableHead, TableRow, Typography, Paper, Tooltip, Chip
|
||||
@@ -18,6 +18,9 @@ const OPCODE_ERASE = 1;
|
||||
const NodeParam = ({ nodeId, nodes }) => {
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [editParamIndex, setEditParamIndex] = useState(null);
|
||||
const [paramsUpdateTimestamp, setParamsUpdateTimestamp] = useState(0);
|
||||
const [fetchingParams, setFetchingParams] = useState(false);
|
||||
const fetchTimeoutRef = useRef(null);
|
||||
|
||||
if (!nodeId) return null;
|
||||
const node = nodes[nodeId];
|
||||
@@ -25,19 +28,40 @@ const NodeParam = ({ nodeId, nodes }) => {
|
||||
|
||||
const handleFetchParams = () => {
|
||||
const localNode = window.localNode;
|
||||
let currentParamIndex = 0;
|
||||
let currentParamIndex = 0;
|
||||
setFetchingParams(true);
|
||||
setParamsUpdateTimestamp(Date.now());
|
||||
if (fetchTimeoutRef.current) clearTimeout(fetchTimeoutRef.current);
|
||||
|
||||
const resetFetchTimeout = () => {
|
||||
if (fetchTimeoutRef.current) clearTimeout(fetchTimeoutRef.current);
|
||||
fetchTimeoutRef.current = setTimeout(() => {
|
||||
setFetchingParams(false);
|
||||
console.warn(`Fetch params timed out for node ${nodeId} at index ${currentParamIndex}`);
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
const callback = (transfer) => {
|
||||
const msg = transfer.payload;
|
||||
console.log('Param response:', { nodeId: transfer.sourceNodeId, index: currentParamIndex, name: msg?.fields?.name?.toString?.() });
|
||||
if (msg && msg.fields.name.items.length > 0) {
|
||||
if (msg && transfer.destNodeId === localNode.nodeId) {
|
||||
localNode.updateNodeParamsFromResponse(transfer, currentParamIndex);
|
||||
setParamsUpdateTimestamp(Date.now());
|
||||
currentParamIndex += 1;
|
||||
localNode.fetchNodeParam(nodeId, currentParamIndex, '', callback);
|
||||
resetFetchTimeout();
|
||||
localNode.fetchNodeParam(nodeId, currentParamIndex, '', callback);
|
||||
}
|
||||
} else {
|
||||
setFetchingParams(false);
|
||||
setParamsUpdateTimestamp(Date.now());
|
||||
if (fetchTimeoutRef.current) clearTimeout(fetchTimeoutRef.current);
|
||||
console.log(`Finished fetching params for node ${nodeId}`);
|
||||
}
|
||||
};
|
||||
|
||||
localNode.fetchNodeParam(nodeId, 0, '', callback);
|
||||
resetFetchTimeout();
|
||||
localNode.fetchNodeParam(nodeId, 0, '', callback);
|
||||
};
|
||||
|
||||
const handleEraseParams = () => {
|
||||
@@ -341,8 +365,9 @@ const NodeParam = ({ nodeId, nodes }) => {
|
||||
variant="contained"
|
||||
sx={{ mr: 1 }}
|
||||
startIcon={<SyncIcon />}
|
||||
disabled={fetchingParams}
|
||||
>
|
||||
Fetch All
|
||||
{fetchingParams ? 'Fetching...' : 'Fetch All'}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSaveParams}
|
||||
@@ -386,7 +411,8 @@ const NodeParam = ({ nodeId, nodes }) => {
|
||||
</Box>
|
||||
</Box>
|
||||
{renderNodeParams()}
|
||||
<ParamEditorSelector
|
||||
<span style={{ display: 'none' }}>{paramsUpdateTimestamp}</span>
|
||||
<ParamEditorSelector
|
||||
open={modalOpen}
|
||||
onClose={() => setModalOpen(false)}
|
||||
nodeId={nodeId}
|
||||
|
||||
@@ -187,7 +187,7 @@ class Node extends EventEmitter {
|
||||
this.emit(topicName, transfer);
|
||||
}
|
||||
} else {
|
||||
if (!transfer.dataTypeId || !transfer.payload) {
|
||||
if (transfer.dataTypeId === null || transfer.dataTypeId === undefined || !transfer.payload) {
|
||||
// console.error('#TODO dataTypeId or payload is null');
|
||||
return;
|
||||
}
|
||||
@@ -425,7 +425,7 @@ class Node extends EventEmitter {
|
||||
}
|
||||
|
||||
fetchNodeParam(sourceNodeId, index, name, callback=null) {
|
||||
// console.log('Fetching node param:', sourceNodeId, index, name);
|
||||
console.log('Fetching node param:', sourceNodeId, index, name);
|
||||
// const currentRequestIndex = index;
|
||||
this.nodeParamsRequestingNodeId = sourceNodeId;
|
||||
this.setNodeParamsRequestingIndex(sourceNodeId, index);
|
||||
|
||||
270
src/local_modules/jspack/jspack.js
Normal file
270
src/local_modules/jspack/jspack.js
Normal file
@@ -0,0 +1,270 @@
|
||||
/*!
|
||||
* Copyright © 2008 Fair Oaks Labs, Inc.
|
||||
* All rights reserved.
|
||||
*/
|
||||
|
||||
// Utility object: Encode/Decode C-style binary primitives to/from octet arrays
|
||||
function JSPack()
|
||||
{
|
||||
// Module-level (private) variables
|
||||
var el, bBE = false, m = this;
|
||||
|
||||
|
||||
// Raw byte arrays
|
||||
m._DeArray = function (a, p, l)
|
||||
{
|
||||
return [a.slice(p,p+l)];
|
||||
};
|
||||
m._EnArray = function (a, p, l, v)
|
||||
{
|
||||
for (var i = 0; i < l; a[p+i] = v[i]?v[i]:0, i++);
|
||||
};
|
||||
|
||||
// ASCII characters
|
||||
m._DeChar = function (a, p)
|
||||
{
|
||||
return String.fromCharCode(a[p]);
|
||||
};
|
||||
m._EnChar = function (a, p, v)
|
||||
{
|
||||
a[p] = v.charCodeAt(0);
|
||||
};
|
||||
|
||||
// Little-endian (un)signed N-byte integers
|
||||
m._DeInt = function (a, p)
|
||||
{
|
||||
var lsb = bBE?(el.len-1):0, nsb = bBE?-1:1, stop = lsb+nsb*el.len, rv, i, f;
|
||||
for (rv = 0, i = lsb, f = 1; i != stop; rv+=(a[p+i]*f), i+=nsb, f*=256);
|
||||
if (el.bSigned && (rv & Math.pow(2, el.len*8-1))) { rv -= Math.pow(2, el.len*8); }
|
||||
return rv;
|
||||
};
|
||||
m._EnInt = function (a, p, v)
|
||||
{
|
||||
var lsb = bBE?(el.len-1):0, nsb = bBE?-1:1, stop = lsb+nsb*el.len, i;
|
||||
v = (v<el.min)?el.min:(v>el.max)?el.max:v;
|
||||
for (i = lsb; i != stop; a[p+i]=v&0xff, i+=nsb, v>>=8);
|
||||
};
|
||||
|
||||
// ASCII character strings
|
||||
m._DeString = function (a, p, l)
|
||||
{
|
||||
for (var rv = new Array(l), i = 0; i < l; rv[i] = String.fromCharCode(a[p+i]), i++);
|
||||
return rv.join('');
|
||||
};
|
||||
m._EnString = function (a, p, l, v)
|
||||
{
|
||||
for (var t, i = 0; i < l; a[p+i] = (t=v.charCodeAt(i))?t:0, i++);
|
||||
};
|
||||
|
||||
// Little-endian N-bit IEEE 754 floating point
|
||||
m._De754 = function (a, p)
|
||||
{
|
||||
var s, e, m, i, d, nBits, mLen, eLen, eBias, eMax;
|
||||
mLen = el.mLen, eLen = el.len*8-el.mLen-1, eMax = (1<<eLen)-1, eBias = eMax>>1;
|
||||
|
||||
i = bBE?0:(el.len-1); d = bBE?1:-1; s = a[p+i]; i+=d; nBits = -7;
|
||||
for (e = s&((1<<(-nBits))-1), s>>=(-nBits), nBits += eLen; nBits > 0; e=e*256+a[p+i], i+=d, nBits-=8);
|
||||
for (m = e&((1<<(-nBits))-1), e>>=(-nBits), nBits += mLen; nBits > 0; m=m*256+a[p+i], i+=d, nBits-=8);
|
||||
|
||||
switch (e)
|
||||
{
|
||||
case 0:
|
||||
// Zero, or denormalized number
|
||||
e = 1-eBias;
|
||||
break;
|
||||
case eMax:
|
||||
// NaN, or +/-Infinity
|
||||
return m?NaN:((s?-1:1)*Infinity);
|
||||
default:
|
||||
// Normalized number
|
||||
m = m + Math.pow(2, mLen);
|
||||
e = e - eBias;
|
||||
break;
|
||||
}
|
||||
return (s?-1:1) * m * Math.pow(2, e-mLen);
|
||||
};
|
||||
m._En754 = function (a, p, v)
|
||||
{
|
||||
var s, e, m, i, d, c, mLen, eLen, eBias, eMax;
|
||||
mLen = el.mLen, eLen = el.len*8-el.mLen-1, eMax = (1<<eLen)-1, eBias = eMax>>1;
|
||||
|
||||
s = v<0?1:0;
|
||||
v = Math.abs(v);
|
||||
if (isNaN(v) || (v == Infinity))
|
||||
{
|
||||
m = isNaN(v)?1:0;
|
||||
e = eMax;
|
||||
}
|
||||
else
|
||||
{
|
||||
e = Math.floor(Math.log(v)/Math.LN2); // Calculate log2 of the value
|
||||
if (v*(c = Math.pow(2, -e)) < 1) { e--; c*=2; } // Math.log() isn't 100% reliable
|
||||
|
||||
// Round by adding 1/2 the significand's LSD
|
||||
if (e+eBias >= 1) { v += el.rt/c; } // Normalized: mLen significand digits
|
||||
else { v += el.rt*Math.pow(2, 1-eBias); } // Denormalized: <= mLen significand digits
|
||||
if (v*c >= 2) { e++; c/=2; } // Rounding can increment the exponent
|
||||
|
||||
if (e+eBias >= eMax)
|
||||
{
|
||||
// Overflow
|
||||
m = 0;
|
||||
e = eMax;
|
||||
}
|
||||
else if (e+eBias >= 1)
|
||||
{
|
||||
// Normalized - term order matters, as Math.pow(2, 52-e) and v*Math.pow(2, 52) can overflow
|
||||
m = (v*c-1)*Math.pow(2, mLen);
|
||||
e = e + eBias;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Denormalized - also catches the '0' case, somewhat by chance
|
||||
m = v*Math.pow(2, eBias-1)*Math.pow(2, mLen);
|
||||
e = 0;
|
||||
}
|
||||
}
|
||||
|
||||
for (i = bBE?(el.len-1):0, d=bBE?-1:1; mLen >= 8; a[p+i]=m&0xff, i+=d, m/=256, mLen-=8);
|
||||
for (e=(e<<mLen)|m, eLen+=mLen; eLen > 0; a[p+i]=e&0xff, i+=d, e/=256, eLen-=8);
|
||||
a[p+i-d] |= s*128;
|
||||
};
|
||||
|
||||
// Convert int64 to array with 3 elements: [lowBits, highBits, unsignedFlag]
|
||||
// '>>>' trick to convert signed 32bit int to unsigned int (because << always results in a signed 32bit int)
|
||||
m._DeInt64 = function (a, p) {
|
||||
var start = bBE ? 0 : 7, nsb = bBE ? 1 : -1, stop = start + nsb * 8, rv = [0,0, !el.bSigned], i, f, rvi;
|
||||
for (i = start, rvi = 1, f = 0;
|
||||
i != stop;
|
||||
rv[rvi] = (((rv[rvi]<<8)>>>0) + a[p + i]), i += nsb, f++, rvi = (f < 4 ? 1 : 0));
|
||||
return rv;
|
||||
};
|
||||
m._EnInt64 = function (a, p, v) {
|
||||
var start = bBE ? 0 : 7, nsb = bBE ? 1 : -1, stop = start + nsb * 8, i, f, rvi, s;
|
||||
for (i = start, rvi = 1, f = 0, s = 24;
|
||||
i != stop;
|
||||
a[p + i] = v[rvi]>>s & 0xff, i += nsb, f++, rvi = (f < 4 ? 1 : 0), s = 24 - (8 * (f % 4)));
|
||||
};
|
||||
|
||||
|
||||
// Class data
|
||||
m._sPattern = '(\\d+)?([AxcbBhHsfdiIlLqQ])';
|
||||
m._lenLut = {'A':1, 'x':1, 'c':1, 'b':1, 'B':1, 'h':2, 'H':2, 's':1, 'f':4, 'd':8, 'i':4, 'I':4, 'l':4, 'L':4, 'q':8, 'Q':8};
|
||||
m._elLut = { 'A': {en:m._EnArray, de:m._DeArray},
|
||||
's': {en:m._EnString, de:m._DeString},
|
||||
'c': {en:m._EnChar, de:m._DeChar},
|
||||
'b': {en:m._EnInt, de:m._DeInt, len:1, bSigned:true, min:-Math.pow(2, 7), max:Math.pow(2, 7)-1},
|
||||
'B': {en:m._EnInt, de:m._DeInt, len:1, bSigned:false, min:0, max:Math.pow(2, 8)-1},
|
||||
'h': {en:m._EnInt, de:m._DeInt, len:2, bSigned:true, min:-Math.pow(2, 15), max:Math.pow(2, 15)-1},
|
||||
'H': {en:m._EnInt, de:m._DeInt, len:2, bSigned:false, min:0, max:Math.pow(2, 16)-1},
|
||||
'i': {en:m._EnInt, de:m._DeInt, len:4, bSigned:true, min:-Math.pow(2, 31), max:Math.pow(2, 31)-1},
|
||||
'I': {en:m._EnInt, de:m._DeInt, len:4, bSigned:false, min:0, max:Math.pow(2, 32)-1},
|
||||
'l': {en:m._EnInt, de:m._DeInt, len:4, bSigned:true, min:-Math.pow(2, 31), max:Math.pow(2, 31)-1},
|
||||
'L': {en:m._EnInt, de:m._DeInt, len:4, bSigned:false, min:0, max:Math.pow(2, 32)-1},
|
||||
'f': {en:m._En754, de:m._De754, len:4, mLen:23, rt:Math.pow(2, -24)-Math.pow(2, -77)},
|
||||
'd': {en:m._En754, de:m._De754, len:8, mLen:52, rt:0},
|
||||
'q': {en:m._EnInt64, de:m._DeInt64, bSigned:true},
|
||||
'Q': {en:m._EnInt64, de:m._DeInt64, bSigned:false}};
|
||||
|
||||
// Unpack a series of n elements of size s from array a at offset p with fxn
|
||||
m._UnpackSeries = function (n, s, a, p)
|
||||
{
|
||||
for (var fxn = el.de, rv = [], i = 0; i < n; rv.push(fxn(a, p+i*s)), i++);
|
||||
return rv;
|
||||
};
|
||||
|
||||
// Pack a series of n elements of size s from array v at offset i to array a at offset p with fxn
|
||||
m._PackSeries = function (n, s, a, p, v, i)
|
||||
{
|
||||
for (var fxn = el.en, o = 0; o < n; fxn(a, p+o*s, v[i+o]), o++);
|
||||
};
|
||||
|
||||
// Unpack the octet array a, beginning at offset p, according to the fmt string
|
||||
m.Unpack = function (fmt, a, p)
|
||||
{
|
||||
// Set the private bBE flag based on the format string - assume big-endianness
|
||||
bBE = (fmt.charAt(0) != '<');
|
||||
|
||||
p = p?p:0;
|
||||
var re = new RegExp(this._sPattern, 'g'), m, n, s, rv = [];
|
||||
while (m = re.exec(fmt))
|
||||
{
|
||||
n = ((m[1]==undefined)||(m[1]==''))?1:parseInt(m[1]);
|
||||
s = this._lenLut[m[2]];
|
||||
if ((p + n*s) > a.length)
|
||||
{
|
||||
return undefined;
|
||||
}
|
||||
switch (m[2])
|
||||
{
|
||||
case 'A': case 's':
|
||||
rv.push(this._elLut[m[2]].de(a, p, n));
|
||||
break;
|
||||
case 'c': case 'b': case 'B': case 'h': case 'H':
|
||||
case 'i': case 'I': case 'l': case 'L': case 'f': case 'd': case 'q': case 'Q':
|
||||
el = this._elLut[m[2]];
|
||||
rv.push(this._UnpackSeries(n, s, a, p));
|
||||
break;
|
||||
}
|
||||
p += n*s;
|
||||
}
|
||||
return Array.prototype.concat.apply([], rv);
|
||||
};
|
||||
|
||||
// Pack the supplied values into the octet array a, beginning at offset p, according to the fmt string
|
||||
m.PackTo = function (fmt, a, p, values)
|
||||
{
|
||||
// Set the private bBE flag based on the format string - assume big-endianness
|
||||
bBE = (fmt.charAt(0) != '<');
|
||||
|
||||
var re = new RegExp(this._sPattern, 'g'), m, n, s, i = 0, j;
|
||||
while (m = re.exec(fmt))
|
||||
{
|
||||
n = ((m[1]==undefined)||(m[1]==''))?1:parseInt(m[1]);
|
||||
s = this._lenLut[m[2]];
|
||||
if ((p + n*s) > a.length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
switch (m[2])
|
||||
{
|
||||
case 'A': case 's':
|
||||
if ((i + 1) > values.length) { return false; }
|
||||
this._elLut[m[2]].en(a, p, n, values[i]);
|
||||
i += 1;
|
||||
break;
|
||||
case 'c': case 'b': case 'B': case 'h': case 'H':
|
||||
case 'i': case 'I': case 'l': case 'L': case 'f': case 'd': case 'q': case 'Q':
|
||||
el = this._elLut[m[2]];
|
||||
if ((i + n) > values.length) { return false; }
|
||||
this._PackSeries(n, s, a, p, values, i);
|
||||
i += n;
|
||||
break;
|
||||
case 'x':
|
||||
for (j = 0; j < n; j++) { a[p+j] = 0; }
|
||||
break;
|
||||
}
|
||||
p += n*s;
|
||||
}
|
||||
return a;
|
||||
};
|
||||
|
||||
// Pack the supplied values into a new octet array, according to the fmt string
|
||||
m.Pack = function (fmt, values)
|
||||
{
|
||||
return this.PackTo(fmt, new Array(this.CalcLength(fmt)), 0, values);
|
||||
};
|
||||
|
||||
// Determine the number of bytes represented by the format string
|
||||
m.CalcLength = function (fmt)
|
||||
{
|
||||
var re = new RegExp(this._sPattern, 'g'), m, sum = 0;
|
||||
while (m = re.exec(fmt))
|
||||
{
|
||||
sum += (((m[1]==undefined)||(m[1]==''))?1:parseInt(m[1])) * this._lenLut[m[2]];
|
||||
}
|
||||
return sum;
|
||||
};
|
||||
};
|
||||
|
||||
exports.jspack = new JSPack();
|
||||
9516
src/mavlink.js
9516
src/mavlink.js
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,9 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import WebSocketClient from './ws_client';
|
||||
import WebSerial from './web_serial';
|
||||
import { mavlink20, MAVLink20Processor } from './mavlink';
|
||||
import SlcanCodec from './slcan';
|
||||
import dronecan from './dronecan';
|
||||
import './mavlink';
|
||||
|
||||
class MavlinkSession extends EventEmitter {
|
||||
constructor() {
|
||||
@@ -12,6 +13,8 @@ class MavlinkSession extends EventEmitter {
|
||||
this.mavlinkProcessor = new MAVLink20Processor(null, this.targetSystem, this.targetComponent);
|
||||
this.wsClient = null;
|
||||
this.serial = null;
|
||||
this.serialProtocol = 'mavlink';
|
||||
this.slcanCodec = null;
|
||||
this.parseBuffer = this.parseBuffer.bind(this);
|
||||
}
|
||||
|
||||
@@ -27,8 +30,20 @@ class MavlinkSession extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
initWebSocketConnection(ip, port) {
|
||||
this.wsClient = new WebSocketClient(`ws://${ip}:${port}`);
|
||||
initWebSocketConnection(ip, port, mavlinkSigning='') {
|
||||
this.serialProtocol = 'mavlink';
|
||||
this.slcanCodec = null;
|
||||
|
||||
if (mavlinkSigning) {
|
||||
const enc = new TextEncoder();
|
||||
const data = enc.encode(mavlinkSigning);
|
||||
const hash = mavlink20.sha256(data);
|
||||
this.mavlinkProcessor.signing.secret_key = new Uint8Array(hash);
|
||||
this.mavlinkProcessor.signing.sign_outgoing = true;
|
||||
this.wsClient = new WebSocketClient(`wss://${ip}:${port}`);
|
||||
} else {
|
||||
this.wsClient = new WebSocketClient(`ws://${ip}:${port}`);
|
||||
}
|
||||
this.mavlinkProcessor.file = this.wsClient;
|
||||
|
||||
this.wsClient.addMessageHandler((buffer) => {
|
||||
@@ -40,13 +55,21 @@ class MavlinkSession extends EventEmitter {
|
||||
this.wsClient.connect();
|
||||
}
|
||||
|
||||
initWebSerialConnection(port, baudRate) {
|
||||
initWebSerialConnection(port, baudRate, options = {}) {
|
||||
this.serial = new WebSerial(port, baudRate);
|
||||
this.mavlinkProcessor.file = this.serial;
|
||||
this.serialProtocol = options.protocol || 'mavlink';
|
||||
this.slcanCodec = this.serialProtocol === 'slcan' ? new SlcanCodec() : null;
|
||||
|
||||
if (this.serialProtocol === 'mavlink') {
|
||||
this.mavlinkProcessor.file = this.serial;
|
||||
}
|
||||
|
||||
this.serial.addMessageHandler((buffer) => {
|
||||
// console.log('Received buffer:', buffer);
|
||||
this.parseBuffer(buffer);
|
||||
if (this.serialProtocol === 'slcan') {
|
||||
this.parseSlcanBuffer(buffer);
|
||||
} else {
|
||||
this.parseBuffer(buffer);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -63,7 +86,11 @@ class MavlinkSession extends EventEmitter {
|
||||
}
|
||||
|
||||
webSerialConnect() {
|
||||
this.serial.connect();
|
||||
this.serial.connect().then(() => {
|
||||
if (this.serialProtocol === 'slcan') {
|
||||
this.openSlcanChannel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
close() {
|
||||
@@ -75,6 +102,17 @@ class MavlinkSession extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
openSlcanChannel() {
|
||||
if (!this.serial || this.serialProtocol !== 'slcan') return;
|
||||
|
||||
console.log('SLCAN: opening CAN channel');
|
||||
setTimeout(() => {
|
||||
if (this.serial && this.serial.connected) {
|
||||
this.serial.write('O\r');
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
parseBuffer(buffer) {
|
||||
// console.log('Parsing buffer:', buffer);
|
||||
const messages = this.mavlinkProcessor.parseBuffer(buffer);
|
||||
@@ -85,6 +123,21 @@ class MavlinkSession extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
parseSlcanBuffer(buffer) {
|
||||
this.slcanCodec.feed(
|
||||
buffer,
|
||||
({ id, data, len }) => {
|
||||
console.log('SLCAN RX frame:', { id: id.toString(16), len, data });
|
||||
if (typeof localNode !== 'undefined' && localNode) {
|
||||
localNode.emit('can-frame', (id | dronecan.TransferManager.FlagEFF) >>> 0, data, len);
|
||||
}
|
||||
if (this.listenerCount('mav-rx') > 0) {
|
||||
this.emit('mav-rx', { protocol: 'slcan', id, data, len });
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
handleMavlinkMsg(message) {
|
||||
switch (message._id) {
|
||||
case mavlink20.MAVLINK_MSG_ID_HEARTBEAT:
|
||||
@@ -110,21 +163,49 @@ class MavlinkSession extends EventEmitter {
|
||||
default:
|
||||
}
|
||||
|
||||
if (this.getMaxListeners('mav-rx') > 0) {
|
||||
if (this.listenerCount('mav-rx') > 0) {
|
||||
this.emit('mav-rx', message);
|
||||
}
|
||||
}
|
||||
|
||||
sendMavlinkMsg(msg) {
|
||||
if (this.serialProtocol === 'slcan') return;
|
||||
|
||||
if ((this.wsClient && this.wsClient.connected) || (this.serial && this.serial.connected)) {
|
||||
this.mavlinkProcessor.send(msg);
|
||||
if (this.getMaxListeners('mav-tx') > 0) {
|
||||
if (this.listenerCount('mav-tx') > 0) {
|
||||
this.emit('mav-tx', msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sendCanFrame(bus, messageId, data, len) {
|
||||
if (this.serialProtocol === 'slcan') {
|
||||
if (this.serial && this.serial.connected && this.slcanCodec) {
|
||||
const line = this.slcanCodec.encodeExtendedFrame(messageId, data, len);
|
||||
console.log('SLCAN TX frame:', line.trim());
|
||||
this.serial.write(line);
|
||||
if (this.listenerCount('mav-tx') > 0) {
|
||||
this.emit('mav-tx', { protocol: 'slcan', id: messageId, data, len });
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const msg = new mavlink20.messages.can_frame(
|
||||
this.targetSystem,
|
||||
this.targetComponent,
|
||||
bus,
|
||||
len,
|
||||
messageId,
|
||||
data.toString('binary')
|
||||
);
|
||||
this.sendMavlinkMsg(msg);
|
||||
}
|
||||
|
||||
enableMavlinkCanForward(bus) {
|
||||
if (this.serialProtocol === 'slcan') return;
|
||||
|
||||
// console.log('Enabling CAN forward on bus:', bus);
|
||||
const msg = new mavlink20.messages.command_long(
|
||||
this.targetSystem, // target_system
|
||||
|
||||
132
src/slcan.js
Normal file
132
src/slcan.js
Normal file
@@ -0,0 +1,132 @@
|
||||
class SlcanCodec {
|
||||
constructor() {
|
||||
this.buffer = '';
|
||||
this.decoder = new TextDecoder('ascii');
|
||||
}
|
||||
|
||||
feed(chunk, onFrame, onError) {
|
||||
const text = this.decoder.decode(chunk, { stream: true });
|
||||
if (text.includes('\x07')) {
|
||||
console.warn('SLCAN adapter returned NACK');
|
||||
}
|
||||
this.buffer += text.replace(/\x07/g, '');
|
||||
this.parseBuffer(onFrame);
|
||||
}
|
||||
|
||||
parseBuffer(onFrame) {
|
||||
while (this.buffer.length > 0) {
|
||||
this.buffer = this.buffer.replace(/^[\r\n]+/, '');
|
||||
if (!this.buffer) return;
|
||||
|
||||
const frameStart = this.buffer.search(/[TtRr]/);
|
||||
if (frameStart === -1) {
|
||||
this.buffer = '';
|
||||
return;
|
||||
}
|
||||
if (frameStart > 0) {
|
||||
this.buffer = this.buffer.slice(frameStart);
|
||||
}
|
||||
|
||||
const type = this.buffer[0];
|
||||
if (type === 'T') {
|
||||
if (this.buffer.length < 10) return;
|
||||
const dlcText = this.buffer[9];
|
||||
if (!/^[0-8]$/.test(dlcText)) {
|
||||
console.warn(`Malformed SLCAN frame header: ${this.buffer.slice(0, 10)}`);
|
||||
this.buffer = this.buffer.slice(1);
|
||||
continue;
|
||||
}
|
||||
|
||||
const dlc = parseInt(dlcText, 16);
|
||||
const frameLength = 10 + dlc * 2;
|
||||
if (this.buffer.length < frameLength) return;
|
||||
|
||||
const line = this.buffer.slice(0, frameLength);
|
||||
this.buffer = this.buffer.slice(frameLength).replace(/^[\r\n]+/, '');
|
||||
this.parseLine(line, onFrame);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (type === 't') {
|
||||
if (this.buffer.length < 5) return;
|
||||
const dlcText = this.buffer[4];
|
||||
if (!/^[0-8]$/.test(dlcText)) {
|
||||
this.buffer = this.buffer.slice(1);
|
||||
continue;
|
||||
}
|
||||
const frameLength = 5 + parseInt(dlcText, 16) * 2;
|
||||
if (this.buffer.length < frameLength) return;
|
||||
this.buffer = this.buffer.slice(frameLength).replace(/^[\r\n]+/, '');
|
||||
continue;
|
||||
}
|
||||
|
||||
const lineEndIndex = this.findLineEnd();
|
||||
if (lineEndIndex === -1) return;
|
||||
this.buffer = this.buffer.slice(lineEndIndex + 1);
|
||||
}
|
||||
}
|
||||
|
||||
findLineEnd() {
|
||||
const crIndex = this.buffer.indexOf('\r');
|
||||
const lfIndex = this.buffer.indexOf('\n');
|
||||
|
||||
if (crIndex === -1) return lfIndex;
|
||||
if (lfIndex === -1) return crIndex;
|
||||
return Math.min(crIndex, lfIndex);
|
||||
}
|
||||
|
||||
parseLine(line, onFrame) {
|
||||
if (!line) return;
|
||||
|
||||
const type = line[0];
|
||||
if (type === 't' || type === 'r' || type === 'R') return;
|
||||
if (type !== 'T') return;
|
||||
|
||||
if (line.length < 10) {
|
||||
console.warn(`Malformed SLCAN frame: ${line}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const idText = line.slice(1, 9);
|
||||
const dlcText = line[9];
|
||||
if (!/^[0-9a-fA-F]{8}$/.test(idText) || !/^[0-8]$/.test(dlcText)) {
|
||||
console.warn(`Malformed SLCAN frame: ${line}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const dlc = parseInt(dlcText, 16);
|
||||
const dataText = line.slice(10, 10 + dlc * 2);
|
||||
if (dataText.length !== dlc * 2 || !/^[0-9a-fA-F]*$/.test(dataText)) {
|
||||
console.warn(`Malformed SLCAN payload: ${line}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = new Uint8Array(dlc);
|
||||
for (let i = 0; i < dlc; i++) {
|
||||
data[i] = parseInt(dataText.slice(i * 2, i * 2 + 2), 16);
|
||||
}
|
||||
|
||||
onFrame?.({
|
||||
id: parseInt(idText, 16) & 0x1FFFFFFF,
|
||||
data,
|
||||
len: dlc,
|
||||
});
|
||||
}
|
||||
|
||||
encodeExtendedFrame(messageId, data, len) {
|
||||
const canId = messageId & 0x1FFFFFFF;
|
||||
const frameLength = Math.min(len, data.length ?? len);
|
||||
if (frameLength > 8) {
|
||||
throw new Error('SLCAN classic CAN frames support a maximum DLC of 8');
|
||||
}
|
||||
|
||||
let dataHex = '';
|
||||
for (let i = 0; i < frameLength; i++) {
|
||||
dataHex += data[i].toString(16).toUpperCase().padStart(2, '0');
|
||||
}
|
||||
|
||||
return `T${canId.toString(16).toUpperCase().padStart(8, '0')}${frameLength.toString(16).toUpperCase()}${dataHex}\r`;
|
||||
}
|
||||
}
|
||||
|
||||
export default SlcanCodec;
|
||||
@@ -12,6 +12,7 @@ class WebSocketClient {
|
||||
connect() {
|
||||
// Reset connected status before attempting a new connection
|
||||
this.connected = false;
|
||||
console.log(`Connecting to WebSocket at ${this.url}`);
|
||||
this.socket = new WebSocket(this.url);
|
||||
|
||||
this.socket.addEventListener('open', (event) => {
|
||||
|
||||
Reference in New Issue
Block a user