From 7bffe34f544fef5edefc94bd5115b26f2160751e Mon Sep 17 00:00:00 2001 From: Huibean Date: Thu, 14 Aug 2025 09:56:58 +0800 Subject: [PATCH] add mavlink sigining and wss support --- .gitignore | 3 +- package-lock.json | 226 +- package.json | 3 +- src/ConnectionSettingsModal.js | 157 +- src/local_modules/jspack/jspack.js | 270 + src/mavlink.js | 9516 ++++------------------------ src/mavlink_session.js | 15 +- src/ws_client.js | 1 + 8 files changed, 1816 insertions(+), 8375 deletions(-) create mode 100644 src/local_modules/jspack/jspack.js diff --git a/.gitignore b/.gitignore index f80a3a3..efd1aac 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ dist .DS_Store mav.parm mav.tlog -mav.tlog.raw \ No newline at end of file +mav.tlog.raw +deploy.sh \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 73d4ac5..7b1d02e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 6b58aec..17e0b36 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/ConnectionSettingsModal.js b/src/ConnectionSettingsModal.js index d542a1f..54c39a0 100644 --- a/src/ConnectionSettingsModal.js +++ b/src/ConnectionSettingsModal.js @@ -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 @@ -74,6 +76,11 @@ 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); @@ -88,11 +95,13 @@ const ConnectionSettingsModal = ({ const [activeConnection, setActiveConnection] = useState(null); // null, 'serial', or 'websocket' // 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); @@ -315,34 +324,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 +379,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,54 +403,48 @@ const ConnectionSettingsModal = ({ if (activeConnection === 'websocket') { // Force close the connection window.mavlinkSession.close(); - - // Always update the UI state regardless of actual connection state setActiveConnection(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'); onConnectionStatusChange(true); @@ -453,20 +454,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 +472,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); } }; @@ -650,8 +642,8 @@ const ConnectionSettingsModal = ({ disabled={activeConnection !== null} size="small" sx={{ flex: 3 }} - error={!!ipError} - helperText={ipError} + error={!!hostError} + helperText={hostError} /> - + @@ -691,7 +683,7 @@ const ConnectionSettingsModal = ({ - {/* Bus Selection */} + {/* Bus Selection and Mavlink Signing */} + + {showMavlinkSigning ? : } + + ), + }} + /> diff --git a/src/local_modules/jspack/jspack.js b/src/local_modules/jspack/jspack.js new file mode 100644 index 0000000..8f4c51f --- /dev/null +++ b/src/local_modules/jspack/jspack.js @@ -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 = (vel.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<>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<>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< 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(); diff --git a/src/mavlink.js b/src/mavlink.js index 4a20045..8d756b4 100644 --- a/src/mavlink.js +++ b/src/mavlink.js @@ -1,24 +1,84 @@ /* MAVLink protocol implementation for node.js (auto-generated by mavgen_javascript.py) -Generated from: all.xml,ardupilotmega.xml,ASLUAV.xml,common.xml,development.xml,icarous.xml,minimal.xml,python_array_test.xml,standard.xml,test.xml,ualberta.xml,uAvionix.xml,storm32.xml,AVSSUAS.xml,cubepilot.xml,csAirLink.xml,loweheiser.xml +Generated from: common.xml,standard.xml,minimal.xml Note: this file has been auto-generated. DO NOT EDIT */ -jspack = require("jspack").jspack, - _ = require("underscore"), - events = require("events"), // for .emit(..), MAVLink20Processor inherits from events.EventEmitter - util = require("util"); +// Detect environment +const isNode = typeof process !== 'undefined' && + process.versions && + process.versions.node; -var Buffer = require('buffer').Buffer; // required in react - no impact in node -var Long = require('long'); - -// Add a convenience method to Buffer -Buffer.prototype.toByteArray = function () { - return Array.prototype.slice.call(this, 0) +// Handle jspack dependency +let jspack; +if (isNode) { + jspack = (global && global.jspack) || require("jspack").jspack; +} else { + // For browser environments (including webpack), try require first, fallback to dynamic import + try { + jspack = require("jspack").jspack; + } catch (e) { + import("./local_modules/jspack/jspack.js").then((mod) => { + jspack = new mod.default() + }).catch((err) => { + console.error("jspack not found"); + }); + } } +// Handle Node.js specific modules +let events, util; +if (isNode) { + events = require("events"); + util = require("util"); +} else { + // Browser polyfills for Node.js modules + util = { + inherits: function(constructor, superConstructor) { + constructor.prototype = Object.create(superConstructor.prototype); + constructor.prototype.constructor = constructor; + } + }; + + // Simple EventEmitter polyfill for browsers + events = { + EventEmitter: function() { + this._events = {}; + + this.on = function(event, listener) { + if (!this._events[event]) { + this._events[event] = []; + } + this._events[event].push(listener); + }; + + this.emit = function(event, ...args) { + if (this._events[event]) { + this._events[event].forEach(listener => { + try { + listener.apply(this, args); + } catch (e) { + console.error('Error in event listener:', e); + } + }); + } + }; + + this.removeListener = function(event, listener) { + if (this._events[event]) { + const index = this._events[event].indexOf(listener); + if (index > -1) { + this._events[event].splice(index, 1); + } + } + }; + } + }; +} + + mavlink20 = function(){}; // Implement the CRC-16/MCRF4XX function (present in the Python version through the mavutil.py package) @@ -26,7 +86,7 @@ mavlink20.x25Crc = function(buffer, crcIN) { var bytes = buffer; var crcOUT = crcIN === undefined ? 0xffff : crcIN; - _.each(bytes, function(e) { + bytes.forEach(function(e) { var tmp = e ^ (crcOUT & 0xff); tmp = (tmp ^ (tmp << 4)) & 0xff; crcOUT = (crcOUT >> 8) ^ (tmp << 8) ^ (tmp << 3) ^ (tmp >> 4); @@ -59,7 +119,7 @@ mavlink20.MAVLINK_IFLAG_SIGNED = 0x01 mavlink20.MAVLINK_SIGNATURE_BLOCK_LEN = 13 // Mavlink headers incorporate sequence, source system (platform) and source component. -mavlink20.header = function(msgId, mlen, seq, srcSystem, srcComponent, incompat_flags=0, compat_flags=0,) { +mavlink20.header = function(msgId, mlen, seq, srcSystem, srcComponent, incompat_flags=0, compat_flags=0) { this.mlen = ( typeof mlen === 'undefined' ) ? 0 : mlen; this.seq = ( typeof seq === 'undefined' ) ? 0 : seq; @@ -73,57 +133,161 @@ mavlink20.header = function(msgId, mlen, seq, srcSystem, srcComponent, incompat_ mavlink20.header.prototype.pack = function() { return jspack.Pack('BBBBBBBHB', [253, this.mlen, this.incompat_flags, this.compat_flags, this.seq, this.srcSystem, this.srcComponent, ((this.msgId & 0xFF) << 8) | ((this.msgId >> 8) & 0xFF), this.msgId>>16]); } - + // Base class declaration: mavlink.message will be the parent class for each // concrete implementation in mavlink.messages. mavlink20.message = function() {}; // Convenience setter to facilitate turning the unpacked array of data into member properties mavlink20.message.prototype.set = function(args,verbose) { -// inspect - _.each(this.fieldnames, function(e, i) { + // inspect + this.fieldnames.forEach(function(e, i) { var num = parseInt(i,10); if (this.hasOwnProperty(e) && isNaN(num) ){ // asking for an attribute that's non-numeric is ok unless its already an attribute we have if ( verbose >= 1) { console.log("WARNING, overwriting an existing property is DANGEROUS:"+e+" ==>"+i+"==>"+args[i]+" -> "+JSON.stringify(this)); } } }, this); - //console.log(this.fieldnames); -// then modify - _.each(this.fieldnames, function(e, i) { + + // then modify + this.fieldnames.forEach(function(e, i) { this[e] = args[i]; }, this); }; -// trying to be the same-ish as the python function of the same name +/* + sha256 implementation + embedded to avoid async issues in web browsers with crypto library + with thanks to https://geraintluff.github.io/sha256/ +*/ +mavlink20.sha256 = function(inputBytes) { + const K = new Uint32Array([ + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, + 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, + 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, + 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, + 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, + 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, + 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, + 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, + 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, + 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, + 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, + 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 + ]); + + function ROTR(n, x) { return (x >>> n) | (x << (32 - n)); } + + function Σ0(x) { return ROTR(2, x) ^ ROTR(13, x) ^ ROTR(22, x); } + function Σ1(x) { return ROTR(6, x) ^ ROTR(11, x) ^ ROTR(25, x); } + function σ0(x) { return ROTR(7, x) ^ ROTR(18, x) ^ (x >>> 3); } + function σ1(x) { return ROTR(17, x) ^ ROTR(19, x) ^ (x >>> 10); } + + function Ch(x, y, z) { return (x & y) ^ (~x & z); } + function Maj(x, y, z) { return (x & y) ^ (x & z) ^ (y & z); } + + const H = new Uint32Array([ + 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, + 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19 + ]); + + const l = inputBytes.length; + const bitLen = l * 8; + + const withPadding = new Uint8Array(((l + 9 + 63) >> 6) << 6); // pad to multiple of 64 bytes + withPadding.set(inputBytes); + withPadding[l] = 0x80; + withPadding.set([ + 0, 0, 0, 0, + (bitLen >>> 24) & 0xff, + (bitLen >>> 16) & 0xff, + (bitLen >>> 8) & 0xff, + bitLen & 0xff + ], withPadding.length - 8); + + const w = new Uint32Array(64); + for (let i = 0; i < withPadding.length; i += 64) { + for (let j = 0; j < 16; j++) { + w[j] = ( + (withPadding[i + 4 * j] << 24) | + (withPadding[i + 4 * j + 1] << 16) | + (withPadding[i + 4 * j + 2] << 8) | + (withPadding[i + 4 * j + 3]) + ) >>> 0; + } + for (let j = 16; j < 64; j++) { + w[j] = (σ1(w[j - 2]) + w[j - 7] + σ0(w[j - 15]) + w[j - 16]) >>> 0; + } + + let [a, b, c, d, e, f, g, h] = H; + + for (let j = 0; j < 64; j++) { + const T1 = (h + Σ1(e) + Ch(e, f, g) + K[j] + w[j]) >>> 0; + const T2 = (Σ0(a) + Maj(a, b, c)) >>> 0; + h = g; + g = f; + f = e; + e = (d + T1) >>> 0; + d = c; + c = b; + b = a; + a = (T1 + T2) >>> 0; + } + + H[0] = (H[0] + a) >>> 0; + H[1] = (H[1] + b) >>> 0; + H[2] = (H[2] + c) >>> 0; + H[3] = (H[3] + d) >>> 0; + H[4] = (H[4] + e) >>> 0; + H[5] = (H[5] + f) >>> 0; + H[6] = (H[6] + g) >>> 0; + H[7] = (H[7] + h) >>> 0; + } + + const output = new Uint8Array(32); + for (let i = 0; i < 8; i++) { + output[i * 4 + 0] = (H[i] >>> 24) & 0xff; + output[i * 4 + 1] = (H[i] >>> 16) & 0xff; + output[i * 4 + 2] = (H[i] >>> 8) & 0xff; + output[i * 4 + 3] = H[i] & 0xff; + } + + return output; +} + +// create a message signature +mavlink20.create_signature = function(key, msgbuf) { + const input = new Uint8Array(32 + msgbuf.length); + input.set(key, 0); + input.set(msgbuf, 32); + + const hash = mavlink20.sha256(input); + const sig = hash.slice(0, 6); + + return sig; +} + +// sign outgoing packet mavlink20.message.prototype.sign_packet = function( mav) { - var crypto= require('crypto'); - var h = crypto.createHash('sha256'); + function packUint48LE(value) { + const bytes = [] + for (let i = 0; i < 6; i++) { + bytes.push(Number((value >> BigInt(8 * i)) & 0xFFn)); + } + return bytes; + } - //mav.signing.timestamp is a 48bit number, or 6 bytes. + var tsbuf = packUint48LE(BigInt(mav.signing.timestamp)); - // due to js not being able to shift numbers more than 32, we'll use this instead.. - // js stores all its numbers as a 64bit float with 53 bits of mantissa, so have room for 48 ok. - // positive shifts left, negative shifts right - function shift(number, shift) { - return number * Math.pow(2, shift); - } - - var thigh = shift(mav.signing.timestamp,-32) // 2 bytes from the top, shifted right by 32 bits - var tlow = (mav.signing.timestamp & 0xfffffff ) // 4 bytes from the bottom - - // I means unsigned 4bytes, H means unsigned 2 bytes // first add the linkid(1 byte) and timestamp(6 bytes) that start the signature - this._msgbuf = this._msgbuf.concat(jspack.Pack(' MAV. Also used to -return a point from MAV -> GCS. - - target_system : System ID. (uint8_t) - target_component : Component ID. (uint8_t) - idx : Point index (first point is 1, 0 is for return point). (uint8_t) - count : Total number of points (for sanity checking). (uint8_t) - lat : Latitude of point. (float) - lng : Longitude of point. (float) - -*/ - mavlink20.messages.fence_point = function( ...moreargs ) { - [ this.target_system , this.target_component , this.idx , this.count , this.lat , this.lng ] = moreargs; - - this._format = ' MAV. Also used to -return a point from MAV -> GCS. - - target_system : System ID. (uint8_t) - target_component : Component ID. (uint8_t) - idx : Point index (first point is 0). (uint8_t) - count : Total number of points (for sanity checking). (uint8_t) - lat : Latitude of point. (int32_t) - lng : Longitude of point. (int32_t) - alt : Transit / loiter altitude relative to home. (int16_t) - break_alt : Break altitude relative to home. (int16_t) - land_dir : Heading to aim for when landing. (uint16_t) - flags : Configuration flags. (uint8_t) - -*/ - mavlink20.messages.rally_point = function( ...moreargs ) { - [ this.target_system , this.target_component , this.idx , this.count , this.lat , this.lng , this.alt , this.break_alt , this.land_dir , this.flags ] = moreargs; - - this._format = '= 1 && this.buf[0] != this.protocol_marker ) { + if( this.buf.length >= 1 && + this.buf[0] != mavlink20.PROTOCOL_MARKER_V2 && + this.buf[0] != mavlink20.PROTOCOL_MARKER_V1) { - // Strip the offending initial byte and throw an error. + // Strip the offending initial bytes and throw an error. var badPrefix = this.buf[0]; - this.bufInError = this.buf.slice(0,1); - this.buf = this.buf.slice(1); + var idx1 = this.buf.indexOf(mavlink20.PROTOCOL_MARKER_V1); + var idx2 = this.buf.indexOf(mavlink20.PROTOCOL_MARKER_V2); + if (idx1 == -1) { + idx1 = idx2; + } + if (idx1 == -1 && idx2 == -1) { + this.bufInError = this.buf; + this.buf = new Uint8Array(); + } else { + this.bufInError = this.buf.slice(0,idx1); + this.buf = this.buf.slice(idx1); + } this.expected_length = mavlink20.HEADER_LEN; //initially we 'expect' at least the length of the header, later parseLength corrects for this. throw new Error("Bad prefix ("+badPrefix+")"); } @@ -20787,43 +13893,44 @@ MAVLink20Processor.prototype.parsePrefix = function() { // us know if we have signing enabled, which affects the real-world length by the signature-block length of 13 bytes. // once successful, 'this.expected_length' is correctly set for the whole packet. MAVLink20Processor.prototype.parseLength = function() { - - if( this.buf.length >= 3 ) { - var unpacked = jspack.Unpack('BBB', this.buf.slice(0, 3)); - var magic = unpacked[0]; // stx ie fd or fe etc - this.expected_length = unpacked[1] + mavlink20.HEADER_LEN + 2 // length of message + header + CRC (ie non-signed length) - this.incompat_flags = unpacked[2]; - // mavlink2 only.. in mavlink1, incompat_flags var above is actually the 'seq', but for this test its ok. - if ((magic == mavlink20.PROTOCOL_MARKER_V2 ) && ( this.incompat_flags & mavlink20.MAVLINK_IFLAG_SIGNED )){ - this.expected_length += mavlink20.MAVLINK_SIGNATURE_BLOCK_LEN; - } + + if( this.buf.length >= 3 ) { + var unpacked = jspack.Unpack('BBB', this.buf.slice(0, 3)); + var magic = unpacked[0]; // stx ie fd or fe etc + this.expected_length = unpacked[1] + mavlink20.HEADER_LEN + 2 // length of message + header + CRC (ie non-signed length) + this.incompat_flags = unpacked[2]; + // mavlink2 only.. in mavlink1, incompat_flags var above is actually the 'seq', but for this test its ok. + if ((magic == mavlink20.PROTOCOL_MARKER_V2 ) && ( this.incompat_flags & mavlink20.MAVLINK_IFLAG_SIGNED )){ + this.expected_length += mavlink20.MAVLINK_SIGNATURE_BLOCK_LEN; + } } } -// input some data bytes, possibly returning a new message - python equiv function is called parse_char / __parse_char_legacy +// input some data bytes, possibly returning a new message - python equiv function is called parse_char / __parse_char_legacy +// c can be null to process any remaining data in the input buffer from a previous call MAVLink20Processor.prototype.parseChar = function(c) { var m = null; try { - - this.pushBuffer(c); + if (c != null) { + this.pushBuffer(c); + } this.parsePrefix(); this.parseLength(); m = this.parsePayload(); } catch(e) { - this.log('error', e.message); this.total_receive_errors += 1; m = new mavlink20.messages.bad_data(this.bufInError, e.message); - this.bufInError = new Buffer.from([]); - + this.bufInError = new Uint8Array(); + } // emit a packet-specific message as well as a generic message, user/s can choose to use either or both of these. - if(null != m) { + if (isNode && null != m) { this.emit(m._name, m); this.emit('message', m); } @@ -20870,7 +13977,7 @@ MAVLink20Processor.prototype.parsePayload = function() { // input some data bytes, possibly returning an array of new messages MAVLink20Processor.prototype.parseBuffer = function(s) { - + // Get a message, if one is available in the stream. var m = this.parseChar(s); @@ -20878,13 +13985,14 @@ MAVLink20Processor.prototype.parseBuffer = function(s) { if ( null === m ) { return null; } - + // While more valid messages can be read from the existing buffer, add // them to the array of new messages and return them. - var ret = [m]; + var ret = []; + ret.push(m); while(true) { - m = this.parseChar(); - if ( null === m ) { + m = this.parseChar(null); + if ( null === m || m._id === mavlink20.MAVLINK_MSG_ID_BAD_DATA) { // No more messages left. return ret; } @@ -20893,122 +14001,86 @@ MAVLink20Processor.prototype.parseBuffer = function(s) { } -// from Buffer to ArrayBuffer -function toArrayBuffer(buf) { - var ab = new ArrayBuffer(buf.length); - var view = new Uint8Array(ab); - for (var i = 0; i < buf.length; ++i) { - view[i] = buf[i]; - } - return ab; -} -// and back -function toBuffer(ab) { - var buf = Buffer.alloc(ab.byteLength); - var view = new Uint8Array(ab); - for (var i = 0; i < buf.length; ++i) { - buf[i] = view[i]; - } - return buf; -} - //check signature on incoming message , many of the comments in this file come from the python impl -MAVLink20Processor.prototype.check_signature = function(msgbuf, srcSystem, srcComponent) { +MAVLink20Processor.prototype.check_signature = function(msgbuf, srcSystem, srcComponent) { + var timestamp_buf = msgbuf.slice(-12,-6); + + var link_id; + if (isNode) { + var link_id_buf = Buffer.from ? Buffer.from(msgbuf.slice(-13,-12)) : new Buffer(msgbuf.slice(-13,-12)); + link_id = link_id_buf[0]; // get the first byte. + } else { + // Browser-compatible buffer handling + link_id = msgbuf.slice(-13,-12)[0]; + } - //if (isinstance(msgbuf, array.array)){ - // msgbuf = msgbuf.tostring() - //} - if ( Buffer.isBuffer(msgbuf) ) { - msgbuf = toArrayBuffer(msgbuf); - } - - //timestamp_buf = msgbuf[-12:-6] - var timestamp_buf= msgbuf.slice(-12,-6); - - //link_id = msgbuf[-13] - var link_id= new Buffer.from(msgbuf.slice(-13,-12)); // just a single byte really, but returned as a buffer - link_id = link_id[0]; // get the first byte. - - //self.mav_sign_unpacker = jspack.Unpack('= 0; i--) { + value = (value << 8n) | BigInt(bytes[i]); + } + return value; + } + var timestamp = Number(unpackUint48LE(timestamp_buf)); - // I means unsigned 4bytes, H means unsigned 2 bytes - var t = jspack.Unpack(' val === sigpart[index]); + } + if (!signaturesMatch) { + return false; + } + //# the timestamp we next send with is the max of the received timestamp and + //# our current timestamp + this.signing.timestamp = Math.max(this.signing.timestamp, timestamp+1); + return true +} /* decode a buffer as a MAVLink message */ MAVLink20Processor.prototype.decode = function(msgbuf) { - var magic, incompat_flags, compat_flags, mlen, seq, srcSystem, srcComponent, unpacked, msgId, signature_len; + var magic, incompat_flags, compat_flags, mlen, seq, srcSystem, srcComponent, unpacked, msgId, signature_len, header_len; // decode the header try { - unpacked = jspack.Unpack('cBBBBBBHB', msgbuf.slice(0, 10)); // the H in here causes msgIDlow to takeup 2 bytes, the rest 1 + if (msgbuf[0] == 253) { + var unpacked = jspack.Unpack('BBBBBBBHB', msgbuf.slice(0, 10)); // the H in here causes msgIDlow to takeup 2 bytes, the rest 1 magic = unpacked[0]; mlen = unpacked[1]; incompat_flags = unpacked[2]; @@ -21018,150 +14090,135 @@ MAVLink20Processor.prototype.decode = function(msgbuf) { srcComponent = unpacked[6]; var msgIDlow = ((unpacked[7] & 0xFF) << 8) | ((unpacked[7] >> 8) & 0xFF); // first-two msgid bytes var msgIDhigh = unpacked[8]; // the 3rd msgid byte - msgId = msgIDlow | (msgIDhigh<<16); // combined result. 0 - 16777215 24bit number + msgId = msgIDlow | (msgIDhigh<<16); // combined result. 0 - 16777215 24bit number + header_len = 10; +} else { + var unpacked = jspack.Unpack('BBBBBB', msgbuf.slice(0, 6)); + magic = unpacked[0]; + mlen = unpacked[1]; + seq = unpacked[2]; + srcSystem = unpacked[3]; + srcComponent = unpacked[4]; + msgID = unpacked[5]; + incompat_flags = 0; + compat_flags = 0; + header_len = 6; +} } catch(e) { throw new Error('Unable to unpack MAVLink header: ' + e.message); } - // TODO allow full parsing of 1.0 inside the 2.0 parser, this is just a start - if (magic == mavlink20.PROTOCOL_MARKER_V1){ - //headerlen = 6; - - // these two are in the same place in both v1 and v2 so no change needed: - //magic = magic; - //mlen = mlen; - - // grab mavlink-v1 header position info from v2 unpacked position - seq1 = incompat_flags; - srcSystem1 = compat_flags; - srcComponent1 = seq; - msgId1 = srcSystem; - // override the v1 vs v2 offsets so we get the correct data either way... - seq = seq1; - srcSystem = srcSystem1; - srcComponent = srcComponent1; - msgId = msgId1; - // don't exist in mavlink1, so zero-them - incompat_flags = 0; - compat_flags = 0; - signature_len = 0; - // todo add more v1 here and don't just return - return; - } - - if (magic.charCodeAt(0) != this.protocol_marker) { - throw new Error("Invalid MAVLink prefix ("+magic.charCodeAt(0)+")"); + if (magic != this.protocol_marker) { + throw new Error("Invalid MAVLink prefix ("+magic+")"); } - // is packet supposed to be signed? - if ( incompat_flags & mavlink20.MAVLINK_IFLAG_SIGNED ){ - signature_len = mavlink20.MAVLINK_SIGNATURE_BLOCK_LEN; - } else { - signature_len = 0; - } - - // header's declared len compared to packets actual len - var actual_len = (msgbuf.length - (mavlink20.HEADER_LEN + 2 + signature_len)); - var actual_len_nosign = (msgbuf.length - (mavlink20.HEADER_LEN + 2 )); - - if ((mlen == actual_len) && (signature_len > 0)){ - var len_if_signed = mlen+signature_len; - //console.log("Packet appears signed && labeled as signed, OK. msgId=" + msgId); - - } else if ((mlen == actual_len_nosign) && (signature_len > 0)){ - - var len_if_signed = mlen+signature_len; - throw new Error("Packet appears unsigned when labeled as signed. Got actual_len "+actual_len_nosign+" expected " + len_if_signed + ", msgId=" + msgId); - - } else if( mlen != actual_len) { - throw new Error("Invalid MAVLink message length. Got " + (msgbuf.length - (mavlink20.HEADER_LEN + 2)) + " expected " + mlen + ", msgId=" + msgId); + // is packet supposed to be signed? + if ( incompat_flags & mavlink20.MAVLINK_IFLAG_SIGNED ){ + signature_len = mavlink20.MAVLINK_SIGNATURE_BLOCK_LEN; + } else { + signature_len = 0; + } - } - - if( false === _.has(mavlink20.map, msgId) ) { + // header's declared len compared to packets actual len + var actual_len = (msgbuf.length - (header_len + 2 + signature_len)); + var actual_len_nosign = (msgbuf.length - (header_len + 2 )); + + if ((mlen == actual_len) && (signature_len > 0)){ + var len_if_signed = mlen+signature_len; + //console.log("Packet appears signed && labeled as signed, OK. msgId=" + msgId); + + } else if ((mlen == actual_len_nosign) && (signature_len > 0)){ + + var len_if_signed = mlen+signature_len; + throw new Error("Packet appears unsigned when labeled as signed. Got actual_len "+actual_len_nosign+" expected " + len_if_signed + ", msgId=" + msgId); + + } else if( mlen != actual_len) { + throw new Error("Invalid MAVLink message length. Got " + (msgbuf.length - (header_len + 2)) + " expected " + mlen + ", msgId=" + msgId); + + } + + if (!(msgId in mavlink20.map)) { throw new Error("Unknown MAVLink message ID (" + msgId + ")"); } - // here's the common chunks of packet we want to work with below.. - var headerBuf= msgbuf.slice(mavlink20.HEADER_LEN); // first10 - var sigBuf = msgbuf.slice(-signature_len); // last 13 or nothing - var crcBuf1 = msgbuf.slice(-2); // either last-2 or last-2-prior-to-signature - var crcBuf2 = msgbuf.slice(-15,-13); // either last-2 or last-2-prior-to-signature - var payloadBuf = msgbuf.slice(mavlink20.HEADER_LEN, -(signature_len+2)); // the remaining bit between the header and the crc - var crcCheckBuf = msgbuf.slice(1, -(signature_len+2)); // the part uses to calculate the crc - ie between the magic and signature, + // here's the common chunks of packet we want to work with below.. + var payloadBuf = msgbuf.slice(mavlink20.HEADER_LEN, -(signature_len+2)); // the remaining bit between the header and the crc + var crcCheckBuf = msgbuf.slice(1, -(signature_len+2)); // the part uses to calculate the crc - ie between the magic and signature, // decode the payload // refs: (fmt, type, order_map, crc_extra) = mavlink20.map[msgId] var decoder = mavlink20.map[msgId]; // decode the checksum - var receivedChecksum = undefined; - if ( signature_len == 0 ) { // unsigned - try { - receivedChecksum = jspack.Unpack(' payloadBuf.length) { - payloadBuf = Buffer.concat([payloadBuf, Buffer.alloc(paylen - payloadBuf.length)]); + payloadBuf = this.concat_buffer(payloadBuf, new Uint8Array(paylen - payloadBuf.length).fill(0)); } // Decode the payload and reorder the fields to match the order map. try { @@ -21178,7 +14235,7 @@ MAVLink20Processor.prototype.decode = function(msgbuf) { if (elementsInMsg == actualElementsInMsg) { // Reorder the fields to match the order map - _.each(t, function(e, i, l) { + t.forEach(function(e, i, l) { args[i] = t[decoder.order_map[i]] }); } else { @@ -21222,13 +14279,11 @@ MAVLink20Processor.prototype.decode = function(msgbuf) { } // Finally reorder the fields to match the order map - _.each(t, function(e, i, l) { + t.forEach(function(e, i, l) { args[i] = tempArgs[decoder.order_map[i]] }); } - - // construct the message object try { // args at this point might look like: { '0': 6, '1': 8, '2': 0, '3': 0, '4': 3, '5': 3 } @@ -21239,11 +14294,11 @@ MAVLink20Processor.prototype.decode = function(msgbuf) { throw new Error('Unable to instantiate MAVLink message of type '+decoder.type+' : ' + e.message); } - m._signed = sig_ok; - if (m._signed) { m._link_id = msgbuf[-13]; } + m._signed = sig_ok; + if (m._signed) { m._link_id = msgbuf[-13]; } m._msgbuf = msgbuf; - m._payload = payloadBuf + m._payload = payloadBuf; m.crc = receivedChecksum; m._header = new mavlink20.header(msgId, mlen, seq, srcSystem, srcComponent, incompat_flags, compat_flags); this.log(m); @@ -21251,8 +14306,21 @@ MAVLink20Processor.prototype.decode = function(msgbuf) { } -// allow loading as both common.js (Node), and/or vanilla javascript in-browser -if(typeof module === "object" && module.exports) { - module.exports = {mavlink20, MAVLink20Processor}; +// Browser and Node.js compatible module exports +if (!isNode) { + // For browsers, attach to window or use global namespace + if (typeof window !== 'undefined') { + window.mavlink20 = mavlink20; + window.MAVLink20Processor = MAVLink20Processor; + } + // Also support global assignment + if (typeof global !== 'undefined') { + global.mavlink20 = mavlink20; + global.MAVLink20Processor = MAVLink20Processor; + } +} else { + // For Node.js, use module.exports + if (typeof module === "object" && module.exports) { + module.exports = {mavlink20, MAVLink20Processor}; + } } - diff --git a/src/mavlink_session.js b/src/mavlink_session.js index a8a1196..21847e5 100644 --- a/src/mavlink_session.js +++ b/src/mavlink_session.js @@ -1,8 +1,8 @@ import { EventEmitter } from 'events'; import WebSocketClient from './ws_client'; import WebSerial from './web_serial'; -import { mavlink20, MAVLink20Processor } from './mavlink'; import dronecan from './dronecan'; +import './mavlink'; class MavlinkSession extends EventEmitter { constructor() { @@ -27,8 +27,17 @@ class MavlinkSession extends EventEmitter { } } - initWebSocketConnection(ip, port) { - this.wsClient = new WebSocketClient(`ws://${ip}:${port}`); + initWebSocketConnection(ip, port, mavlinkSigning='') { + 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) => { diff --git a/src/ws_client.js b/src/ws_client.js index 1698f53..bebda71 100644 --- a/src/ws_client.js +++ b/src/ws_client.js @@ -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) => {