Browse Source

Merge branch 'meshtastic:main' into main

pull/720/head
Mictronics 12 months ago
committed by GitHub
parent
commit
7116c5c1f3
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      README.md
  2. 24
      bun.lock
  3. 22
      packages/core/src/meshDevice.ts
  4. 10
      packages/core/src/utils/queue.ts
  5. 4
      packages/transport-http/src/transport.ts
  6. 8
      packages/web/package.json
  7. 13
      packages/web/src/components/DeviceInfoPanel.tsx
  8. 4
      packages/web/src/components/Dialog/TracerouteResponseDialog.tsx
  9. 4
      packages/web/src/components/PageComponents/Connect/BLE.tsx
  10. 6
      packages/web/src/components/PageComponents/Connect/Serial.tsx
  11. 2
      packages/web/src/components/PageComponents/Messages/TraceRoute.tsx
  12. 19
      packages/web/src/components/ThemeSwitcher.tsx
  13. 16
      packages/web/src/components/UI/Footer.tsx
  14. 2
      packages/web/src/components/UI/Switch.tsx
  15. 32
      packages/web/src/core/hooks/useToggleVisiblility.ts
  16. 21
      packages/web/src/routes.tsx
  17. 16
      packages/web/tsconfig.json
  18. 1
      packages/web/vite-env.d.ts
  19. 10
      packages/web/vite.config.ts
  20. 19
      tsconfig.base.json
  21. 13
      tsconfig.json

2
README.md

@ -63,7 +63,7 @@ Follow the installation instructions on their home page.
1. **Clone the repository:** 1. **Clone the repository:**
```bash ```bash
git clone [https://github.com/meshtastic/meshtastic-web.git](https://github.com/meshtastic/meshtastic-web.git) git clone https://github.com/meshtastic/meshtastic-web.git
cd meshtastic-web cd meshtastic-web
``` ```
2. **Install dependencies for all packages:** 2. **Install dependencies for all packages:**

24
bun.lock

@ -5,6 +5,7 @@
"name": "meshtastic-web", "name": "meshtastic-web",
"dependencies": { "dependencies": {
"@bufbuild/protobuf": "^2.6.1", "@bufbuild/protobuf": "^2.6.1",
"@meshtastic/protobufs": "npm:@jsr/meshtastic__protobufs",
"ste-simple-events": "^3.0.11", "ste-simple-events": "^3.0.11",
"tslog": "^4.9.3", "tslog": "^4.9.3",
}, },
@ -18,7 +19,6 @@
"name": "@meshtastic/core", "name": "@meshtastic/core",
"version": "2.6.5", "version": "2.6.5",
"dependencies": { "dependencies": {
"@meshtastic/protobufs": "npm:@jsr/meshtastic__protobufs",
"crc": "npm:crc@^4.3.2", "crc": "npm:crc@^4.3.2",
}, },
}, },
@ -28,7 +28,7 @@
}, },
"packages/transport-http": { "packages/transport-http": {
"name": "@meshtastic/transport-http", "name": "@meshtastic/transport-http",
"version": "0.2.1", "version": "0.2.2",
}, },
"packages/transport-node": { "packages/transport-node": {
"name": "@meshtastic/transport-node", "name": "@meshtastic/transport-node",
@ -36,14 +36,14 @@
}, },
"packages/transport-web-bluetooth": { "packages/transport-web-bluetooth": {
"name": "@meshtastic/transport-web-bluetooth", "name": "@meshtastic/transport-web-bluetooth",
"version": "0.1.2", "version": "0.1.3",
"devDependencies": { "devDependencies": {
"@types/web-bluetooth": "npm:@types/web-bluetooth@^0.0.20", "@types/web-bluetooth": "npm:@types/web-bluetooth@^0.0.20",
}, },
}, },
"packages/transport-web-serial": { "packages/transport-web-serial": {
"name": "@meshtastic/transport-web-serial", "name": "@meshtastic/transport-web-serial",
"version": "0.2.1", "version": "0.2.2",
"dependencies": { "dependencies": {
"@types/w3c-web-serial": "npm:@types/w3c-web-serial@^1.0.7", "@types/w3c-web-serial": "npm:@types/w3c-web-serial@^1.0.7",
}, },
@ -54,7 +54,7 @@
"dependencies": { "dependencies": {
"@bufbuild/protobuf": "^2.6.0", "@bufbuild/protobuf": "^2.6.0",
"@hookform/resolvers": "^5.1.1", "@hookform/resolvers": "^5.1.1",
"@meshtastic/core": "npm:@jsr/[email protected]", "@meshtastic/core": "workspace:*",
"@meshtastic/transport-http": "npm:@jsr/meshtastic__transport-http", "@meshtastic/transport-http": "npm:@jsr/meshtastic__transport-http",
"@meshtastic/transport-web-bluetooth": "npm:@jsr/meshtastic__transport-web-bluetooth", "@meshtastic/transport-web-bluetooth": "npm:@jsr/meshtastic__transport-web-bluetooth",
"@meshtastic/transport-web-serial": "npm:@jsr/meshtastic__transport-web-serial", "@meshtastic/transport-web-serial": "npm:@jsr/meshtastic__transport-web-serial",
@ -296,6 +296,8 @@
"@jridgewell/resolve-uri": ["@jridgewell/[email protected]", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], "@jridgewell/resolve-uri": ["@jridgewell/[email protected]", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/source-map": ["@jridgewell/[email protected]", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" } }, "sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/[email protected]", "", {}, "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw=="], "@jridgewell/sourcemap-codec": ["@jridgewell/[email protected]", "", {}, "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw=="],
"@jridgewell/trace-mapping": ["@jridgewell/[email protected]", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ=="], "@jridgewell/trace-mapping": ["@jridgewell/[email protected]", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ=="],
@ -1296,6 +1298,8 @@
"source-map-js": ["[email protected]", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], "source-map-js": ["[email protected]", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"source-map-support": ["[email protected]", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
"splaytree-ts": ["[email protected]", "", {}, "sha512-0kGecIZNIReCSiznK3uheYB8sbstLjCZLiwcQwbmLhgHJj2gz6OnSPkVzJQCMnmEz1BQ4gPK59ylhBoEWOhGNA=="], "splaytree-ts": ["[email protected]", "", {}, "sha512-0kGecIZNIReCSiznK3uheYB8sbstLjCZLiwcQwbmLhgHJj2gz6OnSPkVzJQCMnmEz1BQ4gPK59ylhBoEWOhGNA=="],
"split-string": ["[email protected]", "", { "dependencies": { "extend-shallow": "^3.0.0" } }, "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw=="], "split-string": ["[email protected]", "", { "dependencies": { "extend-shallow": "^3.0.0" } }, "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw=="],
@ -1336,6 +1340,8 @@
"tar": ["[email protected]", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="], "tar": ["[email protected]", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="],
"terser": ["[email protected]", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.14.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg=="],
"testing-library": ["[email protected]", "", { "dependencies": { "tslib": "^1.9.0" }, "peerDependencies": { "@angular/common": "^6.0.0-rc.0 || ^6.0.0", "@angular/core": "^6.0.0-rc.0 || ^6.0.0" } }, "sha512-KCbqCCllbgiCXOgmh9MdsgdJ05pmimXGuggtC78pzpxpq/40A3bS+NJoqwCIqZbNnMr6KIZ2mlMZoZCkWVnaWw=="], "testing-library": ["[email protected]", "", { "dependencies": { "tslib": "^1.9.0" }, "peerDependencies": { "@angular/common": "^6.0.0-rc.0 || ^6.0.0", "@angular/core": "^6.0.0-rc.0 || ^6.0.0" } }, "sha512-KCbqCCllbgiCXOgmh9MdsgdJ05pmimXGuggtC78pzpxpq/40A3bS+NJoqwCIqZbNnMr6KIZ2mlMZoZCkWVnaWw=="],
"through2": ["[email protected]", "", { "dependencies": { "readable-stream": "3" } }, "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw=="], "through2": ["[email protected]", "", { "dependencies": { "readable-stream": "3" } }, "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw=="],
@ -1696,8 +1702,6 @@
"lru-cache/yallist": ["[email protected]", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], "lru-cache/yallist": ["[email protected]", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"meshtastic-web/@meshtastic/core": ["@jsr/[email protected]", "https://npm.jsr.io/~/11/@jsr/meshtastic__core/2.6.4.tgz", { "dependencies": { "@bufbuild/protobuf": "^2.2.3", "@jsr/meshtastic__protobufs": "^2.6.2", "crc": "^4.3.2", "ste-simple-events": "^3.0.11", "tslog": "^4.9.3" } }, "sha512-1Kz5DK6peFxluHOJR38vFwfgeJzMXTz+3p6TvibjILVhSQC2U1nu8aJbn6w5zhRqS+j79OmtrRvdzL6VNsTkkQ=="],
"meshtastic-web/@meshtastic/transport-http": ["@jsr/[email protected]", "https://npm.jsr.io/~/11/@jsr/meshtastic__transport-http/0.2.1.tgz", { "dependencies": { "@jsr/meshtastic__core": "^2.6.0" } }, "sha512-lmQKr3aIINKvtGROU4HchmSVqbZSbkIHqajowRRC8IAjsnR0zNTyxz210QyY4pFUF9hpcW3GRjwq5h/VO2JuGg=="], "meshtastic-web/@meshtastic/transport-http": ["@jsr/[email protected]", "https://npm.jsr.io/~/11/@jsr/meshtastic__transport-http/0.2.1.tgz", { "dependencies": { "@jsr/meshtastic__core": "^2.6.0" } }, "sha512-lmQKr3aIINKvtGROU4HchmSVqbZSbkIHqajowRRC8IAjsnR0zNTyxz210QyY4pFUF9hpcW3GRjwq5h/VO2JuGg=="],
"meshtastic-web/@meshtastic/transport-web-bluetooth": ["@jsr/[email protected]", "https://npm.jsr.io/~/11/@jsr/meshtastic__transport-web-bluetooth/0.1.2.tgz", { "dependencies": { "@jsr/meshtastic__core": "^2.6.4" } }, "sha512-Z+5pv9RXNgY0/crKExOH3pZ6LT0HIXFmnBL7NX5AO2knOFRn+4lmxQEhhmiTTlkUfqyEfAvbjuY5u4mq9TPTdQ=="], "meshtastic-web/@meshtastic/transport-web-bluetooth": ["@jsr/[email protected]", "https://npm.jsr.io/~/11/@jsr/meshtastic__transport-web-bluetooth/0.1.2.tgz", { "dependencies": { "@jsr/meshtastic__core": "^2.6.4" } }, "sha512-Z+5pv9RXNgY0/crKExOH3pZ6LT0HIXFmnBL7NX5AO2knOFRn+4lmxQEhhmiTTlkUfqyEfAvbjuY5u4mq9TPTdQ=="],
@ -1724,6 +1728,8 @@
"recast/tslib": ["[email protected]", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "recast/tslib": ["[email protected]", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"source-map-support/source-map": ["[email protected]", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"split-string/extend-shallow": ["[email protected]", "", { "dependencies": { "assign-symbols": "^1.0.0", "is-extendable": "^1.0.1" } }, "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q=="], "split-string/extend-shallow": ["[email protected]", "", { "dependencies": { "assign-symbols": "^1.0.0", "is-extendable": "^1.0.1" } }, "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q=="],
"string_decoder/safe-buffer": ["[email protected]", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], "string_decoder/safe-buffer": ["[email protected]", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
@ -1732,6 +1738,8 @@
"sweepline-intersections/tinyqueue": ["[email protected]", "", {}, "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA=="], "sweepline-intersections/tinyqueue": ["[email protected]", "", {}, "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA=="],
"terser/commander": ["[email protected]", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
"topojson-client/commander": ["[email protected]", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], "topojson-client/commander": ["[email protected]", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
"topojson-server/commander": ["[email protected]", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], "topojson-server/commander": ["[email protected]", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
@ -1748,8 +1756,6 @@
"geojson-polygon-self-intersections/rbush/quickselect": ["[email protected]", "", {}, "sha512-qN0Gqdw4c4KGPsBOQafj6yj/PA6c/L63f6CaZ/DCF/xF4Esu3jVmKLUDYxghFx8Kb/O7y9tI7x2RjTSXwdK1iQ=="], "geojson-polygon-self-intersections/rbush/quickselect": ["[email protected]", "", {}, "sha512-qN0Gqdw4c4KGPsBOQafj6yj/PA6c/L63f6CaZ/DCF/xF4Esu3jVmKLUDYxghFx8Kb/O7y9tI7x2RjTSXwdK1iQ=="],
"meshtastic-web/@meshtastic/core/@bufbuild/protobuf": ["@bufbuild/[email protected]", "", {}, "sha512-6cuonJVNOIL7lTj5zgo/Rc2bKAo4/GvN+rKCrUj7GdEHRzCk8zKOfFwUsL9nAVk5rSIsRmlgcpLzTRysopEeeg=="],
"meshtastic-web/@types/node/undici-types": ["[email protected]", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], "meshtastic-web/@types/node/undici-types": ["[email protected]", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
"peek-stream/through2/readable-stream": ["[email protected]", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], "peek-stream/through2/readable-stream": ["[email protected]", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],

22
packages/core/src/meshDevice.ts

@ -1,7 +1,6 @@
import { Logger } from "tslog";
import { create, fromBinary, toBinary } from "@bufbuild/protobuf"; import { create, fromBinary, toBinary } from "@bufbuild/protobuf";
import * as Protobuf from "@meshtastic/protobufs"; import * as Protobuf from "@meshtastic/protobufs";
import { Logger } from "tslog";
import { Constants } from "./constants.ts"; import { Constants } from "./constants.ts";
import type { Destination, PacketMetadata, Transport } from "./types.ts"; import type { Destination, PacketMetadata, Transport } from "./types.ts";
@ -40,6 +39,8 @@ export class MeshDevice {
public xModem: Xmodem; public xModem: Xmodem;
private _heartbeatIntervalId: ReturnType<typeof setInterval> | undefined;
constructor(transport: Transport, configId?: number) { constructor(transport: Transport, configId?: number) {
this.log = new Logger({ this.log = new Logger({
name: "iMeshDevice", name: "iMeshDevice",
@ -741,6 +742,18 @@ export class MeshDevice {
return this.sendRaw(toBinary(Protobuf.Mesh.ToRadioSchema, toRadio)); return this.sendRaw(toBinary(Protobuf.Mesh.ToRadioSchema, toRadio));
} }
/**
* Initializes the heartbeat interval, which sends a heartbeat ping every interval milliseconds.
*/
public setHeartbeatInterval(interval: number): void {
if (this._heartbeatIntervalId !== undefined) {
clearInterval(this._heartbeatIntervalId);
}
this._heartbeatIntervalId = setInterval(() => {
this.heartbeat();
}, interval);
}
/** /**
* Sends a trace route packet to the designated node * Sends a trace route packet to the designated node
*/ */
@ -798,6 +811,11 @@ export class MeshDevice {
/** Disconnects from the device **/ /** Disconnects from the device **/
public async disconnect(): Promise<void> { public async disconnect(): Promise<void> {
this.log.debug(Emitter[Emitter.Disconnect], "🔌 Disconnecting from device"); this.log.debug(Emitter[Emitter.Disconnect], "🔌 Disconnecting from device");
if (this._heartbeatIntervalId !== undefined) {
clearInterval(this._heartbeatIntervalId);
}
this.complete(); this.complete();
await this.transport.toDevice.close(); await this.transport.toDevice.close();
} }

10
packages/core/src/utils/queue.ts

@ -44,6 +44,16 @@ export class Queue {
if (this.queue.findIndex((qi) => qi.id === item.id) !== -1) { if (this.queue.findIndex((qi) => qi.id === item.id) !== -1) {
this.remove(item.id); this.remove(item.id);
const decoded = fromBinary(Protobuf.Mesh.ToRadioSchema, item.data); const decoded = fromBinary(Protobuf.Mesh.ToRadioSchema, item.data);
if (
decoded.payloadVariant.case === "heartbeat" ||
decoded.payloadVariant.case === "wantConfigId"
) {
// heartbeat and wantConfigId packets are not acknowledged by the device, assume success after timeout
resolve(item.id);
return;
}
console.warn( console.warn(
`Packet ${item.id} of type ${decoded.payloadVariant.case} timed out`, `Packet ${item.id} of type ${decoded.payloadVariant.case} timed out`,
); );

4
packages/transport-http/src/transport.ts

@ -12,7 +12,9 @@ export class TransportHTTP implements Types.Transport {
tls?: boolean, tls?: boolean,
): Promise<TransportHTTP> { ): Promise<TransportHTTP> {
const connectionUrl = `${tls ? "https" : "http"}://${address}`; const connectionUrl = `${tls ? "https" : "http"}://${address}`;
await fetch(`${connectionUrl}/json/report`); await fetch(`${connectionUrl}/api/v1/toradio`, {
method: "OPTIONS",
});
await Promise.resolve(); await Promise.resolve();
return new TransportHTTP(connectionUrl); return new TransportHTTP(connectionUrl);
} }

8
packages/web/package.json

@ -14,14 +14,14 @@
"homepage": "https://meshtastic.org", "homepage": "https://meshtastic.org",
"scripts": { "scripts": {
"build": "bunx --bun vite build", "build": "vite build",
"build:analyze": "BUNDLE_ANALYZE=true bun run build", "build:analyze": "BUNDLE_ANALYZE=true bun run build",
"check": "biome check src/", "check": "biome check src/",
"check:fix": "biome check --write src/", "check:fix": "biome check --write src/",
"dev": "bunx --bun vite", "dev": "vite",
"test": "bunx --bun vitest", "test": "vitest",
"ts:check": "bun run tsc --noEmit", "ts:check": "bun run tsc --noEmit",
"preview": "bunx --bun vite preview", "preview": "vite preview",
"generate:routes": "bun @tanstack/router-cli generate --outDir src/ routes --rootRoutePath /", "generate:routes": "bun @tanstack/router-cli generate --outDir src/ routes --rootRoutePath /",
"package": "gzipper c -i html,js,css,png,ico,svg,json,webmanifest,txt dist dist/output && tar -cvf dist/build.tar -C ./dist/output/ ." "package": "gzipper c -i html,js,css,png,ico,svg,json,webmanifest,txt dist dist/output && tar -cvf dist/build.tar -C ./dist/output/ ."
}, },

13
packages/web/src/components/DeviceInfoPanel.tsx

@ -85,6 +85,12 @@ export const DeviceInfoPanel = ({
]; ];
const actionButtons: ActionButtonConfig[] = [ const actionButtons: ActionButtonConfig[] = [
{
id: "theme",
label: t("theme.changeTheme"),
icon: Palette,
render: () => <ThemeSwitcher />,
},
{ {
id: "changeName", id: "changeName",
label: t("sidebar.deviceInfo.deviceName.changeName"), label: t("sidebar.deviceInfo.deviceName.changeName"),
@ -97,12 +103,7 @@ export const DeviceInfoPanel = ({
icon: SearchIcon, icon: SearchIcon,
onClick: setCommandPaletteOpen, onClick: setCommandPaletteOpen,
}, },
{
id: "theme",
label: t("theme.changeTheme"),
icon: Palette,
render: () => <ThemeSwitcher />,
},
{ {
id: "language", id: "language",
label: t("language.changeLanguage"), label: t("language.changeLanguage"),

4
packages/web/src/components/Dialog/TracerouteResponseDialog.tsx

@ -30,7 +30,7 @@ export const TracerouteResponseDialog = ({
const routeBack: number[] = traceroute?.data.routeBack ?? []; const routeBack: number[] = traceroute?.data.routeBack ?? [];
const snrTowards = (traceroute?.data.snrTowards ?? []).map((snr) => snr / 4); const snrTowards = (traceroute?.data.snrTowards ?? []).map((snr) => snr / 4);
const snrBack = (traceroute?.data.snrBack ?? []).map((snr) => snr / 4); const snrBack = (traceroute?.data.snrBack ?? []).map((snr) => snr / 4);
const from = getNode(traceroute?.from ?? 0); const from = getNode(traceroute?.to ?? 0); // The origin of the traceroute = the "to" node of the mesh packet
const fromLongName = const fromLongName =
from?.user?.longName ?? from?.user?.longName ??
(from ? `!${numberToHexUnpadded(from?.num)}` : t("unknown.shortName")); (from ? `!${numberToHexUnpadded(from?.num)}` : t("unknown.shortName"));
@ -40,7 +40,7 @@ export const TracerouteResponseDialog = ({
? `${numberToHexUnpadded(from?.num).substring(0, 4)}` ? `${numberToHexUnpadded(from?.num).substring(0, 4)}`
: t("unknown.shortName")); : t("unknown.shortName"));
const toUser = getNode(traceroute?.to ?? 0); const toUser = getNode(traceroute?.from ?? 0); // The destination of the traceroute = the "from" node of the mesh packet
if (!toUser || !from) { if (!toUser || !from) {
return null; return null;

4
packages/web/src/components/PageComponents/Connect/BLE.tsx

@ -36,6 +36,10 @@ export const BLE = ({ closeDialog }: TabElementProps) => {
setSelectedDevice(id); setSelectedDevice(id);
device.addConnection(connection); device.addConnection(connection);
subscribeAll(device, connection, messageStore); subscribeAll(device, connection, messageStore);
const HEARTBEAT_INTERVAL = 5*60*1000;
connection.setHeartbeatInterval(HEARTBEAT_INTERVAL);
closeDialog(); closeDialog();
}; };

6
packages/web/src/components/PageComponents/Connect/Serial.tsx

@ -43,6 +43,9 @@ export const Serial = ({ closeDialog }: TabElementProps) => {
device.addConnection(connection); device.addConnection(connection);
subscribeAll(device, connection, messageStore); subscribeAll(device, connection, messageStore);
const HEARTBEAT_INTERVAL = 5*60*1000;
connection.setHeartbeatInterval(HEARTBEAT_INTERVAL);
closeDialog(); closeDialog();
}; };
@ -52,7 +55,7 @@ export const Serial = ({ closeDialog }: TabElementProps) => {
disabled={connectionInProgress} disabled={connectionInProgress}
> >
<div className="flex h-48 flex-col gap-2 overflow-y-auto"> <div className="flex h-48 flex-col gap-2 overflow-y-auto">
{serialPorts.map((port) => { {serialPorts.map((port, idx) => {
const { usbProductId, usbVendorId } = port.getInfo(); const { usbProductId, usbVendorId } = port.getInfo();
const vendor = usbVendorId ?? t("unknown.shortName"); const vendor = usbVendorId ?? t("unknown.shortName");
const product = usbProductId ?? t("unknown.shortName"); const product = usbProductId ?? t("unknown.shortName");
@ -70,6 +73,7 @@ export const Serial = ({ closeDialog }: TabElementProps) => {
{t("newDeviceDialog.serialConnection.deviceIdentifier", { {t("newDeviceDialog.serialConnection.deviceIdentifier", {
vendorId: vendor, vendorId: vendor,
productId: product, productId: product,
index: idx,
})} })}
</Button> </Button>
); );

2
packages/web/src/components/PageComponents/Messages/TraceRoute.tsx

@ -41,7 +41,7 @@ const RoutePath = ({ title, from, to, path, snr }: RoutePathProps) => {
<span key={getNode(hop)?.num ?? hop}> <span key={getNode(hop)?.num ?? hop}>
<p> <p>
{getNode(hop)?.user?.longName ?? {getNode(hop)?.user?.longName ??
`${t("traceRoute.nodeUnknownPrefix")}${numberToHexUnpadded(hop)}`} `${t("unknown.longName")} (!${numberToHexUnpadded(hop)})`}
</p> </p>
<p> <p>
{snr?.[i + 1] ?? t("unknown.num")} {snr?.[i + 1] ?? t("unknown.num")}

19
packages/web/src/components/ThemeSwitcher.tsx

@ -1,7 +1,8 @@
import { useTheme } from "@core/hooks/useTheme.ts";
import { cn } from "@core/utils/cn.ts";
import { Monitor, Moon, Sun } from "lucide-react"; import { Monitor, Moon, Sun } from "lucide-react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useTheme } from "../core/hooks/useTheme.ts";
import { useToggleVisibility } from "../core/hooks/useToggleVisiblility.ts";
import { cn } from "../core/utils/cn.ts";
import { Button } from "./UI/Button.tsx"; import { Button } from "./UI/Button.tsx";
import { Subtle } from "./UI/Typography/Subtle.tsx"; import { Subtle } from "./UI/Typography/Subtle.tsx";
@ -12,10 +13,16 @@ interface ThemeSwitcherProps {
disableHover?: boolean; disableHover?: boolean;
} }
const TOOLTIP_TIMEOUT = 2000; // 2 seconds
export default function ThemeSwitcher({ export default function ThemeSwitcher({
className: passedClassName = "", className: passedClassName = "",
disableHover = false, disableHover = false,
}: ThemeSwitcherProps) { }: ThemeSwitcherProps) {
const [showTooltip, toggleShowTooltip] = useToggleVisibility({
timeout: TOOLTIP_TIMEOUT,
});
const { preference, setPreference } = useTheme(); const { preference, setPreference } = useTheme();
const { t } = useTranslation("ui"); const { t } = useTranslation("ui");
@ -35,8 +42,10 @@ export default function ThemeSwitcher({
const toggleTheme = () => { const toggleTheme = () => {
const preferences: ThemePreference[] = ["light", "dark", "system"]; const preferences: ThemePreference[] = ["light", "dark", "system"];
const currentIndex = preferences.indexOf(preference); const currentIndex = preferences.indexOf(preference);
const nextPreference = preferences[(currentIndex + 1) % preferences.length]; const nextPreference =
preferences[(currentIndex + 1) % preferences.length] ?? "system";
setPreference(nextPreference); setPreference(nextPreference);
toggleShowTooltip();
}; };
const preferenceDisplayMap: Record<ThemePreference, string> = { const preferenceDisplayMap: Record<ThemePreference, string> = {
@ -65,12 +74,12 @@ export default function ThemeSwitcher({
<span <span
data-label="theme-preference-tooltip" data-label="theme-preference-tooltip"
className={cn( className={cn(
"transition-opacity duration-150", "transition-opacity duration-150 hidden",
"block absolute w-max max-w-xs", "block absolute w-max max-w-xs",
"p-1 text-xs text-white dark:text-black bg-black dark:bg-white", "p-1 text-xs text-white dark:text-black bg-black dark:bg-white",
"rounded-md shadow-lg", "rounded-md shadow-lg",
"left-1/2 -translate-x-1/2 -top-8", "left-1/2 -translate-x-1/2 -top-8",
"opacity-0", showTooltip ? "visible" : "hidden opacity-0",
)} )}
> >
{currentDisplayPreference} {currentDisplayPreference}

16
packages/web/src/components/UI/Footer.tsx

@ -1,13 +1,11 @@
import { cn } from "@core/utils/cn.ts"; import { cn } from "@core/utils/cn.ts";
import { Trans, useTranslation } from "react-i18next"; import { Trans } from "react-i18next";
type FooterProps = { type FooterProps = {
className?: string; className?: string;
}; };
const Footer = ({ className, ...props }: FooterProps) => { const Footer = ({ className, ...props }: FooterProps) => {
const { t } = useTranslation();
return ( return (
<footer <footer
className={cn( className={cn(
@ -16,11 +14,15 @@ const Footer = ({ className, ...props }: FooterProps) => {
)} )}
{...props} {...props}
> >
<div className="justify-start px-2"> <div className="px-2">
<span className="font-semibold text-gray-500/40 dark:text-gray-400/40">
{String(import.meta.env.VITE_VERSION)?.toUpperCase()}
</span>
<span className="font-semibold text-gray-500/40 dark:text-gray-400/40 mx-2">
-
</span>
<span className="font-semibold text-gray-500/40 dark:text-gray-400/40"> <span className="font-semibold text-gray-500/40 dark:text-gray-400/40">
{t("footer.commitSha", { {`#${String(import.meta.env.VITE_COMMIT_HASH)?.toUpperCase()}`}
sha: String(import.meta.env.VITE_COMMIT_HASH)?.toUpperCase(),
})}
</span> </span>
</div> </div>
<p className="ml-auto mr-auto text-gray-500 dark:text-gray-400"> <p className="ml-auto mr-auto text-gray-500 dark:text-gray-400">

2
packages/web/src/components/UI/Switch.tsx

@ -8,7 +8,7 @@ const Switch = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SwitchPrimitives.Root <SwitchPrimitives.Root
className={cn( className={cn(
"peer inline-flex h-[24px] w-[44px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus:outline-hidden focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-slate-900 data-[state=unchecked]:bg-slate-200 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900 dark:data-[state=checked]:bg-slate-600 dark:data-[state=unchecked]:bg-slate-300", "peer inline-flex h-[24px] w-[44px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus:outline-hidden focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-green-500 data-[state=unchecked]:bg-slate-200 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900 dark:data-[state=checked]:bg-green-600 dark:data-[state=unchecked]:bg-slate-300",
className, className,
)} )}
{...props} {...props}

32
packages/web/src/core/hooks/useToggleVisiblility.ts

@ -0,0 +1,32 @@
import { useCallback, useEffect, useRef, useState } from "react";
export function useToggleVisibility({ timeout }: { timeout?: number } = {}) {
const [isVisible, setIsVisible] = useState(false);
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const show = useCallback(() => {
setIsVisible(true);
if (timeout) {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
setIsVisible(false);
timeoutRef.current = null;
}, timeout);
}
}, [timeout]);
// Clear timeout on unmount
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, []);
return [isVisible, show] as const;
}

21
packages/web/src/routes.tsx

@ -14,7 +14,7 @@ import {
redirect, redirect,
} from "@tanstack/react-router"; } from "@tanstack/react-router";
import type { useTranslation } from "react-i18next"; import type { useTranslation } from "react-i18next";
import { z } from "zod"; import { z } from "zod/v4";
import { App } from "./App.tsx"; import { App } from "./App.tsx";
interface AppContext { interface AppContext {
@ -55,23 +55,6 @@ const messagesRoute = createRoute({
}, },
}); });
const chatIdSchema = z.string().refine(
(val) => {
const num = Number(val);
if (Number.isNaN(num) || !Number.isInteger(num)) {
return false;
}
const isChannelId = num >= 0 && num <= 10;
const isNodeId = num >= 1000000000 && num <= 9999999999;
return isChannelId || isNodeId;
},
{
message: "Chat ID must be a channel (0-10) or a valid node ID.",
},
);
export const messagesWithParamsRoute = createRoute({ export const messagesWithParamsRoute = createRoute({
getParentRoute: () => rootRoute, getParentRoute: () => rootRoute,
path: "/messages/$type/$chatId", path: "/messages/$type/$chatId",
@ -83,7 +66,7 @@ export const messagesWithParamsRoute = createRoute({
message: 'Type must be "direct" or "broadcast".', message: 'Type must be "direct" or "broadcast".',
}) })
.parse(params.type), .parse(params.type),
chatId: chatIdSchema.parse(params.chatId), chatId: z.coerce.number().int().min(0).max(4294967294).parse(params.chatId), // max is 0xffffffff - 1
}), }),
}); });

16
packages/web/tsconfig.json

@ -1,20 +1,6 @@
{ {
"extends": "../../tsconfig.base.json",
"compilerOptions": { "compilerOptions": {
"target": "ES2022",
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "ESNext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"strictNullChecks": true, "strictNullChecks": true,

1
packages/web/vite-env.d.ts

@ -3,6 +3,7 @@
interface ImportMetaEnv { interface ImportMetaEnv {
readonly env: { readonly env: {
readonly VITE_COMMIT_HASH: string; readonly VITE_COMMIT_HASH: string;
readonly VITE_VERSION: string;
}; };
} }

10
packages/web/vite.config.ts

@ -6,6 +6,7 @@ import react from "@vitejs/plugin-react";
import { defineConfig } from "vite"; import { defineConfig } from "vite";
let hash = ""; let hash = "";
let version = "v0.0.0";
try { try {
hash = execSync("git rev-parse --short HEAD", { encoding: "utf8" }).trim(); hash = execSync("git rev-parse --short HEAD", { encoding: "utf8" }).trim();
} catch (error) { } catch (error) {
@ -13,6 +14,14 @@ try {
hash = "DEV"; hash = "DEV";
} }
try {
version = execSync("git describe --tags --abbrev=0", {
encoding: "utf8",
}).trim();
} catch (error) {
console.error("Error getting git version:", error);
}
const CONTENT_SECURITY_POLICY = const CONTENT_SECURITY_POLICY =
"script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' data: https://rsms.me https://cdn.jsdelivr.net; img-src 'self' data:; font-src 'self' data: https://rsms.me https://cdn.jsdelivr.net; worker-src 'self' blob:; object-src 'none'; base-uri 'self';"; "script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' data: https://rsms.me https://cdn.jsdelivr.net; img-src 'self' data:; font-src 'self' data: https://rsms.me https://cdn.jsdelivr.net; worker-src 'self' blob:; object-src 'none'; base-uri 'self';";
@ -37,6 +46,7 @@ export default defineConfig({
}, },
define: { define: {
"import.meta.env.VITE_COMMIT_HASH": JSON.stringify(hash), "import.meta.env.VITE_COMMIT_HASH": JSON.stringify(hash),
"import.meta.env.VITE_VERSION": JSON.stringify(version),
}, },
build: { build: {
emptyOutDir: true, emptyOutDir: true,

19
tsconfig.base.json

@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"isolatedModules": true,
"skipLibCheck": true,
"noUncheckedIndexedAccess": true,
"noFallthroughCasesInSwitch": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"jsx": "react-jsx",
"baseUrl": "."
}
}

13
tsconfig.json

@ -0,0 +1,13 @@
{
"extends": "./tsconfig.base.json",
"files": [],
"references": [
{ "path": "packages/web" },
{ "path": "packages/transport-deno" },
{ "path": "packages/transport-node" },
{ "path": "packages/transport-web-serial" },
{ "path": "packages/transport-web-bluetooth" },
{ "path": "packages/transport-http" }
]
}
Loading…
Cancel
Save