Browse Source

Fix/add npm jsr building (#722)

* fixed github workflows to improve handling of mutl runtimes

* updating readme

* Update packages/core/src/meshDevice.ts

Co-authored-by: Copilot <[email protected]>

* Update packages/core/package.json

Co-authored-by: Copilot <[email protected]>

* Update packages/transport-http/package.json

Co-authored-by: Copilot <[email protected]>

---------

Co-authored-by: Copilot <[email protected]>
pull/723/head
Dan Ditomaso 11 months ago
committed by GitHub
parent
commit
8a443e9cad
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 9
      .github/workflows/ci.yml
  2. 12
      .github/workflows/release.yml
  3. 16
      README.md
  4. 19
      biome.json
  5. 117
      bun.lock
  6. 5472
      deno.lock
  7. 23
      package.json
  8. 11
      packages/core/deno.json
  9. 11
      packages/core/package.json
  10. 4
      packages/core/src/constants.ts
  11. 2296
      packages/core/src/meshDevice.ts
  12. 168
      packages/core/src/types.ts
  13. 748
      packages/core/src/utils/eventSystem.ts
  14. 200
      packages/core/src/utils/queue.ts
  15. 432
      packages/core/src/utils/transform/decodePacket.ts
  16. 126
      packages/core/src/utils/transform/fromDevice.ts
  17. 24
      packages/core/src/utils/transform/toDevice.ts
  18. 232
      packages/core/src/utils/xmodem.ts
  19. 0
      packages/transport-deno/package.json
  20. 44
      packages/transport-deno/src/transport.ts
  21. 5
      packages/transport-http/jsr.json
  22. 9
      packages/transport-http/package.json
  23. 146
      packages/transport-http/src/transport.ts
  24. 0
      packages/transport-node/package.json
  25. 112
      packages/transport-node/src/transport.ts
  26. 5
      packages/transport-web-bluetooth/jsr.json
  27. 10
      packages/transport-web-bluetooth/package.json
  28. 260
      packages/transport-web-bluetooth/src/transport.ts
  29. 14
      packages/transport-web-serial/deno.json
  30. 5
      packages/transport-web-serial/jsr.json
  31. 14
      packages/transport-web-serial/package.json
  32. 74
      packages/transport-web-serial/src/transport.ts
  33. 22
      packages/web/README.md
  34. 4
      packages/web/package.json
  35. 648
      packages/web/src/components/CommandPalette/index.tsx
  36. 8
      packages/web/vite-env.d.ts
  37. 92
      packages/web/vite.config.ts
  38. 42
      packages/web/vitest.config.ts
  39. 128
      scripts/build_npm_package.ts

9
.github/workflows/ci.yml

@ -30,7 +30,7 @@ jobs:
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: ~/.cache/deno path: ~/.cache/deno
key: ${{ runner.os }}-deno-${{ hashFiles('**/deno.lock', '**/deno.json', '**/deno.jsonc') }} key: ${{ runner.os }}-deno-${{ hashFiles('**/deno.lock', '**/package.json') }}
restore-keys: | restore-keys: |
${{ runner.os }}-deno- ${{ runner.os }}-deno-
@ -40,7 +40,7 @@ jobs:
path: | path: |
~/.bun/install/cache ~/.bun/install/cache
packages/web/node_modules packages/web/node_modules
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }} key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
restore-keys: | restore-keys: |
${{ runner.os }}-bun- ${{ runner.os }}-bun-
@ -60,10 +60,7 @@ jobs:
for pkg_dir in ${{ steps.changed_packages.outputs.all_changed_and_modified_files }}; do for pkg_dir in ${{ steps.changed_packages.outputs.all_changed_and_modified_files }}; do
echo "🔍 Inspecting $pkg_dir..." echo "🔍 Inspecting $pkg_dir..."
if [[ -f "$pkg_dir/deno.json" ]]; then if [[ -f "$pkg_dir/deno.lock" ]]; then
echo "🔧 Building with Deno: $pkg_dir"
deno task build "$pkg_dir"
elif [[ -f "$pkg_dir/bun.lockb" ]]; then
echo "🔧 Building with Bun: $pkg_dir" echo "🔧 Building with Bun: $pkg_dir"
(cd "$pkg_dir" && bun install && bun run build) (cd "$pkg_dir" && bun install && bun run build)
else else

12
.github/workflows/release.yml

@ -47,7 +47,7 @@ jobs:
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: ~/.cache/deno path: ~/.cache/deno
key: ${{ runner.os }}-deno-${{ hashFiles('**/deno.lock', '**/deno.json', '**/deno.jsonc') }} key: ${{ runner.os }}-deno-${{ hashFiles('**/deno.lock') }}
restore-keys: | restore-keys: |
${{ runner.os }}-deno- ${{ runner.os }}-deno-
@ -80,7 +80,7 @@ jobs:
run: | run: |
set -euo pipefail set -euo pipefail
excluded=("packages/web" "packages/transport-deno") excluded=("packages/web" "packages/transport-deno" "packages/transport-node")
for pkg_dir in ${{ steps.changed_packages.outputs.all_changed_and_modified_files }}; do for pkg_dir in ${{ steps.changed_packages.outputs.all_changed_and_modified_files }}; do
echo "🔍 Inspecting $pkg_dir" echo "🔍 Inspecting $pkg_dir"
@ -90,12 +90,12 @@ jobs:
continue continue
fi fi
if [[ -f "$pkg_dir/deno.json" ]]; then if [[ -f "$pkg_dir/jsr.json" ]]; then
echo "🦕 Building with Deno: $pkg_dir" echo "🦕 Publishing to NPM: $pkg_dir"
deno task build "$pkg_dir" bun run build:npm $pkg_dir
echo "📦 Publishing to JSR" echo "📦 Publishing to JSR"
(cd "$pkg_dir" && deno publish --allow-dirty) (cd "$pkg_dir" && deno publish --allow-dirty)
elif [[ -f "$pkg_dir/bun.lockb" ]]; then elif [[ -f "$pkg_dir/bun.lock" ]]; then
echo "🥖 Building with Bun: $pkg_dir" echo "🥖 Building with Bun: $pkg_dir"
(cd "$pkg_dir" && bun install && bun run build) (cd "$pkg_dir" && bun install && bun run build)
else else

16
README.md

@ -56,8 +56,8 @@ This monorepo leverages the following technologies:
### Prerequisites ### Prerequisites
You'll need to have [Bun](https://bun.sh/) installed to work with this You'll need to have [Bun](https://bun.sh/) installed to work with this monorepo.
monorepo. Follow the installation instructions on their home page. Follow the installation instructions on their home page.
### Development Setup ### Development Setup
@ -80,22 +80,12 @@ monorepo. Follow the installation instructions on their home page.
To start the development server for the web client: To start the development server for the web client:
```bash ```bash
bun run --filter web dev cd ./packages/web && bun run dev
``` ```
This will typically run the web client on http://localhost:3000 and requires a This will typically run the web client on http://localhost:3000 and requires a
Chromium browser Chromium browser
## Meshtastic JS Packages
While the js packages are primarily libraries, you can run their tests or
specific development scripts if defined within their package.json files. For
example, to run tests for a specific package:
```bash
bun run --filter core test
```
### Feedback ### Feedback
If you encounter any issues with nightly builds, please report them in our If you encounter any issues with nightly builds, please report them in our

19
packages/web/biome.json → biome.json

@ -1,7 +1,6 @@
{ {
"$schema": "https://biomejs.dev/schemas/2.0.5/schema.json",
"files": { "files": {
"includes": ["**", "!node_modules/**", "!**/*.css", "!dist/**", "!build/**", "!coverage/**", "!**/*.d.ts"], "includes": ["**/*.ts", "**/*.tsx", "!**/*.test.ts", "!**/*.test.tsx", "!npm_modules/**", "!dist/**", "!npm/**"],
"ignoreUnknown": false "ignoreUnknown": false
}, },
"formatter": { "formatter": {
@ -14,6 +13,7 @@
}, },
"linter": { "linter": {
"enabled": true, "enabled": true,
"includes": ["**", "!test/**"],
"rules": { "rules": {
"recommended": true, "recommended": true,
"suspicious": { "suspicious": {
@ -21,13 +21,9 @@
"noDebugger": "error" "noDebugger": "error"
}, },
"style": { "style": {
"useConst": "error",
"useBlockStatements": "error", "useBlockStatements": "error",
"useSingleVarDeclarator": "off" "useSingleVarDeclarator": "off"
}, },
"complexity": {
"noForEach": "off"
},
"correctness": { "correctness": {
"noUnusedVariables": "error", "noUnusedVariables": "error",
"noUnusedImports": "error" "noUnusedImports": "error"
@ -40,10 +36,9 @@
"semicolons": "always" "semicolons": "always"
} }
}, },
"json": { "json": {
"formatter": { "formatter": {
"indentStyle": "space", "enabled": false
"indentWidth": 2 }
} }
}
} }

117
bun.lock

@ -3,12 +3,51 @@
"workspaces": { "workspaces": {
"": { "": {
"name": "meshtastic-web", "name": "meshtastic-web",
"dependencies": {
"@bufbuild/protobuf": "^2.6.1",
"ste-simple-events": "^3.0.11",
"tslog": "^4.9.3",
},
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^1.8.3", "@types/node": "^22.16.4",
"bun": "^1.1.18", "bun": "^1.2.18",
"typescript": "^5.8.3", "typescript": "^5.8.3",
}, },
}, },
"packages/core": {
"name": "@meshtastic/core",
"version": "2.6.5",
"dependencies": {
"@meshtastic/protobufs": "npm:@jsr/meshtastic__protobufs",
"crc": "npm:crc@^4.3.2",
},
},
"packages/transport-deno": {
"name": "@meshtastic/transport-deno",
"version": "0.1.1",
},
"packages/transport-http": {
"name": "@meshtastic/transport-http",
"version": "0.2.1",
},
"packages/transport-node": {
"name": "@meshtastic/transport-node",
"version": "0.0.1",
},
"packages/transport-web-bluetooth": {
"name": "@meshtastic/transport-web-bluetooth",
"version": "0.1.2",
"devDependencies": {
"@types/web-bluetooth": "npm:@types/web-bluetooth@^0.0.20",
},
},
"packages/transport-web-serial": {
"name": "@meshtastic/transport-web-serial",
"version": "0.2.1",
"dependencies": {
"@types/w3c-web-serial": "npm:@types/w3c-web-serial@^1.0.7",
},
},
"packages/web": { "packages/web": {
"name": "meshtastic-web", "name": "meshtastic-web",
"version": "2.7.0-0", "version": "2.7.0-0",
@ -167,25 +206,25 @@
"@babel/types": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ=="], "@babel/types": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ=="],
"@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="], "@biomejs/biome": ["@biomejs/biome@2.0.6", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.0.6", "@biomejs/cli-darwin-x64": "2.0.6", "@biomejs/cli-linux-arm64": "2.0.6", "@biomejs/cli-linux-arm64-musl": "2.0.6", "@biomejs/cli-linux-x64": "2.0.6", "@biomejs/cli-linux-x64-musl": "2.0.6", "@biomejs/cli-win32-arm64": "2.0.6", "@biomejs/cli-win32-x64": "2.0.6" }, "bin": { "biome": "bin/biome" } }, "sha512-RRP+9cdh5qwe2t0gORwXaa27oTOiQRQvrFf49x2PA1tnpsyU7FIHX4ZOFMtBC4QNtyWsN7Dqkf5EDbg4X+9iqA=="],
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="], "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.0.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-AzdiNNjNzsE6LfqWyBvcL29uWoIuZUkndu+wwlXW13EKcBHbbKjNQEZIJKYDc6IL+p7bmWGx3v9ZtcRyIoIz5A=="],
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@1.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg=="], "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.0.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-wJjjP4E7bO4WJmiQaLnsdXMa516dbtC6542qeRkyJg0MqMXP0fvs4gdsHhZ7p9XWTAmGIjZHFKXdsjBvKGIJJQ=="],
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g=="], "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.0.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-ZSVf6TYo5rNMUHIW1tww+rs/krol7U5A1Is/yzWyHVZguuB0lBnIodqyFuwCNqG9aJGyk7xIMS8HG0qGUPz0SA=="],
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA=="], "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.0.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-CVPEMlin3bW49sBqLBg2x016Pws7eUXA27XYDFlEtponD0luYjg2zQaMJ2nOqlkKG9fqzzkamdYxHdMDc2gZFw=="],
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg=="], "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.0.6", "", { "os": "linux", "cpu": "x64" }, "sha512-geM1MkHTV1Kh2Cs/Xzot9BOF3WBacihw6bkEmxkz4nSga8B9/hWy5BDiOG3gHDGIBa8WxT0nzsJs2f/hPqQIQw=="],
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg=="], "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.0.6", "", { "os": "linux", "cpu": "x64" }, "sha512-mKHE/e954hR/hSnAcJSjkf4xGqZc/53Kh39HVW1EgO5iFi0JutTN07TSjEMg616julRtfSNJi0KNyxvc30Y4rQ=="],
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@1.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg=="], "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.0.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-290V4oSFoKaprKE1zkYVsDfAdn0An5DowZ+GIABgjoq1ndhvNxkJcpxPsiYtT7slbVe3xmlT0ncdfOsN7KruzA=="],
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="], "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.0.6", "", { "os": "win32", "cpu": "x64" }, "sha512-bfM1Bce0d69Ao7pjTjUS+AWSZ02+5UHdiAP85Th8e9yV5xzw6JrHXbL5YWlcEKQ84FIZMdDc7ncuti1wd2sdbw=="],
"@bufbuild/protobuf": ["@bufbuild/[email protected].0", "", {}, "sha512-6cuonJVNOIL7lTj5zgo/Rc2bKAo4/GvN+rKCrUj7GdEHRzCk8zKOfFwUsL9nAVk5rSIsRmlgcpLzTRysopEeeg=="], "@bufbuild/protobuf": ["@bufbuild/[email protected].1", "", {}, "sha512-DaG6XlyKpz08bmHY5SGX2gfIllaqtDJ/KwVoxsmP22COOLYwDBe7yD3DZGwXem/Xq7QOc9cuR7R3MpAv5CFfDw=="],
"@esbuild/aix-ppc64": ["@esbuild/[email protected]", "", { "os": "aix", "cpu": "ppc64" }, "sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw=="], "@esbuild/aix-ppc64": ["@esbuild/[email protected]", "", { "os": "aix", "cpu": "ppc64" }, "sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw=="],
@ -281,13 +320,19 @@
"@maplibre/maplibre-gl-style-spec": ["@maplibre/[email protected]", "", { "dependencies": { "@mapbox/jsonlint-lines-primitives": "~2.0.2", "@mapbox/unitbezier": "^0.0.1", "json-stringify-pretty-compact": "^4.0.0", "minimist": "^1.2.8", "quickselect": "^3.0.0", "rw": "^1.3.3", "tinyqueue": "^3.0.0" }, "bin": { "gl-style-format": "dist/gl-style-format.mjs", "gl-style-migrate": "dist/gl-style-migrate.mjs", "gl-style-validate": "dist/gl-style-validate.mjs" } }, "sha512-IGJtuBbaGzOUgODdBRg66p8stnwj9iDXkgbYKoYcNiiQmaez5WVRfXm4b03MCDwmZyX93csbfHFWEJJYHnn5oA=="], "@maplibre/maplibre-gl-style-spec": ["@maplibre/[email protected]", "", { "dependencies": { "@mapbox/jsonlint-lines-primitives": "~2.0.2", "@mapbox/unitbezier": "^0.0.1", "json-stringify-pretty-compact": "^4.0.0", "minimist": "^1.2.8", "quickselect": "^3.0.0", "rw": "^1.3.3", "tinyqueue": "^3.0.0" }, "bin": { "gl-style-format": "dist/gl-style-format.mjs", "gl-style-migrate": "dist/gl-style-migrate.mjs", "gl-style-validate": "dist/gl-style-validate.mjs" } }, "sha512-IGJtuBbaGzOUgODdBRg66p8stnwj9iDXkgbYKoYcNiiQmaez5WVRfXm4b03MCDwmZyX93csbfHFWEJJYHnn5oA=="],
"@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/core": ["@meshtastic/core@workspace:packages/core"],
"@meshtastic/protobufs": ["@jsr/[email protected]", "https://npm.jsr.io/~/11/@jsr/meshtastic__protobufs/2.7.0.tgz", { "dependencies": { "@bufbuild/protobuf": "^2.2.3" } }, "sha512-ndZhUyB/ADSyjJI+iSeSOoIKqNGZ2+ERVjfY0qnh4jgF740tFTwefC5mzZhOqDLbreGFYS79+429NtH5Ujdzdg=="],
"@meshtastic/transport-deno": ["@meshtastic/transport-deno@workspace:packages/transport-deno"],
"@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/transport-http": ["@meshtastic/transport-http@workspace:packages/transport-http"],
"@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/transport-node": ["@meshtastic/transport-node@workspace:packages/transport-node"],
"@meshtastic/transport-web-serial": ["@jsr/[email protected]", "https://npm.jsr.io/~/11/@jsr/meshtastic__transport-web-serial/0.2.1.tgz", { "dependencies": { "@jsr/meshtastic__core": "^2.6.0" } }, "sha512-yumjEGLkAuJYOC3aWKvZzbQqi/LnqaKfNpVCY7Ki7oLtAshNiZrBLiwiFhN7+ZR9FfMdJThyBMqREBDRRWTO1Q=="], "@meshtastic/transport-web-bluetooth": ["@meshtastic/transport-web-bluetooth@workspace:packages/transport-web-bluetooth"],
"@meshtastic/transport-web-serial": ["@meshtastic/transport-web-serial@workspace:packages/transport-web-serial"],
"@noble/curves": ["@noble/[email protected]", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-HxngEd2XUcg9xi20JkwlLCtYwfoFw4JGkuZpT+WlsPD4gB/cxkvTD8fSsoAnphGZhFdZYKeQIPCuFlWPm1uE0g=="], "@noble/curves": ["@noble/[email protected]", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-HxngEd2XUcg9xi20JkwlLCtYwfoFw4JGkuZpT+WlsPD4gB/cxkvTD8fSsoAnphGZhFdZYKeQIPCuFlWPm1uE0g=="],
@ -781,7 +826,7 @@
"@types/mapbox__vector-tile": ["@types/[email protected]", "", { "dependencies": { "@types/geojson": "*", "@types/mapbox__point-geometry": "*", "@types/pbf": "*" } }, "sha512-bpd8dRn9pr6xKvuEBQup8pwQfD4VUyqO/2deGjfpe6AwC8YRlyEipvefyRJUSiCJTZuCb8Pl1ciVV5ekqJ96Bg=="], "@types/mapbox__vector-tile": ["@types/[email protected]", "", { "dependencies": { "@types/geojson": "*", "@types/mapbox__point-geometry": "*", "@types/pbf": "*" } }, "sha512-bpd8dRn9pr6xKvuEBQup8pwQfD4VUyqO/2deGjfpe6AwC8YRlyEipvefyRJUSiCJTZuCb8Pl1ciVV5ekqJ96Bg=="],
"@types/node": ["@types/node@24.0.14", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-4zXMWD91vBLGRtHK3YbIoFMia+1nqEz72coM42C5ETjnNCa/heoj7NT1G67iAfOqMmcfhuCZ4uNpyz8EjlAejw=="], "@types/node": ["@types/node@22.16.4", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-PYRhNtZdm2wH/NT2k/oAJ6/f2VD2N2Dag0lGlx2vWgMSJXGNmlce5MiTQzoWAiIJtso30mjnfQCOKVH+kAQC/g=="],
"@types/pbf": ["@types/[email protected]", "", {}, "sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA=="], "@types/pbf": ["@types/[email protected]", "", {}, "sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA=="],
@ -795,7 +840,7 @@
"@types/w3c-web-serial": ["@types/[email protected]", "", {}, "sha512-QQOT+bxQJhRGXoZDZGLs3ksLud1dMNnMiSQtBA0w8KXvLpXX4oM4TZb6J0GgJ8UbCaHo5s9/4VQT8uXy9JER2A=="], "@types/w3c-web-serial": ["@types/[email protected]", "", {}, "sha512-QQOT+bxQJhRGXoZDZGLs3ksLud1dMNnMiSQtBA0w8KXvLpXX4oM4TZb6J0GgJ8UbCaHo5s9/4VQT8uXy9JER2A=="],
"@types/web-bluetooth": ["@types/[email protected]1", "", {}, "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA=="], "@types/web-bluetooth": ["@types/[email protected]0", "", {}, "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow=="],
"@types/whatwg-mimetype": ["@types/[email protected]", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="], "@types/whatwg-mimetype": ["@types/[email protected]", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="],
@ -1335,7 +1380,7 @@
"typewise-core": ["[email protected]", "", {}, "sha512-2SCC/WLzj2SbUwzFOzqMCkz5amXLlxtJqDKTICqg30x+2DZxcfZN2MvQZmGfXWKNWaKK9pBPsvkcwv8bF/gxKg=="], "typewise-core": ["[email protected]", "", {}, "sha512-2SCC/WLzj2SbUwzFOzqMCkz5amXLlxtJqDKTICqg30x+2DZxcfZN2MvQZmGfXWKNWaKK9pBPsvkcwv8bF/gxKg=="],
"undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"union-value": ["[email protected]", "", { "dependencies": { "arr-union": "^3.1.0", "get-value": "^2.0.6", "is-extendable": "^0.1.1", "set-value": "^2.0.1" } }, "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg=="], "union-value": ["[email protected]", "", { "dependencies": { "arr-union": "^3.1.0", "get-value": "^2.0.6", "is-extendable": "^0.1.1", "set-value": "^2.0.1" } }, "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg=="],
@ -1393,6 +1438,12 @@
"zustand": ["[email protected]", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-ihAqNeUVhe0MAD+X8M5UzqyZ9k3FFZLBTtqo6JLPwV53cbRB/mJwBI0PxcIgqhBBHlEs8G45OTDTMq3gNcLq3A=="], "zustand": ["[email protected]", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-ihAqNeUVhe0MAD+X8M5UzqyZ9k3FFZLBTtqo6JLPwV53cbRB/mJwBI0PxcIgqhBBHlEs8G45OTDTMq3gNcLq3A=="],
"@jsr/meshtastic__core/@bufbuild/protobuf": ["@bufbuild/[email protected]", "", {}, "sha512-6cuonJVNOIL7lTj5zgo/Rc2bKAo4/GvN+rKCrUj7GdEHRzCk8zKOfFwUsL9nAVk5rSIsRmlgcpLzTRysopEeeg=="],
"@jsr/meshtastic__protobufs/@bufbuild/protobuf": ["@bufbuild/[email protected]", "", {}, "sha512-6cuonJVNOIL7lTj5zgo/Rc2bKAo4/GvN+rKCrUj7GdEHRzCk8zKOfFwUsL9nAVk5rSIsRmlgcpLzTRysopEeeg=="],
"@meshtastic/protobufs/@bufbuild/protobuf": ["@bufbuild/[email protected]", "", {}, "sha512-6cuonJVNOIL7lTj5zgo/Rc2bKAo4/GvN+rKCrUj7GdEHRzCk8zKOfFwUsL9nAVk5rSIsRmlgcpLzTRysopEeeg=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/[email protected]", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-A9CnAbC6ARNMKcIcrQwq6HeHCjpcBZ5wSx4U01WXCqEKlrzB9F9315WDNHkrs2xbx7YjjSxbUYxuN6EQzpcY2g=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/[email protected]", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-A9CnAbC6ARNMKcIcrQwq6HeHCjpcBZ5wSx4U01WXCqEKlrzB9F9315WDNHkrs2xbx7YjjSxbUYxuN6EQzpcY2g=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/[email protected]", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-hHyapA4A3gPaDCNfiqyZUStTMqIkKRshqPIuDOXv1hcBnD4U3l8cP0T1HMCfGRxQ6V64TGCcoswChANyOAwbQg=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/[email protected]", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-hHyapA4A3gPaDCNfiqyZUStTMqIkKRshqPIuDOXv1hcBnD4U3l8cP0T1HMCfGRxQ6V64TGCcoswChANyOAwbQg=="],
@ -1645,7 +1696,15 @@
"lru-cache/yallist": ["[email protected]", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], "lru-cache/yallist": ["[email protected]", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"meshtastic-web/@biomejs/biome": ["@biomejs/[email protected]", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.0.6", "@biomejs/cli-darwin-x64": "2.0.6", "@biomejs/cli-linux-arm64": "2.0.6", "@biomejs/cli-linux-arm64-musl": "2.0.6", "@biomejs/cli-linux-x64": "2.0.6", "@biomejs/cli-linux-x64-musl": "2.0.6", "@biomejs/cli-win32-arm64": "2.0.6", "@biomejs/cli-win32-x64": "2.0.6" }, "bin": { "biome": "bin/biome" } }, "sha512-RRP+9cdh5qwe2t0gORwXaa27oTOiQRQvrFf49x2PA1tnpsyU7FIHX4ZOFMtBC4QNtyWsN7Dqkf5EDbg4X+9iqA=="], "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-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-serial": ["@jsr/[email protected]", "https://npm.jsr.io/~/11/@jsr/meshtastic__transport-web-serial/0.2.1.tgz", { "dependencies": { "@jsr/meshtastic__core": "^2.6.0" } }, "sha512-yumjEGLkAuJYOC3aWKvZzbQqi/LnqaKfNpVCY7Ki7oLtAshNiZrBLiwiFhN7+ZR9FfMdJThyBMqREBDRRWTO1Q=="],
"meshtastic-web/@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-4zXMWD91vBLGRtHK3YbIoFMia+1nqEz72coM42C5ETjnNCa/heoj7NT1G67iAfOqMmcfhuCZ4uNpyz8EjlAejw=="],
"peek-stream/through2": ["[email protected]", "", { "dependencies": { "readable-stream": "~2.3.6", "xtend": "~4.0.1" } }, "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ=="], "peek-stream/through2": ["[email protected]", "", { "dependencies": { "readable-stream": "~2.3.6", "xtend": "~4.0.1" } }, "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ=="],
@ -1689,23 +1748,9 @@
"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=="],
"happy-dom/@types/node/undici-types": ["[email protected]", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], "meshtastic-web/@meshtastic/core/@bufbuild/protobuf": ["@bufbuild/[email protected]", "", {}, "sha512-6cuonJVNOIL7lTj5zgo/Rc2bKAo4/GvN+rKCrUj7GdEHRzCk8zKOfFwUsL9nAVk5rSIsRmlgcpLzTRysopEeeg=="],
"meshtastic-web/@biomejs/biome/@biomejs/cli-darwin-arm64": ["@biomejs/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-AzdiNNjNzsE6LfqWyBvcL29uWoIuZUkndu+wwlXW13EKcBHbbKjNQEZIJKYDc6IL+p7bmWGx3v9ZtcRyIoIz5A=="],
"meshtastic-web/@biomejs/biome/@biomejs/cli-darwin-x64": ["@biomejs/[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-wJjjP4E7bO4WJmiQaLnsdXMa516dbtC6542qeRkyJg0MqMXP0fvs4gdsHhZ7p9XWTAmGIjZHFKXdsjBvKGIJJQ=="],
"meshtastic-web/@biomejs/biome/@biomejs/cli-linux-arm64": ["@biomejs/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-ZSVf6TYo5rNMUHIW1tww+rs/krol7U5A1Is/yzWyHVZguuB0lBnIodqyFuwCNqG9aJGyk7xIMS8HG0qGUPz0SA=="],
"meshtastic-web/@biomejs/biome/@biomejs/cli-linux-arm64-musl": ["@biomejs/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-CVPEMlin3bW49sBqLBg2x016Pws7eUXA27XYDFlEtponD0luYjg2zQaMJ2nOqlkKG9fqzzkamdYxHdMDc2gZFw=="],
"meshtastic-web/@biomejs/biome/@biomejs/cli-linux-x64": ["@biomejs/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-geM1MkHTV1Kh2Cs/Xzot9BOF3WBacihw6bkEmxkz4nSga8B9/hWy5BDiOG3gHDGIBa8WxT0nzsJs2f/hPqQIQw=="],
"meshtastic-web/@biomejs/biome/@biomejs/cli-linux-x64-musl": ["@biomejs/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-mKHE/e954hR/hSnAcJSjkf4xGqZc/53Kh39HVW1EgO5iFi0JutTN07TSjEMg616julRtfSNJi0KNyxvc30Y4rQ=="],
"meshtastic-web/@biomejs/biome/@biomejs/cli-win32-arm64": ["@biomejs/[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-290V4oSFoKaprKE1zkYVsDfAdn0An5DowZ+GIABgjoq1ndhvNxkJcpxPsiYtT7slbVe3xmlT0ncdfOsN7KruzA=="],
"meshtastic-web/@biomejs/biome/@biomejs/cli-win32-x64": ["@biomejs/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-bfM1Bce0d69Ao7pjTjUS+AWSZ02+5UHdiAP85Th8e9yV5xzw6JrHXbL5YWlcEKQ84FIZMdDc7ncuti1wd2sdbw=="], "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=="],

5472
deno.lock

File diff suppressed because it is too large

23
package.json

@ -12,13 +12,28 @@
"url": "https://github.com/meshtastic/web/issues" "url": "https://github.com/meshtastic/web/issues"
}, },
"homepage": "https://meshtastic.org", "homepage": "https://meshtastic.org",
"workspaces": ["packages/web"], "workspaces": ["packages/*"],
"simple-git-hooks": {
"pre-commit": "bun run check:fix"
},
"scripts": { "scripts": {
"lint": "biome lint",
"lint:fix": "biome lint --write",
"format": "biome format",
"format:fix": "biome format . --write",
"check": "biome check",
"check:fix": "biome check --write",
"build:npm": "deno run -A scripts/build_npm_package.ts" "build:npm": "deno run -A scripts/build_npm_package.ts"
}, },
"dependencies": {
"@bufbuild/protobuf": "^2.6.1",
"@meshtastic/protobufs": "npm:@jsr/meshtastic__protobufs",
"ste-simple-events": "^3.0.11",
"tslog": "^4.9.3"
},
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^1.8.3", "bun": "^1.2.18",
"bun": "^1.1.18", "typescript": "^5.8.3",
"typescript": "^5.8.3" "@types/node": "^22.16.4"
} }
} }

11
packages/core/deno.json

@ -1,11 +0,0 @@
{
"name": "@meshtastic/core",
"version": "2.6.4",
"description": "Core functionalities for Meshtastic web applications.",
"exports": {
".": "./mod.ts"
},
"imports": {
"crc": "npm:crc@^4.3.2"
}
}

11
packages/core/package.json

@ -0,0 +1,11 @@
{
"name": "@meshtastic/core",
"version": "2.6.5",
"description": "Core functionalities for Meshtastic web applications.",
"exports": {
".": "./mod.ts"
},
"dependencies": {
"crc": "npm:crc@^4.3.2"
}
}

4
packages/core/src/constants.ts

@ -5,6 +5,6 @@ const broadcastNum = 0xffffffff;
const minFwVer = 2.2; const minFwVer = 2.2;
export const Constants = { export const Constants = {
broadcastNum, broadcastNum,
minFwVer, minFwVer,
}; };

2296
packages/core/src/meshDevice.ts

File diff suppressed because it is too large

168
packages/core/src/types.ts

@ -1,45 +1,45 @@
import type * as Protobuf from "@meshtastic/protobufs"; import type * as Protobuf from "@meshtastic/protobufs";
interface Packet { interface Packet {
type: "packet"; type: "packet";
data: Uint8Array; data: Uint8Array;
} }
interface DebugLog { interface DebugLog {
type: "debug"; type: "debug";
data: string; data: string;
} }
export type DeviceOutput = Packet | DebugLog; export type DeviceOutput = Packet | DebugLog;
export interface Transport { export interface Transport {
toDevice: WritableStream<Uint8Array>; toDevice: WritableStream<Uint8Array>;
fromDevice: ReadableStream<DeviceOutput>; fromDevice: ReadableStream<DeviceOutput>;
} }
export interface QueueItem { export interface QueueItem {
id: number; id: number;
data: Uint8Array; data: Uint8Array;
sent: boolean; sent: boolean;
added: Date; added: Date;
promise: Promise<number>; promise: Promise<number>;
} }
export interface HttpRetryConfig { export interface HttpRetryConfig {
maxRetries: number; maxRetries: number;
initialDelayMs: number; initialDelayMs: number;
maxDelayMs: number; maxDelayMs: number;
backoffFactor: number; backoffFactor: number;
} }
export enum DeviceStatusEnum { export enum DeviceStatusEnum {
DeviceRestarting = 1, DeviceRestarting = 1,
DeviceDisconnected = 2, DeviceDisconnected = 2,
DeviceConnecting = 3, DeviceConnecting = 3,
DeviceReconnecting = 4, DeviceReconnecting = 4,
DeviceConnected = 5, DeviceConnected = 5,
DeviceConfiguring = 6, DeviceConfiguring = 6,
DeviceConfigured = 7, DeviceConfigured = 7,
} }
export type LogEventPacket = LogEvent & { date: Date }; export type LogEventPacket = LogEvent & { date: Date };
@ -47,83 +47,83 @@ export type LogEventPacket = LogEvent & { date: Date };
export type PacketDestination = "broadcast" | "direct"; export type PacketDestination = "broadcast" | "direct";
export interface PacketMetadata<T> { export interface PacketMetadata<T> {
id: number; id: number;
rxTime: Date; rxTime: Date;
type: PacketDestination; type: PacketDestination;
from: number; from: number;
to: number; to: number;
channel: ChannelNumber; channel: ChannelNumber;
data: T; data: T;
} }
export enum EmitterScope { export enum EmitterScope {
MeshDevice = 1, MeshDevice = 1,
SerialConnection = 2, SerialConnection = 2,
NodeSerialConnection = 3, NodeSerialConnection = 3,
BleConnection = 4, BleConnection = 4,
HttpConnection = 5, HttpConnection = 5,
} }
export enum Emitter { export enum Emitter {
Constructor = 0, Constructor = 0,
SendText = 1, SendText = 1,
SendWaypoint = 2, SendWaypoint = 2,
SendPacket = 3, SendPacket = 3,
SendRaw = 4, SendRaw = 4,
SetConfig = 5, SetConfig = 5,
SetModuleConfig = 6, SetModuleConfig = 6,
ConfirmSetConfig = 7, ConfirmSetConfig = 7,
SetOwner = 8, SetOwner = 8,
SetChannel = 9, SetChannel = 9,
ConfirmSetChannel = 10, ConfirmSetChannel = 10,
ClearChannel = 11, ClearChannel = 11,
GetChannel = 12, GetChannel = 12,
GetAllChannels = 13, GetAllChannels = 13,
GetConfig = 14, GetConfig = 14,
GetModuleConfig = 15, GetModuleConfig = 15,
GetOwner = 16, GetOwner = 16,
Configure = 17, Configure = 17,
HandleFromRadio = 18, HandleFromRadio = 18,
HandleMeshPacket = 19, HandleMeshPacket = 19,
Connect = 20, Connect = 20,
Ping = 21, Ping = 21,
ReadFromRadio = 22, ReadFromRadio = 22,
WriteToRadio = 23, WriteToRadio = 23,
SetDebugMode = 24, SetDebugMode = 24,
GetMetadata = 25, GetMetadata = 25,
ResetNodes = 26, ResetNodes = 26,
Shutdown = 27, Shutdown = 27,
Reboot = 28, Reboot = 28,
RebootOta = 29, RebootOta = 29,
FactoryReset = 30, FactoryReset = 30,
EnterDfuMode = 31, EnterDfuMode = 31,
RemoveNodeByNum = 32, RemoveNodeByNum = 32,
SetCannedMessages = 33, SetCannedMessages = 33,
Disconnect = 34, Disconnect = 34,
} }
export interface LogEvent { export interface LogEvent {
scope: EmitterScope; scope: EmitterScope;
emitter: Emitter; emitter: Emitter;
message: string; message: string;
level: Protobuf.Mesh.LogRecord_Level; level: Protobuf.Mesh.LogRecord_Level;
packet?: Uint8Array; packet?: Uint8Array;
} }
export enum ChannelNumber { export enum ChannelNumber {
Primary = 0, Primary = 0,
Channel1 = 1, Channel1 = 1,
Channel2 = 2, Channel2 = 2,
Channel3 = 3, Channel3 = 3,
Channel4 = 4, Channel4 = 4,
Channel5 = 5, Channel5 = 5,
Channel6 = 6, Channel6 = 6,
Admin = 7, Admin = 7,
} }
export type Destination = number | "self" | "broadcast"; export type Destination = number | "self" | "broadcast";
export interface PacketError { export interface PacketError {
id: number; id: number;
error: Protobuf.Mesh.Routing_Error; error: Protobuf.Mesh.Routing_Error;
} }

748
packages/core/src/utils/eventSystem.ts

@ -4,378 +4,378 @@ import type { PacketMetadata } from "../types.ts";
import type * as Types from "../types.ts"; import type * as Types from "../types.ts";
export class EventSystem { export class EventSystem {
/** /**
* Fires when a new FromRadio message has been received from the device * Fires when a new FromRadio message has been received from the device
* *
* @event onLogEvent * @event onLogEvent
*/ */
public readonly onLogEvent: SimpleEventDispatcher<Types.LogEventPacket> = public readonly onLogEvent: SimpleEventDispatcher<Types.LogEventPacket> =
new SimpleEventDispatcher<Types.LogEventPacket>(); new SimpleEventDispatcher<Types.LogEventPacket>();
/** /**
* Fires when a new FromRadio message has been received from the device * Fires when a new FromRadio message has been received from the device
* *
* @event onFromRadio * @event onFromRadio
*/ */
public readonly onFromRadio: SimpleEventDispatcher<Protobuf.Mesh.FromRadio> = public readonly onFromRadio: SimpleEventDispatcher<Protobuf.Mesh.FromRadio> =
new SimpleEventDispatcher<Protobuf.Mesh.FromRadio>(); new SimpleEventDispatcher<Protobuf.Mesh.FromRadio>();
/** /**
* Fires when a new FromRadio message containing a Data packet has been * Fires when a new FromRadio message containing a Data packet has been
* received from the device * received from the device
* *
* @event onMeshPacket * @event onMeshPacket
*/ */
public readonly onMeshPacket: SimpleEventDispatcher<Protobuf.Mesh.MeshPacket> = public readonly onMeshPacket: SimpleEventDispatcher<Protobuf.Mesh.MeshPacket> =
new SimpleEventDispatcher<Protobuf.Mesh.MeshPacket>(); new SimpleEventDispatcher<Protobuf.Mesh.MeshPacket>();
/** /**
* Fires when a new MyNodeInfo message has been received from the device * Fires when a new MyNodeInfo message has been received from the device
* *
* @event onMyNodeInfo * @event onMyNodeInfo
*/ */
public readonly onMyNodeInfo: SimpleEventDispatcher<Protobuf.Mesh.MyNodeInfo> = public readonly onMyNodeInfo: SimpleEventDispatcher<Protobuf.Mesh.MyNodeInfo> =
new SimpleEventDispatcher<Protobuf.Mesh.MyNodeInfo>(); new SimpleEventDispatcher<Protobuf.Mesh.MyNodeInfo>();
/** /**
* Fires when a new MeshPacket message containing a NodeInfo packet has been * Fires when a new MeshPacket message containing a NodeInfo packet has been
* received from device * received from device
* *
* @event onNodeInfoPacket * @event onNodeInfoPacket
*/ */
public readonly onNodeInfoPacket: SimpleEventDispatcher<Protobuf.Mesh.NodeInfo> = public readonly onNodeInfoPacket: SimpleEventDispatcher<Protobuf.Mesh.NodeInfo> =
new SimpleEventDispatcher<Protobuf.Mesh.NodeInfo>(); new SimpleEventDispatcher<Protobuf.Mesh.NodeInfo>();
/** /**
* Fires when a new Channel message is received * Fires when a new Channel message is received
* *
* @event onChannelPacket * @event onChannelPacket
*/ */
public readonly onChannelPacket: SimpleEventDispatcher<Protobuf.Channel.Channel> = public readonly onChannelPacket: SimpleEventDispatcher<Protobuf.Channel.Channel> =
new SimpleEventDispatcher<Protobuf.Channel.Channel>(); new SimpleEventDispatcher<Protobuf.Channel.Channel>();
/** /**
* Fires when a new Config message is received * Fires when a new Config message is received
* *
* @event onConfigPacket * @event onConfigPacket
*/ */
public readonly onConfigPacket: SimpleEventDispatcher<Protobuf.Config.Config> = public readonly onConfigPacket: SimpleEventDispatcher<Protobuf.Config.Config> =
new SimpleEventDispatcher<Protobuf.Config.Config>(); new SimpleEventDispatcher<Protobuf.Config.Config>();
/** /**
* Fires when a new ModuleConfig message is received * Fires when a new ModuleConfig message is received
* *
* @event onModuleConfigPacket * @event onModuleConfigPacket
*/ */
public readonly onModuleConfigPacket: SimpleEventDispatcher<Protobuf.ModuleConfig.ModuleConfig> = public readonly onModuleConfigPacket: SimpleEventDispatcher<Protobuf.ModuleConfig.ModuleConfig> =
new SimpleEventDispatcher<Protobuf.ModuleConfig.ModuleConfig>(); new SimpleEventDispatcher<Protobuf.ModuleConfig.ModuleConfig>();
/** /**
* Fires when a new MeshPacket message containing a ATAK packet has been * Fires when a new MeshPacket message containing a ATAK packet has been
* received from device * received from device
* *
* @event onAtakPacket * @event onAtakPacket
*/ */
public readonly onAtakPacket: SimpleEventDispatcher< public readonly onAtakPacket: SimpleEventDispatcher<
PacketMetadata<Uint8Array> PacketMetadata<Uint8Array>
> = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>(); > = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
/** /**
* Fires when a new MeshPacket message containing a Text packet has been * Fires when a new MeshPacket message containing a Text packet has been
* received from device * received from device
* *
* @event onMessagePacket * @event onMessagePacket
*/ */
public readonly onMessagePacket: SimpleEventDispatcher< public readonly onMessagePacket: SimpleEventDispatcher<
PacketMetadata<string> PacketMetadata<string>
> = new SimpleEventDispatcher<PacketMetadata<string>>(); > = new SimpleEventDispatcher<PacketMetadata<string>>();
/** /**
* Fires when a new MeshPacket message containing a Remote Hardware packet has * Fires when a new MeshPacket message containing a Remote Hardware packet has
* been received from device * been received from device
* *
* @event onRemoteHardwarePacket * @event onRemoteHardwarePacket
*/ */
public readonly onRemoteHardwarePacket: SimpleEventDispatcher< public readonly onRemoteHardwarePacket: SimpleEventDispatcher<
PacketMetadata<Protobuf.RemoteHardware.HardwareMessage> PacketMetadata<Protobuf.RemoteHardware.HardwareMessage>
> = new SimpleEventDispatcher< > = new SimpleEventDispatcher<
PacketMetadata<Protobuf.RemoteHardware.HardwareMessage> PacketMetadata<Protobuf.RemoteHardware.HardwareMessage>
>(); >();
/** /**
* Fires when a new MeshPacket message containing a Position packet has been * Fires when a new MeshPacket message containing a Position packet has been
* received from device * received from device
* *
* @event onPositionPacket * @event onPositionPacket
*/ */
public readonly onPositionPacket: SimpleEventDispatcher< public readonly onPositionPacket: SimpleEventDispatcher<
PacketMetadata<Protobuf.Mesh.Position> PacketMetadata<Protobuf.Mesh.Position>
> = new SimpleEventDispatcher<PacketMetadata<Protobuf.Mesh.Position>>(); > = new SimpleEventDispatcher<PacketMetadata<Protobuf.Mesh.Position>>();
/** /**
* Fires when a new MeshPacket message containing a User packet has been * Fires when a new MeshPacket message containing a User packet has been
* received from device * received from device
* *
* @event onUserPacket * @event onUserPacket
*/ */
public readonly onUserPacket: SimpleEventDispatcher< public readonly onUserPacket: SimpleEventDispatcher<
PacketMetadata<Protobuf.Mesh.User> PacketMetadata<Protobuf.Mesh.User>
> = new SimpleEventDispatcher<PacketMetadata<Protobuf.Mesh.User>>(); > = new SimpleEventDispatcher<PacketMetadata<Protobuf.Mesh.User>>();
/** /**
* Fires when a new MeshPacket message containing a Routing packet has been * Fires when a new MeshPacket message containing a Routing packet has been
* received from device * received from device
* *
* @event onRoutingPacket * @event onRoutingPacket
*/ */
public readonly onRoutingPacket: SimpleEventDispatcher< public readonly onRoutingPacket: SimpleEventDispatcher<
PacketMetadata<Protobuf.Mesh.Routing> PacketMetadata<Protobuf.Mesh.Routing>
> = new SimpleEventDispatcher<PacketMetadata<Protobuf.Mesh.Routing>>(); > = new SimpleEventDispatcher<PacketMetadata<Protobuf.Mesh.Routing>>();
/** /**
* Fires when the device receives a Metadata packet * Fires when the device receives a Metadata packet
* *
* @event onDeviceMetadataPacket * @event onDeviceMetadataPacket
*/ */
public readonly onDeviceMetadataPacket: SimpleEventDispatcher< public readonly onDeviceMetadataPacket: SimpleEventDispatcher<
PacketMetadata<Protobuf.Mesh.DeviceMetadata> PacketMetadata<Protobuf.Mesh.DeviceMetadata>
> = new SimpleEventDispatcher<PacketMetadata<Protobuf.Mesh.DeviceMetadata>>(); > = new SimpleEventDispatcher<PacketMetadata<Protobuf.Mesh.DeviceMetadata>>();
/** /**
* Fires when the device receives a Canned Message Module message packet * Fires when the device receives a Canned Message Module message packet
* *
* @event onCannedMessageModulePacket * @event onCannedMessageModulePacket
*/ */
public readonly onCannedMessageModulePacket: SimpleEventDispatcher< public readonly onCannedMessageModulePacket: SimpleEventDispatcher<
PacketMetadata<string> PacketMetadata<string>
> = new SimpleEventDispatcher<PacketMetadata<string>>(); > = new SimpleEventDispatcher<PacketMetadata<string>>();
/** /**
* Fires when a new MeshPacket message containing a Waypoint packet has been * Fires when a new MeshPacket message containing a Waypoint packet has been
* received from device * received from device
* *
* @event onWaypointPacket * @event onWaypointPacket
*/ */
public readonly onWaypointPacket: SimpleEventDispatcher< public readonly onWaypointPacket: SimpleEventDispatcher<
PacketMetadata<Protobuf.Mesh.Waypoint> PacketMetadata<Protobuf.Mesh.Waypoint>
> = new SimpleEventDispatcher<PacketMetadata<Protobuf.Mesh.Waypoint>>(); > = new SimpleEventDispatcher<PacketMetadata<Protobuf.Mesh.Waypoint>>();
/** /**
* Fires when a new MeshPacket message containing an Audio packet has been * Fires when a new MeshPacket message containing an Audio packet has been
* received from device * received from device
* *
* @event onAudioPacket * @event onAudioPacket
*/ */
public readonly onAudioPacket: SimpleEventDispatcher< public readonly onAudioPacket: SimpleEventDispatcher<
PacketMetadata<Uint8Array> PacketMetadata<Uint8Array>
> = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>(); > = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
/** /**
* Fires when a new MeshPacket message containing a Detection Sensor packet has been * Fires when a new MeshPacket message containing a Detection Sensor packet has been
* received from device * received from device
* *
* @event onDetectionSensorPacket * @event onDetectionSensorPacket
*/ */
public readonly onDetectionSensorPacket: SimpleEventDispatcher< public readonly onDetectionSensorPacket: SimpleEventDispatcher<
PacketMetadata<Uint8Array> PacketMetadata<Uint8Array>
> = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>(); > = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
/** /**
* Fires when a new MeshPacket message containing a Ping packet has been * Fires when a new MeshPacket message containing a Ping packet has been
* received from device * received from device
* *
* @event onPingPacket * @event onPingPacket
*/ */
public readonly onPingPacket: SimpleEventDispatcher< public readonly onPingPacket: SimpleEventDispatcher<
PacketMetadata<Uint8Array> PacketMetadata<Uint8Array>
> = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>(); > = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
/** /**
* Fires when a new MeshPacket message containing a IP Tunnel packet has been * Fires when a new MeshPacket message containing a IP Tunnel packet has been
* received from device * received from device
* *
* @event onIpTunnelPacket * @event onIpTunnelPacket
*/ */
public readonly onIpTunnelPacket: SimpleEventDispatcher< public readonly onIpTunnelPacket: SimpleEventDispatcher<
PacketMetadata<Uint8Array> PacketMetadata<Uint8Array>
> = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>(); > = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
/** /**
* Fires when a new MeshPacket message containing a Paxcounter packet has been * Fires when a new MeshPacket message containing a Paxcounter packet has been
* received from device * received from device
* *
* @event onPaxcounterPacket * @event onPaxcounterPacket
*/ */
public readonly onPaxcounterPacket: SimpleEventDispatcher< public readonly onPaxcounterPacket: SimpleEventDispatcher<
PacketMetadata<Protobuf.PaxCount.Paxcount> PacketMetadata<Protobuf.PaxCount.Paxcount>
> = new SimpleEventDispatcher<PacketMetadata<Protobuf.PaxCount.Paxcount>>(); > = new SimpleEventDispatcher<PacketMetadata<Protobuf.PaxCount.Paxcount>>();
/** /**
* Fires when a new MeshPacket message containing a Serial packet has been * Fires when a new MeshPacket message containing a Serial packet has been
* received from device * received from device
* *
* @event onSerialPacket * @event onSerialPacket
*/ */
public readonly onSerialPacket: SimpleEventDispatcher< public readonly onSerialPacket: SimpleEventDispatcher<
PacketMetadata<Uint8Array> PacketMetadata<Uint8Array>
> = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>(); > = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
/** /**
* Fires when a new MeshPacket message containing a Store and Forward packet * Fires when a new MeshPacket message containing a Store and Forward packet
* has been received from device * has been received from device
* *
* @event onStoreForwardPacket * @event onStoreForwardPacket
*/ */
public readonly onStoreForwardPacket: SimpleEventDispatcher< public readonly onStoreForwardPacket: SimpleEventDispatcher<
PacketMetadata<Uint8Array> PacketMetadata<Uint8Array>
> = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>(); > = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
/** /**
* Fires when a new MeshPacket message containing a Store and Forward packet * Fires when a new MeshPacket message containing a Store and Forward packet
* has been received from device * has been received from device
* *
* @event onRangeTestPacket * @event onRangeTestPacket
*/ */
public readonly onRangeTestPacket: SimpleEventDispatcher< public readonly onRangeTestPacket: SimpleEventDispatcher<
PacketMetadata<Uint8Array> PacketMetadata<Uint8Array>
> = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>(); > = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
/** /**
* Fires when a new MeshPacket message containing a Telemetry packet has been * Fires when a new MeshPacket message containing a Telemetry packet has been
* received from device * received from device
* *
* @event onTelemetryPacket * @event onTelemetryPacket
*/ */
public readonly onTelemetryPacket: SimpleEventDispatcher< public readonly onTelemetryPacket: SimpleEventDispatcher<
PacketMetadata<Protobuf.Telemetry.Telemetry> PacketMetadata<Protobuf.Telemetry.Telemetry>
> = new SimpleEventDispatcher<PacketMetadata<Protobuf.Telemetry.Telemetry>>(); > = new SimpleEventDispatcher<PacketMetadata<Protobuf.Telemetry.Telemetry>>();
/** /**
* Fires when a new MeshPacket message containing a ZPS packet has been * Fires when a new MeshPacket message containing a ZPS packet has been
* received from device * received from device
* *
* @event onZPSPacket * @event onZPSPacket
*/ */
public readonly onZpsPacket: SimpleEventDispatcher< public readonly onZpsPacket: SimpleEventDispatcher<
PacketMetadata<Uint8Array> PacketMetadata<Uint8Array>
> = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>(); > = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
/** /**
* Fires when a new MeshPacket message containing a Simulator packet has been * Fires when a new MeshPacket message containing a Simulator packet has been
* received from device * received from device
* *
* @event onSimulatorPacket * @event onSimulatorPacket
*/ */
public readonly onSimulatorPacket: SimpleEventDispatcher< public readonly onSimulatorPacket: SimpleEventDispatcher<
PacketMetadata<Uint8Array> PacketMetadata<Uint8Array>
> = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>(); > = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
/** /**
* Fires when a new MeshPacket message containing a Trace Route packet has been * Fires when a new MeshPacket message containing a Trace Route packet has been
* received from device * received from device
* *
* @event onTraceRoutePacket * @event onTraceRoutePacket
*/ */
public readonly onTraceRoutePacket: SimpleEventDispatcher< public readonly onTraceRoutePacket: SimpleEventDispatcher<
PacketMetadata<Protobuf.Mesh.RouteDiscovery> PacketMetadata<Protobuf.Mesh.RouteDiscovery>
> = new SimpleEventDispatcher<PacketMetadata<Protobuf.Mesh.RouteDiscovery>>(); > = new SimpleEventDispatcher<PacketMetadata<Protobuf.Mesh.RouteDiscovery>>();
/** /**
* Fires when a new MeshPacket message containing a Neighbor Info packet has been * Fires when a new MeshPacket message containing a Neighbor Info packet has been
* received from device * received from device
* *
* @event onNeighborInfoPacket * @event onNeighborInfoPacket
*/ */
public readonly onNeighborInfoPacket: SimpleEventDispatcher< public readonly onNeighborInfoPacket: SimpleEventDispatcher<
PacketMetadata<Protobuf.Mesh.NeighborInfo> PacketMetadata<Protobuf.Mesh.NeighborInfo>
> = new SimpleEventDispatcher<PacketMetadata<Protobuf.Mesh.NeighborInfo>>(); > = new SimpleEventDispatcher<PacketMetadata<Protobuf.Mesh.NeighborInfo>>();
/** /**
* Fires when a new MeshPacket message containing an ATAK packet has been * Fires when a new MeshPacket message containing an ATAK packet has been
* received from device * received from device
* *
* @event onAtakPluginPacket * @event onAtakPluginPacket
*/ */
public readonly onAtakPluginPacket: SimpleEventDispatcher< public readonly onAtakPluginPacket: SimpleEventDispatcher<
PacketMetadata<Uint8Array> PacketMetadata<Uint8Array>
> = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>(); > = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
/** /**
* Fires when a new MeshPacket message containing a Map Report packet has been * Fires when a new MeshPacket message containing a Map Report packet has been
* received from device * received from device
* *
* @event onMapReportPacket * @event onMapReportPacket
*/ */
public readonly onMapReportPacket: SimpleEventDispatcher< public readonly onMapReportPacket: SimpleEventDispatcher<
PacketMetadata<Uint8Array> PacketMetadata<Uint8Array>
> = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>(); > = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
/** /**
* Fires when a new MeshPacket message containing a Private packet has been * Fires when a new MeshPacket message containing a Private packet has been
* received from device * received from device
* *
* @event onPrivatePacket * @event onPrivatePacket
*/ */
public readonly onPrivatePacket: SimpleEventDispatcher< public readonly onPrivatePacket: SimpleEventDispatcher<
PacketMetadata<Uint8Array> PacketMetadata<Uint8Array>
> = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>(); > = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
/** /**
* Fires when a new MeshPacket message containing an ATAK Forwarder packet has been * Fires when a new MeshPacket message containing an ATAK Forwarder packet has been
* received from device * received from device
* *
* @event onAtakForwarderPacket * @event onAtakForwarderPacket
*/ */
public readonly onAtakForwarderPacket: SimpleEventDispatcher< public readonly onAtakForwarderPacket: SimpleEventDispatcher<
PacketMetadata<Uint8Array> PacketMetadata<Uint8Array>
> = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>(); > = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
/** /**
* Fires when the devices connection or configuration status changes * Fires when the devices connection or configuration status changes
* *
* @event onDeviceStatus * @event onDeviceStatus
*/ */
public readonly onDeviceStatus: SimpleEventDispatcher<Types.DeviceStatusEnum> = public readonly onDeviceStatus: SimpleEventDispatcher<Types.DeviceStatusEnum> =
new SimpleEventDispatcher<Types.DeviceStatusEnum>(); new SimpleEventDispatcher<Types.DeviceStatusEnum>();
/** /**
* Fires when a new FromRadio message containing a LogRecord packet has been * Fires when a new FromRadio message containing a LogRecord packet has been
* received from device * received from device
* *
* @event onLogRecord * @event onLogRecord
*/ */
public readonly onLogRecord: SimpleEventDispatcher<Protobuf.Mesh.LogRecord> = public readonly onLogRecord: SimpleEventDispatcher<Protobuf.Mesh.LogRecord> =
new SimpleEventDispatcher<Protobuf.Mesh.LogRecord>(); new SimpleEventDispatcher<Protobuf.Mesh.LogRecord>();
/** /**
* Fires when the device receives a meshPacket, returns a timestamp * Fires when the device receives a meshPacket, returns a timestamp
* *
* @event onMeshHeartbeat * @event onMeshHeartbeat
*/ */
public readonly onMeshHeartbeat: SimpleEventDispatcher<Date> = public readonly onMeshHeartbeat: SimpleEventDispatcher<Date> =
new SimpleEventDispatcher<Date>(); new SimpleEventDispatcher<Date>();
/** /**
* Outputs any debug log data (currently serial connections only) * Outputs any debug log data (currently serial connections only)
* *
* @event onDeviceDebugLog * @event onDeviceDebugLog
*/ */
public readonly onDeviceDebugLog: SimpleEventDispatcher<Uint8Array> = public readonly onDeviceDebugLog: SimpleEventDispatcher<Uint8Array> =
new SimpleEventDispatcher<Uint8Array>(); new SimpleEventDispatcher<Uint8Array>();
/** /**
* Outputs status of pending settings changes * Outputs status of pending settings changes
* *
* @event onpendingSettingsChange * @event onpendingSettingsChange
*/ */
public readonly onPendingSettingsChange: SimpleEventDispatcher<boolean> = public readonly onPendingSettingsChange: SimpleEventDispatcher<boolean> =
new SimpleEventDispatcher<boolean>(); new SimpleEventDispatcher<boolean>();
/** /**
* Fires when a QueueStatus message is generated * Fires when a QueueStatus message is generated
* *
* @event onQueueStatus * @event onQueueStatus
*/ */
public readonly onQueueStatus: SimpleEventDispatcher<Protobuf.Mesh.QueueStatus> = public readonly onQueueStatus: SimpleEventDispatcher<Protobuf.Mesh.QueueStatus> =
new SimpleEventDispatcher<Protobuf.Mesh.QueueStatus>(); new SimpleEventDispatcher<Protobuf.Mesh.QueueStatus>();
} }

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

@ -4,116 +4,116 @@ import { SimpleEventDispatcher } from "ste-simple-events";
import type { PacketError, QueueItem } from "../types.ts"; import type { PacketError, QueueItem } from "../types.ts";
export class Queue { export class Queue {
private queue: QueueItem[] = []; private queue: QueueItem[] = [];
private lock = false; private lock = false;
private ackNotifier = new SimpleEventDispatcher<number>(); private ackNotifier = new SimpleEventDispatcher<number>();
private errorNotifier = new SimpleEventDispatcher<PacketError>(); private errorNotifier = new SimpleEventDispatcher<PacketError>();
private timeout: number; private timeout: number;
constructor() { constructor() {
this.timeout = 60000; this.timeout = 60000;
} }
public getState(): QueueItem[] { public getState(): QueueItem[] {
return this.queue; return this.queue;
} }
public clear(): void { public clear(): void {
this.queue = []; this.queue = [];
} }
public push(item: Omit<QueueItem, "promise" | "sent" | "added">): void { public push(item: Omit<QueueItem, "promise" | "sent" | "added">): void {
const queueItem: QueueItem = { const queueItem: QueueItem = {
...item, ...item,
sent: false, sent: false,
added: new Date(), added: new Date(),
promise: new Promise<number>((resolve, reject) => { promise: new Promise<number>((resolve, reject) => {
this.ackNotifier.subscribe((id) => { this.ackNotifier.subscribe((id) => {
if (item.id === id) { if (item.id === id) {
this.remove(item.id); this.remove(item.id);
resolve(id); resolve(id);
} }
}); });
this.errorNotifier.subscribe((e) => { this.errorNotifier.subscribe((e) => {
if (item.id === e.id) { if (item.id === e.id) {
this.remove(item.id); this.remove(item.id);
reject(e); reject(e);
} }
}); });
setTimeout(() => { setTimeout(() => {
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);
console.warn( console.warn(
`Packet ${item.id} of type ${decoded.payloadVariant.case} timed out`, `Packet ${item.id} of type ${decoded.payloadVariant.case} timed out`,
); );
reject({ reject({
id: item.id, id: item.id,
error: Protobuf.Mesh.Routing_Error.TIMEOUT, error: Protobuf.Mesh.Routing_Error.TIMEOUT,
}); });
} }
}, this.timeout); }, this.timeout);
}), }),
}; };
this.queue.push(queueItem); this.queue.push(queueItem);
} }
public remove(id: number): void { public remove(id: number): void {
if (this.lock) { if (this.lock) {
setTimeout(() => this.remove(id), 100); setTimeout(() => this.remove(id), 100);
return; return;
} }
this.queue = this.queue.filter((item) => item.id !== id); this.queue = this.queue.filter((item) => item.id !== id);
} }
public processAck(id: number): void { public processAck(id: number): void {
this.ackNotifier.dispatch(id); this.ackNotifier.dispatch(id);
} }
public processError(e: PacketError): void { public processError(e: PacketError): void {
console.error( console.error(
`Error received for packet ${e.id}: ${ `Error received for packet ${e.id}: ${
Protobuf.Mesh.Routing_Error[e.error] Protobuf.Mesh.Routing_Error[e.error]
}`, }`,
); );
this.errorNotifier.dispatch(e); this.errorNotifier.dispatch(e);
} }
public wait(id: number): Promise<number> { public wait(id: number): Promise<number> {
const queueItem = this.queue.find((qi) => qi.id === id); const queueItem = this.queue.find((qi) => qi.id === id);
if (!queueItem) { if (!queueItem) {
throw new Error("Packet does not exist"); throw new Error("Packet does not exist");
} }
return queueItem.promise; return queueItem.promise;
} }
public async processQueue( public async processQueue(
outputStream: WritableStream<Uint8Array>, outputStream: WritableStream<Uint8Array>,
): Promise<void> { ): Promise<void> {
if (this.lock) { if (this.lock) {
return; return;
} }
this.lock = true; this.lock = true;
const writer = outputStream.getWriter(); const writer = outputStream.getWriter();
try { try {
while (this.queue.filter((p) => !p.sent).length > 0) { while (this.queue.filter((p) => !p.sent).length > 0) {
const item = this.queue.filter((p) => !p.sent)[0]; const item = this.queue.filter((p) => !p.sent)[0];
if (item) { if (item) {
await new Promise((resolve) => setTimeout(resolve, 200)); await new Promise((resolve) => setTimeout(resolve, 200));
try { try {
await writer.write(item.data); await writer.write(item.data);
item.sent = true; item.sent = true;
} catch (error) { } catch (error) {
console.error(`Error sending packet ${item.id}`, error); console.error(`Error sending packet ${item.id}`, error);
} }
} }
} }
} finally { } finally {
writer.releaseLock(); writer.releaseLock();
this.lock = false; this.lock = false;
} }
} }
} }

432
packages/core/src/utils/transform/decodePacket.ts

@ -4,219 +4,219 @@ import type { MeshDevice } from "../../../mod.ts";
import type { DeviceOutput } from "../../types.ts"; import type { DeviceOutput } from "../../types.ts";
export const decodePacket = (device: MeshDevice) => export const decodePacket = (device: MeshDevice) =>
new WritableStream<DeviceOutput>({ new WritableStream<DeviceOutput>({
write(chunk) { write(chunk) {
switch (chunk.type) { switch (chunk.type) {
case "debug": { case "debug": {
break; break;
} }
case "packet": { case "packet": {
const decodedMessage = fromBinary( const decodedMessage = fromBinary(
Protobuf.Mesh.FromRadioSchema, Protobuf.Mesh.FromRadioSchema,
chunk.data, chunk.data,
); );
device.events.onFromRadio.dispatch(decodedMessage); device.events.onFromRadio.dispatch(decodedMessage);
/** @todo Add map here when `all=true` gets fixed. */ /** @todo Add map here when `all=true` gets fixed. */
switch (decodedMessage.payloadVariant.case) { switch (decodedMessage.payloadVariant.case) {
case "packet": { case "packet": {
device.handleMeshPacket(decodedMessage.payloadVariant.value); device.handleMeshPacket(decodedMessage.payloadVariant.value);
break; break;
} }
case "myInfo": { case "myInfo": {
device.events.onMyNodeInfo.dispatch( device.events.onMyNodeInfo.dispatch(
decodedMessage.payloadVariant.value, decodedMessage.payloadVariant.value,
); );
device.log.info( device.log.info(
Types.Emitter[Types.Emitter.HandleFromRadio], Types.Emitter[Types.Emitter.HandleFromRadio],
"📱 Received Node info for this device", "📱 Received Node info for this device",
); );
break; break;
} }
case "nodeInfo": { case "nodeInfo": {
device.log.info( device.log.info(
Types.Emitter[Types.Emitter.HandleFromRadio], Types.Emitter[Types.Emitter.HandleFromRadio],
`📱 Received Node Info packet for node: ${decodedMessage.payloadVariant.value.num}`, `📱 Received Node Info packet for node: ${decodedMessage.payloadVariant.value.num}`,
); );
device.events.onNodeInfoPacket.dispatch( device.events.onNodeInfoPacket.dispatch(
decodedMessage.payloadVariant.value, decodedMessage.payloadVariant.value,
); );
//TODO: HERE //TODO: HERE
if (decodedMessage.payloadVariant.value.position) { if (decodedMessage.payloadVariant.value.position) {
device.events.onPositionPacket.dispatch({ device.events.onPositionPacket.dispatch({
id: decodedMessage.id, id: decodedMessage.id,
rxTime: new Date(), rxTime: new Date(),
from: decodedMessage.payloadVariant.value.num, from: decodedMessage.payloadVariant.value.num,
to: decodedMessage.payloadVariant.value.num, to: decodedMessage.payloadVariant.value.num,
type: "direct", type: "direct",
channel: Types.ChannelNumber.Primary, channel: Types.ChannelNumber.Primary,
data: decodedMessage.payloadVariant.value.position, data: decodedMessage.payloadVariant.value.position,
}); });
} }
//TODO: HERE //TODO: HERE
if (decodedMessage.payloadVariant.value.user) { if (decodedMessage.payloadVariant.value.user) {
device.events.onUserPacket.dispatch({ device.events.onUserPacket.dispatch({
id: decodedMessage.id, id: decodedMessage.id,
rxTime: new Date(), rxTime: new Date(),
from: decodedMessage.payloadVariant.value.num, from: decodedMessage.payloadVariant.value.num,
to: decodedMessage.payloadVariant.value.num, to: decodedMessage.payloadVariant.value.num,
type: "direct", type: "direct",
channel: Types.ChannelNumber.Primary, channel: Types.ChannelNumber.Primary,
data: decodedMessage.payloadVariant.value.user, data: decodedMessage.payloadVariant.value.user,
}); });
} }
break; break;
} }
case "config": { case "config": {
if (decodedMessage.payloadVariant.value.payloadVariant.case) { if (decodedMessage.payloadVariant.value.payloadVariant.case) {
device.log.trace( device.log.trace(
Types.Emitter[Types.Emitter.HandleFromRadio], Types.Emitter[Types.Emitter.HandleFromRadio],
`💾 Received Config packet of variant: ${decodedMessage.payloadVariant.value.payloadVariant.case}`, `💾 Received Config packet of variant: ${decodedMessage.payloadVariant.value.payloadVariant.case}`,
); );
} else { } else {
device.log.warn( device.log.warn(
Types.Emitter[Types.Emitter.HandleFromRadio], Types.Emitter[Types.Emitter.HandleFromRadio],
`⚠️ Received Config packet of variant: ${"UNK"}`, `⚠️ Received Config packet of variant: ${"UNK"}`,
); );
} }
device.events.onConfigPacket.dispatch( device.events.onConfigPacket.dispatch(
decodedMessage.payloadVariant.value, decodedMessage.payloadVariant.value,
); );
break; break;
} }
case "logRecord": { case "logRecord": {
device.log.trace( device.log.trace(
Types.Emitter[Types.Emitter.HandleFromRadio], Types.Emitter[Types.Emitter.HandleFromRadio],
"Received onLogRecord", "Received onLogRecord",
); );
device.events.onLogRecord.dispatch( device.events.onLogRecord.dispatch(
decodedMessage.payloadVariant.value, decodedMessage.payloadVariant.value,
); );
break; break;
} }
case "configCompleteId": { case "configCompleteId": {
if (decodedMessage.payloadVariant.value !== device.configId) { if (decodedMessage.payloadVariant.value !== device.configId) {
device.log.error( device.log.error(
Types.Emitter[Types.Emitter.HandleFromRadio], Types.Emitter[Types.Emitter.HandleFromRadio],
`❌ Invalid config id received from device, expected ${device.configId} but received ${decodedMessage.payloadVariant.value}`, `❌ Invalid config id received from device, expected ${device.configId} but received ${decodedMessage.payloadVariant.value}`,
); );
} }
device.log.info( device.log.info(
Types.Emitter[Types.Emitter.HandleFromRadio], Types.Emitter[Types.Emitter.HandleFromRadio],
`⚙️ Valid config id received from device: ${device.configId}`, `⚙️ Valid config id received from device: ${device.configId}`,
); );
device.updateDeviceStatus( device.updateDeviceStatus(
Types.DeviceStatusEnum.DeviceConfigured, Types.DeviceStatusEnum.DeviceConfigured,
); );
break; break;
} }
case "rebooted": { case "rebooted": {
device.configure().catch(() => { device.configure().catch(() => {
// TODO: FIX, workaround for `wantConfigId` not getting acks. // TODO: FIX, workaround for `wantConfigId` not getting acks.
}); });
break; break;
} }
case "moduleConfig": { case "moduleConfig": {
if (decodedMessage.payloadVariant.value.payloadVariant.case) { if (decodedMessage.payloadVariant.value.payloadVariant.case) {
device.log.trace( device.log.trace(
Types.Emitter[Types.Emitter.HandleFromRadio], Types.Emitter[Types.Emitter.HandleFromRadio],
`💾 Received Module Config packet of variant: ${decodedMessage.payloadVariant.value.payloadVariant.case}`, `💾 Received Module Config packet of variant: ${decodedMessage.payloadVariant.value.payloadVariant.case}`,
); );
} else { } else {
device.log.warn( device.log.warn(
Types.Emitter[Types.Emitter.HandleFromRadio], Types.Emitter[Types.Emitter.HandleFromRadio],
"⚠️ Received Module Config packet of variant: UNK", "⚠️ Received Module Config packet of variant: UNK",
); );
} }
device.events.onModuleConfigPacket.dispatch( device.events.onModuleConfigPacket.dispatch(
decodedMessage.payloadVariant.value, decodedMessage.payloadVariant.value,
); );
break; break;
} }
case "channel": { case "channel": {
device.log.trace( device.log.trace(
Types.Emitter[Types.Emitter.HandleFromRadio], Types.Emitter[Types.Emitter.HandleFromRadio],
`🔐 Received Channel: ${decodedMessage.payloadVariant.value.index}`, `🔐 Received Channel: ${decodedMessage.payloadVariant.value.index}`,
); );
device.events.onChannelPacket.dispatch( device.events.onChannelPacket.dispatch(
decodedMessage.payloadVariant.value, decodedMessage.payloadVariant.value,
); );
break; break;
} }
case "queueStatus": { case "queueStatus": {
device.log.trace( device.log.trace(
Types.Emitter[Types.Emitter.HandleFromRadio], Types.Emitter[Types.Emitter.HandleFromRadio],
`🚧 Received Queue Status: ${decodedMessage.payloadVariant.value}`, `🚧 Received Queue Status: ${decodedMessage.payloadVariant.value}`,
); );
device.events.onQueueStatus.dispatch( device.events.onQueueStatus.dispatch(
decodedMessage.payloadVariant.value, decodedMessage.payloadVariant.value,
); );
break; break;
} }
case "xmodemPacket": { case "xmodemPacket": {
device.xModem.handlePacket(decodedMessage.payloadVariant.value); device.xModem.handlePacket(decodedMessage.payloadVariant.value);
break; break;
} }
case "metadata": { case "metadata": {
if ( if (
Number.parseFloat( Number.parseFloat(
decodedMessage.payloadVariant.value.firmwareVersion, decodedMessage.payloadVariant.value.firmwareVersion,
) < Constants.minFwVer ) < Constants.minFwVer
) { ) {
device.log.fatal( device.log.fatal(
Types.Emitter[Types.Emitter.HandleFromRadio], Types.Emitter[Types.Emitter.HandleFromRadio],
`Device firmware outdated. Min supported: ${Constants.minFwVer} got : ${decodedMessage.payloadVariant.value.firmwareVersion}`, `Device firmware outdated. Min supported: ${Constants.minFwVer} got : ${decodedMessage.payloadVariant.value.firmwareVersion}`,
); );
} }
device.log.debug( device.log.debug(
Types.Emitter[Types.Emitter.GetMetadata], Types.Emitter[Types.Emitter.GetMetadata],
"🏷️ Received metadata packet", "🏷️ Received metadata packet",
); );
device.events.onDeviceMetadataPacket.dispatch({ device.events.onDeviceMetadataPacket.dispatch({
id: decodedMessage.id, id: decodedMessage.id,
rxTime: new Date(), rxTime: new Date(),
from: 0, from: 0,
to: 0, to: 0,
type: "direct", type: "direct",
channel: Types.ChannelNumber.Primary, channel: Types.ChannelNumber.Primary,
data: decodedMessage.payloadVariant.value, data: decodedMessage.payloadVariant.value,
}); });
break; break;
} }
case "mqttClientProxyMessage": { case "mqttClientProxyMessage": {
break; break;
} }
default: { default: {
device.log.warn( device.log.warn(
Types.Emitter[Types.Emitter.HandleFromRadio], Types.Emitter[Types.Emitter.HandleFromRadio],
`⚠️ Unhandled payload variant: ${decodedMessage.payloadVariant.case}`, `⚠️ Unhandled payload variant: ${decodedMessage.payloadVariant.case}`,
); );
} }
} }
} }
} }
}, },
}); });

126
packages/core/src/utils/transform/fromDevice.ts

@ -1,71 +1,71 @@
import type { DeviceOutput } from "../../types.ts"; import type { DeviceOutput } from "../../types.ts";
export const fromDeviceStream: () => TransformStream<Uint8Array, DeviceOutput> = export const fromDeviceStream: () => TransformStream<Uint8Array, DeviceOutput> =
( (
// onReleaseEvent: SimpleEventDispatcher<boolean>, // onReleaseEvent: SimpleEventDispatcher<boolean>,
) => { ) => {
let byteBuffer = new Uint8Array([]); let byteBuffer = new Uint8Array([]);
const textDecoder = new TextDecoder(); const textDecoder = new TextDecoder();
return new TransformStream<Uint8Array, DeviceOutput>({ return new TransformStream<Uint8Array, DeviceOutput>({
transform(chunk: Uint8Array, controller): void { transform(chunk: Uint8Array, controller): void {
// onReleaseEvent.subscribe(() => { // onReleaseEvent.subscribe(() => {
// controller.terminate(); // controller.terminate();
// }); // });
byteBuffer = new Uint8Array([...byteBuffer, ...chunk]); byteBuffer = new Uint8Array([...byteBuffer, ...chunk]);
let processingExhausted = false; let processingExhausted = false;
while (byteBuffer.length !== 0 && !processingExhausted) { while (byteBuffer.length !== 0 && !processingExhausted) {
const framingIndex = byteBuffer.findIndex((byte) => byte === 0x94); const framingIndex = byteBuffer.findIndex((byte) => byte === 0x94);
const framingByte2 = byteBuffer[framingIndex + 1]; const framingByte2 = byteBuffer[framingIndex + 1];
if (framingByte2 === 0xc3) { if (framingByte2 === 0xc3) {
if (byteBuffer.subarray(0, framingIndex).length) { if (byteBuffer.subarray(0, framingIndex).length) {
controller.enqueue({ controller.enqueue({
type: "debug", type: "debug",
data: textDecoder.decode(byteBuffer.subarray(0, framingIndex)), data: textDecoder.decode(byteBuffer.subarray(0, framingIndex)),
}); });
byteBuffer = byteBuffer.subarray(framingIndex); byteBuffer = byteBuffer.subarray(framingIndex);
} }
const msb = byteBuffer[2]; const msb = byteBuffer[2];
const lsb = byteBuffer[3]; const lsb = byteBuffer[3];
if ( if (
msb !== undefined && msb !== undefined &&
lsb !== undefined && lsb !== undefined &&
byteBuffer.length >= 4 + (msb << 8) + lsb byteBuffer.length >= 4 + (msb << 8) + lsb
) { ) {
const packet = byteBuffer.subarray(4, 4 + (msb << 8) + lsb); const packet = byteBuffer.subarray(4, 4 + (msb << 8) + lsb);
const malformedDetectorIndex = packet.findIndex( const malformedDetectorIndex = packet.findIndex(
(byte) => byte === 0x94, (byte) => byte === 0x94,
); );
if ( if (
malformedDetectorIndex !== -1 && malformedDetectorIndex !== -1 &&
packet[malformedDetectorIndex + 1] === 0xc3 packet[malformedDetectorIndex + 1] === 0xc3
) { ) {
console.warn( console.warn(
`⚠️ Malformed packet found, discarding: ${byteBuffer `⚠️ Malformed packet found, discarding: ${byteBuffer
.subarray(0, malformedDetectorIndex - 1) .subarray(0, malformedDetectorIndex - 1)
.toString()}`, .toString()}`,
); );
byteBuffer = byteBuffer.subarray(malformedDetectorIndex); byteBuffer = byteBuffer.subarray(malformedDetectorIndex);
} else { } else {
byteBuffer = byteBuffer.subarray(3 + (msb << 8) + lsb + 1); byteBuffer = byteBuffer.subarray(3 + (msb << 8) + lsb + 1);
controller.enqueue({ controller.enqueue({
type: "packet", type: "packet",
data: packet, data: packet,
}); });
} }
} else { } else {
/** Only partioal message in buffer, wait for the rest */ /** Only partioal message in buffer, wait for the rest */
processingExhausted = true; processingExhausted = true;
} }
} else { } else {
/** Message not complete, only 1 byte in buffer */ /** Message not complete, only 1 byte in buffer */
processingExhausted = true; processingExhausted = true;
} }
} }
}, },
}); });
}; };

24
packages/core/src/utils/transform/toDevice.ts

@ -2,15 +2,15 @@
* Pads packets with appropriate framing information before writing to the output stream. * Pads packets with appropriate framing information before writing to the output stream.
*/ */
export const toDeviceStream: TransformStream<Uint8Array, Uint8Array> = export const toDeviceStream: TransformStream<Uint8Array, Uint8Array> =
new TransformStream<Uint8Array, Uint8Array>({ new TransformStream<Uint8Array, Uint8Array>({
transform(chunk: Uint8Array, controller): void { transform(chunk: Uint8Array, controller): void {
const bufLen = chunk.length; const bufLen = chunk.length;
const header = new Uint8Array([ const header = new Uint8Array([
0x94, 0x94,
0xc3, 0xc3,
(bufLen >> 8) & 0xff, (bufLen >> 8) & 0xff,
bufLen & 0xff, bufLen & 0xff,
]); ]);
controller.enqueue(new Uint8Array([...header, ...chunk])); controller.enqueue(new Uint8Array([...header, ...chunk]));
}, },
}); });

232
packages/core/src/utils/xmodem.ts

@ -6,130 +6,130 @@ import crc16ccitt from "crc/calculators/crc16ccitt";
type XmodemProps = (toRadio: Uint8Array, id?: number) => Promise<number>; type XmodemProps = (toRadio: Uint8Array, id?: number) => Promise<number>;
export class Xmodem { export class Xmodem {
private sendRaw: XmodemProps; private sendRaw: XmodemProps;
private rxBuffer: Uint8Array[]; private rxBuffer: Uint8Array[];
private txBuffer: Uint8Array[]; private txBuffer: Uint8Array[];
private textEncoder: TextEncoder; private textEncoder: TextEncoder;
private counter: number; private counter: number;
constructor(sendRaw: XmodemProps) { constructor(sendRaw: XmodemProps) {
this.sendRaw = sendRaw; this.sendRaw = sendRaw;
this.rxBuffer = []; this.rxBuffer = [];
this.txBuffer = []; this.txBuffer = [];
this.textEncoder = new TextEncoder(); this.textEncoder = new TextEncoder();
this.counter = 0; this.counter = 0;
} }
async downloadFile(filename: string): Promise<number> { async downloadFile(filename: string): Promise<number> {
return await this.sendCommand( return await this.sendCommand(
Protobuf.Xmodem.XModem_Control.STX, Protobuf.Xmodem.XModem_Control.STX,
this.textEncoder.encode(filename), this.textEncoder.encode(filename),
0, 0,
); );
} }
async uploadFile(filename: string, data: Uint8Array): Promise<number> { async uploadFile(filename: string, data: Uint8Array): Promise<number> {
for (let i = 0; i < data.length; i += 128) { for (let i = 0; i < data.length; i += 128) {
this.txBuffer.push(data.slice(i, i + 128)); this.txBuffer.push(data.slice(i, i + 128));
} }
return await this.sendCommand( return await this.sendCommand(
Protobuf.Xmodem.XModem_Control.SOH, Protobuf.Xmodem.XModem_Control.SOH,
this.textEncoder.encode(filename), this.textEncoder.encode(filename),
0, 0,
); );
} }
async sendCommand( async sendCommand(
command: Protobuf.Xmodem.XModem_Control, command: Protobuf.Xmodem.XModem_Control,
buffer?: Uint8Array, buffer?: Uint8Array,
sequence?: number, sequence?: number,
crc16?: number, crc16?: number,
): Promise<number> { ): Promise<number> {
const toRadio = create(Protobuf.Mesh.ToRadioSchema, { const toRadio = create(Protobuf.Mesh.ToRadioSchema, {
payloadVariant: { payloadVariant: {
case: "xmodemPacket", case: "xmodemPacket",
value: { value: {
buffer, buffer,
control: command, control: command,
seq: sequence, seq: sequence,
crc16: crc16, crc16: crc16,
}, },
}, },
}); });
return await this.sendRaw(toBinary(Protobuf.Mesh.ToRadioSchema, toRadio)); return await this.sendRaw(toBinary(Protobuf.Mesh.ToRadioSchema, toRadio));
} }
async handlePacket(packet: Protobuf.Xmodem.XModem): Promise<number> { async handlePacket(packet: Protobuf.Xmodem.XModem): Promise<number> {
await new Promise((resolve) => setTimeout(resolve, 100)); await new Promise((resolve) => setTimeout(resolve, 100));
switch (packet.control) { switch (packet.control) {
case Protobuf.Xmodem.XModem_Control.NUL: { case Protobuf.Xmodem.XModem_Control.NUL: {
// nothing // nothing
break; break;
} }
case Protobuf.Xmodem.XModem_Control.SOH: { case Protobuf.Xmodem.XModem_Control.SOH: {
this.counter = packet.seq; this.counter = packet.seq;
if (this.validateCrc16(packet)) { if (this.validateCrc16(packet)) {
this.rxBuffer[this.counter] = packet.buffer; this.rxBuffer[this.counter] = packet.buffer;
return this.sendCommand(Protobuf.Xmodem.XModem_Control.ACK); return this.sendCommand(Protobuf.Xmodem.XModem_Control.ACK);
} }
return await this.sendCommand( return await this.sendCommand(
Protobuf.Xmodem.XModem_Control.NAK, Protobuf.Xmodem.XModem_Control.NAK,
undefined, undefined,
packet.seq, packet.seq,
); );
} }
case Protobuf.Xmodem.XModem_Control.STX: { case Protobuf.Xmodem.XModem_Control.STX: {
break; break;
} }
case Protobuf.Xmodem.XModem_Control.EOT: { case Protobuf.Xmodem.XModem_Control.EOT: {
// end of transmission // end of transmission
break; break;
} }
case Protobuf.Xmodem.XModem_Control.ACK: { case Protobuf.Xmodem.XModem_Control.ACK: {
this.counter++; this.counter++;
if (this.txBuffer[this.counter - 1]) { if (this.txBuffer[this.counter - 1]) {
return this.sendCommand( return this.sendCommand(
Protobuf.Xmodem.XModem_Control.SOH, Protobuf.Xmodem.XModem_Control.SOH,
this.txBuffer[this.counter - 1], this.txBuffer[this.counter - 1],
this.counter, this.counter,
crc16ccitt(this.txBuffer[this.counter - 1] ?? new Uint8Array()), crc16ccitt(this.txBuffer[this.counter - 1] ?? new Uint8Array()),
); );
} }
if (this.counter === this.txBuffer.length + 1) { if (this.counter === this.txBuffer.length + 1) {
return this.sendCommand(Protobuf.Xmodem.XModem_Control.EOT); return this.sendCommand(Protobuf.Xmodem.XModem_Control.EOT);
} }
this.clear(); this.clear();
break; break;
} }
case Protobuf.Xmodem.XModem_Control.NAK: { case Protobuf.Xmodem.XModem_Control.NAK: {
return this.sendCommand( return this.sendCommand(
Protobuf.Xmodem.XModem_Control.SOH, Protobuf.Xmodem.XModem_Control.SOH,
this.txBuffer[this.counter], this.txBuffer[this.counter],
this.counter, this.counter,
crc16ccitt(this.txBuffer[this.counter - 1] ?? new Uint8Array()), crc16ccitt(this.txBuffer[this.counter - 1] ?? new Uint8Array()),
); );
} }
case Protobuf.Xmodem.XModem_Control.CAN: { case Protobuf.Xmodem.XModem_Control.CAN: {
this.clear(); this.clear();
break; break;
} }
case Protobuf.Xmodem.XModem_Control.CTRLZ: { case Protobuf.Xmodem.XModem_Control.CTRLZ: {
break; break;
} }
} }
return Promise.resolve(0); return Promise.resolve(0);
} }
validateCrc16(packet: Protobuf.Xmodem.XModem): boolean { validateCrc16(packet: Protobuf.Xmodem.XModem): boolean {
return crc16ccitt(packet.buffer) === packet.crc16; return crc16ccitt(packet.buffer) === packet.crc16;
} }
clear() { clear() {
this.counter = 0; this.counter = 0;
this.rxBuffer = []; this.rxBuffer = [];
this.txBuffer = []; this.txBuffer = [];
} }
} }

0
packages/transport-deno/deno.json → packages/transport-deno/package.json

44
packages/transport-deno/src/transport.ts

@ -2,31 +2,31 @@ import { Utils } from "@meshtastic/core";
import type { Types } from "@meshtastic/core"; import type { Types } from "@meshtastic/core";
export class TransportDeno implements Types.Transport { export class TransportDeno implements Types.Transport {
private _toDevice: WritableStream<Uint8Array>; private _toDevice: WritableStream<Uint8Array>;
private _fromDevice: ReadableStream<Types.DeviceOutput>; private _fromDevice: ReadableStream<Types.DeviceOutput>;
public static async create(hostname: string): Promise<TransportDeno> { public static async create(hostname: string): Promise<TransportDeno> {
const connection = await Deno.connect({ const connection = await Deno.connect({
hostname, hostname,
port: 4403, port: 4403,
}); });
return new TransportDeno(connection); return new TransportDeno(connection);
} }
constructor(connection: Deno.Conn) { constructor(connection: Deno.Conn) {
Utils.toDeviceStream.readable.pipeTo(connection.writable); Utils.toDeviceStream.readable.pipeTo(connection.writable);
this._toDevice = Utils.toDeviceStream.writable; this._toDevice = Utils.toDeviceStream.writable;
this._fromDevice = connection.readable.pipeThrough( this._fromDevice = connection.readable.pipeThrough(
Utils.fromDeviceStream(), Utils.fromDeviceStream(),
); );
} }
get toDevice(): WritableStream<Uint8Array> { get toDevice(): WritableStream<Uint8Array> {
return this._toDevice; return this._toDevice;
} }
get fromDevice(): ReadableStream<Types.DeviceOutput> { get fromDevice(): ReadableStream<Types.DeviceOutput> {
return this._fromDevice; return this._fromDevice;
} }
} }

5
packages/transport-http/jsr.json

@ -0,0 +1,5 @@
{
"name": "@meshtastic/transport-http",
"version": "0.2.2",
"exports": "./mod.ts"
}

9
packages/transport-http/deno.json → packages/transport-http/package.json

@ -1,8 +1,9 @@
{ {
"name": "@meshtastic/transport-http", "name": "@meshtastic/transport-http",
"version": "0.2.1", "version": "0.2.2",
"description": "A transport layer for Meshtastic applications using HTTP.", "description": "A transport layer for Meshtastic applications using HTTP.",
"exports": { "exports": {".": "./mod.ts"},
".": "./mod.ts" "tasks": {
"build": "deno build"
} }
} }

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

@ -1,89 +1,89 @@
import type { Types } from "@meshtastic/core"; import type { Types } from "@meshtastic/core";
export class TransportHTTP implements Types.Transport { export class TransportHTTP implements Types.Transport {
private _toDevice: WritableStream<Uint8Array>; private _toDevice: WritableStream<Uint8Array>;
private _fromDevice: ReadableStream<Types.DeviceOutput>; private _fromDevice: ReadableStream<Types.DeviceOutput>;
private url: string; private url: string;
private receiveBatchRequests: boolean; private receiveBatchRequests: boolean;
private fetchInterval: number; private fetchInterval: number;
public static async create( public static async create(
address: string, address: string,
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}/json/report`);
await Promise.resolve(); await Promise.resolve();
return new TransportHTTP(connectionUrl); return new TransportHTTP(connectionUrl);
} }
constructor(url: string) { constructor(url: string) {
this.url = url; this.url = url;
this.receiveBatchRequests = false; this.receiveBatchRequests = false;
this.fetchInterval = 3000; this.fetchInterval = 3000;
this._toDevice = new WritableStream<Uint8Array>({ this._toDevice = new WritableStream<Uint8Array>({
write: async (chunk) => { write: async (chunk) => {
await this.writeToRadio(chunk); await this.writeToRadio(chunk);
}, },
}); });
let controller: ReadableStreamDefaultController<Types.DeviceOutput>; let controller: ReadableStreamDefaultController<Types.DeviceOutput>;
this._fromDevice = new ReadableStream<Types.DeviceOutput>({ this._fromDevice = new ReadableStream<Types.DeviceOutput>({
start: (ctrl) => { start: (ctrl) => {
controller = ctrl; controller = ctrl;
}, },
}); });
setInterval(async () => { setInterval(async () => {
await this.readFromRadio(controller); await this.readFromRadio(controller);
}, this.fetchInterval); }, this.fetchInterval);
} }
private async readFromRadio( private async readFromRadio(
controller: ReadableStreamDefaultController<Types.DeviceOutput>, controller: ReadableStreamDefaultController<Types.DeviceOutput>,
): Promise<void> { ): Promise<void> {
let readBuffer = new ArrayBuffer(1); let readBuffer = new ArrayBuffer(1);
while (readBuffer.byteLength > 0) { while (readBuffer.byteLength > 0) {
const response = await fetch( const response = await fetch(
`${this.url}/api/v1/fromradio?all=${ `${this.url}/api/v1/fromradio?all=${
this.receiveBatchRequests ? "true" : "false" this.receiveBatchRequests ? "true" : "false"
}`, }`,
{ {
method: "GET", method: "GET",
headers: { headers: {
Accept: "application/x-protobuf", Accept: "application/x-protobuf",
}, },
}, },
); );
readBuffer = await response.arrayBuffer(); readBuffer = await response.arrayBuffer();
if (readBuffer.byteLength > 0) { if (readBuffer.byteLength > 0) {
controller.enqueue({ controller.enqueue({
type: "packet", type: "packet",
data: new Uint8Array(readBuffer), data: new Uint8Array(readBuffer),
}); });
} }
} }
} }
private async writeToRadio(data: Uint8Array): Promise<void> { private async writeToRadio(data: Uint8Array): Promise<void> {
await fetch(`${this.url}/api/v1/toradio`, { await fetch(`${this.url}/api/v1/toradio`, {
method: "PUT", method: "PUT",
headers: { headers: {
"Content-Type": "application/x-protobuf", "Content-Type": "application/x-protobuf",
}, },
body: data, body: data,
}); });
} }
get toDevice(): WritableStream<Uint8Array> { get toDevice(): WritableStream<Uint8Array> {
return this._toDevice; return this._toDevice;
} }
get fromDevice(): ReadableStream<Types.DeviceOutput> { get fromDevice(): ReadableStream<Types.DeviceOutput> {
return this._fromDevice; return this._fromDevice;
} }
} }

0
packages/transport-node/deno.json → packages/transport-node/package.json

112
packages/transport-node/src/transport.ts

@ -4,73 +4,73 @@ import { Utils } from "@meshtastic/core";
import type { Types } from "@meshtastic/core"; import type { Types } from "@meshtastic/core";
export class TransportNode implements Types.Transport { export class TransportNode implements Types.Transport {
private readonly _toDevice: WritableStream<Uint8Array>; private readonly _toDevice: WritableStream<Uint8Array>;
private readonly _fromDevice: ReadableStream<Types.DeviceOutput>; private readonly _fromDevice: ReadableStream<Types.DeviceOutput>;
/** /**
* Creates and connects a new TransportNode instance. * Creates and connects a new TransportNode instance.
* @param hostname - The IP address or hostname of the Meshtastic device. * @param hostname - The IP address or hostname of the Meshtastic device.
* @param port - The port number for the TCP connection (defaults to 4403). * @param port - The port number for the TCP connection (defaults to 4403).
* @returns A promise that resolves with a connected TransportNode instance. * @returns A promise that resolves with a connected TransportNode instance.
*/ */
public static create(hostname: string, port = 4403): Promise<TransportNode> { public static create(hostname: string, port = 4403): Promise<TransportNode> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const socket = new Socket(); const socket = new Socket();
const onError = (err: Error) => { const onError = (err: Error) => {
socket.destroy(); socket.destroy();
reject(err); reject(err);
}; };
socket.once("error", onError); socket.once("error", onError);
socket.connect(port, hostname, () => { socket.connect(port, hostname, () => {
socket.removeListener("error", onError); socket.removeListener("error", onError);
resolve(new TransportNode(socket)); resolve(new TransportNode(socket));
}); });
}); });
} }
/** /**
* Constructs a new TransportNode. * Constructs a new TransportNode.
* @param connection - An active Node.js net.Socket connection. * @param connection - An active Node.js net.Socket connection.
*/ */
constructor(connection: Socket) { constructor(connection: Socket) {
connection.on("error", (err) => { connection.on("error", (err) => {
console.error("Socket connection error:", err); console.error("Socket connection error:", err);
}); });
const fromDeviceSource = Readable.toWeb( const fromDeviceSource = Readable.toWeb(
connection, connection,
) as ReadableStream<Uint8Array>; ) as ReadableStream<Uint8Array>;
this._fromDevice = fromDeviceSource.pipeThrough(Utils.fromDeviceStream()); this._fromDevice = fromDeviceSource.pipeThrough(Utils.fromDeviceStream());
// Stream for data going FROM the application TO the Meshtastic device. // Stream for data going FROM the application TO the Meshtastic device.
const toDeviceTransform = Utils.toDeviceStream; const toDeviceTransform = Utils.toDeviceStream;
this._toDevice = toDeviceTransform.writable; this._toDevice = toDeviceTransform.writable;
// The readable end of the transform is then piped to the Node.js socket. // The readable end of the transform is then piped to the Node.js socket.
// A similar assertion is needed here because `Writable.toWeb` also returns // A similar assertion is needed here because `Writable.toWeb` also returns
// a generically typed stream (`WritableStream<any>`). // a generically typed stream (`WritableStream<any>`).
toDeviceTransform.readable toDeviceTransform.readable
.pipeTo(Writable.toWeb(connection) as WritableStream<Uint8Array>) .pipeTo(Writable.toWeb(connection) as WritableStream<Uint8Array>)
.catch((err) => { .catch((err) => {
console.error("Error piping data to socket:", err); console.error("Error piping data to socket:", err);
connection.destroy(err as Error); connection.destroy(err as Error);
}); });
} }
/** /**
* The WritableStream to send data to the Meshtastic device. * The WritableStream to send data to the Meshtastic device.
*/ */
public get toDevice(): WritableStream<Uint8Array> { public get toDevice(): WritableStream<Uint8Array> {
return this._toDevice; return this._toDevice;
} }
/** /**
* The ReadableStream to receive data from the Meshtastic device. * The ReadableStream to receive data from the Meshtastic device.
*/ */
public get fromDevice(): ReadableStream<Types.DeviceOutput> { public get fromDevice(): ReadableStream<Types.DeviceOutput> {
return this._fromDevice; return this._fromDevice;
} }
} }

5
packages/transport-web-bluetooth/jsr.json

@ -0,0 +1,5 @@
{
"name": "@meshtastic/transport-web-bluetooth",
"version": "0.1.3",
"exports": "./mod.ts"
}

10
packages/transport-web-bluetooth/deno.json → packages/transport-web-bluetooth/package.json

@ -1,14 +1,14 @@
{ {
"name": "@meshtastic/transport-web-bluetooth", "name": "@meshtastic/transport-web-bluetooth",
"version": "0.1.2", "version": "0.1.3",
"description": "A transport layer for Meshtastic applications using Web Bluetooth.", "description": "A transport layer for Meshtastic applications using Web Bluetooth.",
"exports": { "exports": {
".": "./mod.ts" ".": "./mod.ts"
}, },
"imports": { "devDependencies": {
"@types/web-bluetooth": "npm:@types/web-bluetooth@^0.0.20" "@types/web-bluetooth": "npm:@types/web-bluetooth@^0.0.20"
}, },
"compilerOptions": { "compilerOptions": {
"types": ["@types/web-bluetooth"] "types": ["@types/web-bluetooth"]
} }
} }

260
packages/transport-web-bluetooth/src/transport.ts

@ -1,133 +1,135 @@
import type { Types } from "@meshtastic/core"; import type { Types } from "@meshtastic/core";
export class TransportWebBluetooth implements Types.Transport { export class TransportWebBluetooth implements Types.Transport {
private _toDevice: WritableStream<Uint8Array>; private _toDevice: WritableStream<Uint8Array>;
private _fromDevice: ReadableStream<Types.DeviceOutput>; private _fromDevice: ReadableStream<Types.DeviceOutput>;
private _fromDeviceController?: ReadableStreamDefaultController<Types.DeviceOutput>; private _fromDeviceController?: ReadableStreamDefaultController<Types.DeviceOutput>;
private _isFirstWrite = true; private _isFirstWrite = true;
private toRadioCharacteristic: BluetoothRemoteGATTCharacteristic; private toRadioCharacteristic: BluetoothRemoteGATTCharacteristic;
private fromRadioCharacteristic: BluetoothRemoteGATTCharacteristic; private fromRadioCharacteristic: BluetoothRemoteGATTCharacteristic;
private fromNumCharacteristic: BluetoothRemoteGATTCharacteristic; private fromNumCharacteristic: BluetoothRemoteGATTCharacteristic;
static ToRadioUuid = "f75c76d2-129e-4dad-a1dd-7866124401e7"; static ToRadioUuid = "f75c76d2-129e-4dad-a1dd-7866124401e7";
static FromRadioUuid = "2c55e69e-4993-11ed-b878-0242ac120002"; static FromRadioUuid = "2c55e69e-4993-11ed-b878-0242ac120002";
static FromNumUuid = "ed9da18c-a800-4f66-a670-aa7547e34453"; static FromNumUuid = "ed9da18c-a800-4f66-a670-aa7547e34453";
static ServiceUuid = "6ba1b218-15a8-461f-9fa8-5dcae273eafd"; static ServiceUuid = "6ba1b218-15a8-461f-9fa8-5dcae273eafd";
public static async create(): Promise<TransportWebBluetooth> { public static async create(): Promise<TransportWebBluetooth> {
const device = await navigator.bluetooth.requestDevice({ const device = await navigator.bluetooth.requestDevice({
filters: [{ services: [this.ServiceUuid] }], filters: [{ services: [TransportWebBluetooth.ServiceUuid] }],
}); });
return await this.prepareConnection(device); return await TransportWebBluetooth.prepareConnection(device);
} }
public static async createFromDevice( public static async createFromDevice(
device: BluetoothDevice, device: BluetoothDevice,
): Promise<TransportWebBluetooth> { ): Promise<TransportWebBluetooth> {
return await this.prepareConnection(device); return await TransportWebBluetooth.prepareConnection(device);
} }
public static async prepareConnection( public static async prepareConnection(
device: BluetoothDevice, device: BluetoothDevice,
): Promise<TransportWebBluetooth> { ): Promise<TransportWebBluetooth> {
const gattServer = await device.gatt?.connect(); const gattServer = await device.gatt?.connect();
if (!gattServer) { if (!gattServer) {
throw new Error("Failed to connect to GATT server"); throw new Error("Failed to connect to GATT server");
} }
const service = await gattServer.getPrimaryService(this.ServiceUuid); const service = await gattServer.getPrimaryService(
TransportWebBluetooth.ServiceUuid,
const toRadioCharacteristic = await service.getCharacteristic( );
this.ToRadioUuid,
); const toRadioCharacteristic = await service.getCharacteristic(
const fromRadioCharacteristic = await service.getCharacteristic( TransportWebBluetooth.ToRadioUuid,
this.FromRadioUuid, );
); const fromRadioCharacteristic = await service.getCharacteristic(
const fromNumCharacteristic = await service.getCharacteristic( TransportWebBluetooth.FromRadioUuid,
this.FromNumUuid, );
); const fromNumCharacteristic = await service.getCharacteristic(
TransportWebBluetooth.FromNumUuid,
if ( );
!toRadioCharacteristic ||
!fromRadioCharacteristic || if (
!fromNumCharacteristic !toRadioCharacteristic ||
) { !fromRadioCharacteristic ||
throw new Error("Failed to find required characteristics"); !fromNumCharacteristic
} ) {
throw new Error("Failed to find required characteristics");
console.log("Connected to device", device.name); }
return new TransportWebBluetooth( console.log("Connected to device", device.name);
toRadioCharacteristic,
fromRadioCharacteristic, return new TransportWebBluetooth(
fromNumCharacteristic, toRadioCharacteristic,
); fromRadioCharacteristic,
} fromNumCharacteristic,
);
constructor( }
toRadioCharacteristic: BluetoothRemoteGATTCharacteristic,
fromRadioCharacteristic: BluetoothRemoteGATTCharacteristic, constructor(
fromNumCharacteristic: BluetoothRemoteGATTCharacteristic, toRadioCharacteristic: BluetoothRemoteGATTCharacteristic,
) { fromRadioCharacteristic: BluetoothRemoteGATTCharacteristic,
this.toRadioCharacteristic = toRadioCharacteristic; fromNumCharacteristic: BluetoothRemoteGATTCharacteristic,
this.fromRadioCharacteristic = fromRadioCharacteristic; ) {
this.fromNumCharacteristic = fromNumCharacteristic; this.toRadioCharacteristic = toRadioCharacteristic;
this.fromRadioCharacteristic = fromRadioCharacteristic;
this._fromDevice = new ReadableStream({ this.fromNumCharacteristic = fromNumCharacteristic;
start: (ctrl) => {
this._fromDeviceController = ctrl; this._fromDevice = new ReadableStream({
}, start: (ctrl) => {
}); this._fromDeviceController = ctrl;
},
this._toDevice = new WritableStream({ });
write: async (chunk) => {
await this.toRadioCharacteristic.writeValue(chunk); this._toDevice = new WritableStream({
write: async (chunk) => {
if (this._isFirstWrite && this._fromDeviceController) { await this.toRadioCharacteristic.writeValue(chunk);
this._isFirstWrite = false;
setTimeout(() => { if (this._isFirstWrite && this._fromDeviceController) {
this.readFromRadio(this._fromDeviceController!); this._isFirstWrite = false;
}, 50); setTimeout(() => {
} this.readFromRadio(this._fromDeviceController!);
}, }, 50);
}); }
},
this.fromNumCharacteristic.addEventListener( });
"characteristicvaluechanged",
() => { this.fromNumCharacteristic.addEventListener(
if (this._fromDeviceController) { "characteristicvaluechanged",
this.readFromRadio(this._fromDeviceController); () => {
} if (this._fromDeviceController) {
}, this.readFromRadio(this._fromDeviceController);
); }
},
this.fromNumCharacteristic.startNotifications(); );
}
this.fromNumCharacteristic.startNotifications();
get toDevice(): WritableStream<Uint8Array> { }
return this._toDevice;
} get toDevice(): WritableStream<Uint8Array> {
return this._toDevice;
get fromDevice(): ReadableStream<Types.DeviceOutput> { }
return this._fromDevice;
} get fromDevice(): ReadableStream<Types.DeviceOutput> {
return this._fromDevice;
protected async readFromRadio( }
controller: ReadableStreamDefaultController<Types.DeviceOutput>,
): Promise<void> { protected async readFromRadio(
let hasMoreData = true; controller: ReadableStreamDefaultController<Types.DeviceOutput>,
while (hasMoreData && this.fromRadioCharacteristic) { ): Promise<void> {
const value = await this.fromRadioCharacteristic.readValue(); let hasMoreData = true;
if (value.byteLength === 0) { while (hasMoreData && this.fromRadioCharacteristic) {
hasMoreData = false; const value = await this.fromRadioCharacteristic.readValue();
continue; if (value.byteLength === 0) {
} hasMoreData = false;
controller.enqueue({ continue;
type: "packet", }
data: new Uint8Array(value.buffer), controller.enqueue({
}); type: "packet",
} data: new Uint8Array(value.buffer),
} });
}
}
} }

14
packages/transport-web-serial/deno.json

@ -1,14 +0,0 @@
{
"name": "@meshtastic/transport-web-serial",
"version": "0.2.1",
"description": "A transport layer for Meshtastic applications using Web Serial API.",
"exports": {
".": "./mod.ts"
},
"imports": {
"@types/w3c-web-serial": "npm:@types/w3c-web-serial@^1.0.7"
},
"compilerOptions": {
"types": ["@types/w3c-web-serial"]
}
}

5
packages/transport-web-serial/jsr.json

@ -0,0 +1,5 @@
{
"name": "@meshtastic/transport-web-serial",
"version": "0.2.2",
"exports": "./mod.ts"
}

14
packages/transport-web-serial/package.json

@ -0,0 +1,14 @@
{
"name": "@meshtastic/transport-web-serial",
"version": "0.2.2",
"description": "A transport layer for Meshtastic applications using Web Serial API.",
"exports": {
".": "./mod.ts"
},
"dependencies": {
"@types/w3c-web-serial": "npm:@types/w3c-web-serial@^1.0.7"
},
"compilerOptions": {
"types": ["@types/w3c-web-serial"]
}
}

74
packages/transport-web-serial/src/transport.ts

@ -2,41 +2,41 @@ import { Utils } from "@meshtastic/core";
import type { Types } from "@meshtastic/core"; import type { Types } from "@meshtastic/core";
export class TransportWebSerial implements Types.Transport { export class TransportWebSerial implements Types.Transport {
private _toDevice: WritableStream<Uint8Array>; private _toDevice: WritableStream<Uint8Array>;
private _fromDevice: ReadableStream<Types.DeviceOutput>; private _fromDevice: ReadableStream<Types.DeviceOutput>;
public static async create(baudRate?: number): Promise<TransportWebSerial> { public static async create(baudRate?: number): Promise<TransportWebSerial> {
const port = await navigator.serial.requestPort(); const port = await navigator.serial.requestPort();
await port.open({ baudRate: baudRate || 115200 }); await port.open({ baudRate: baudRate || 115200 });
return new TransportWebSerial(port); return new TransportWebSerial(port);
} }
public static async createFromPort( public static async createFromPort(
port: SerialPort, port: SerialPort,
baudRate?: number, baudRate?: number,
): Promise<TransportWebSerial> { ): Promise<TransportWebSerial> {
await port.open({ baudRate: baudRate || 115200 }); await port.open({ baudRate: baudRate || 115200 });
return new TransportWebSerial(port); return new TransportWebSerial(port);
} }
constructor(connection: SerialPort) { constructor(connection: SerialPort) {
if (!connection.readable || !connection.writable) { if (!connection.readable || !connection.writable) {
throw new Error("Stream not accessible"); throw new Error("Stream not accessible");
} }
Utils.toDeviceStream.readable.pipeTo(connection.writable); Utils.toDeviceStream.readable.pipeTo(connection.writable);
this._toDevice = Utils.toDeviceStream.writable; this._toDevice = Utils.toDeviceStream.writable;
this._fromDevice = connection.readable.pipeThrough( this._fromDevice = connection.readable.pipeThrough(
Utils.fromDeviceStream(), Utils.fromDeviceStream(),
); );
} }
get toDevice(): WritableStream<Uint8Array> { get toDevice(): WritableStream<Uint8Array> {
return this._toDevice; return this._toDevice;
} }
get fromDevice(): ReadableStream<Types.DeviceOutput> { get fromDevice(): ReadableStream<Types.DeviceOutput> {
return this._fromDevice; return this._fromDevice;
} }
} }

22
packages/web/README.md

@ -18,23 +18,6 @@ or served from a node
![Alt](https://repobeats.axiom.co/api/embed/e5b062db986cb005d83e81724c00cb2b9cce8e4c.svg "Repobeats analytics image") ![Alt](https://repobeats.axiom.co/api/embed/e5b062db986cb005d83e81724c00cb2b9cce8e4c.svg "Repobeats analytics image")
## Progress Web App Support (PWA)
Meshtastic Web Client now includes Progressive Web App (PWA) functionality,
allowing users to:
- Install the app on desktop and mobile devices
- Access the interface offline
- Receive updates automatically
- Experience faster load times with caching
To install as a PWA:
- On desktop: Look for the install icon in your browser's address bar
- On mobile: Use "Add to Home Screen" option in your browser menu
PWA functionality works with both the hosted version and self-hosted instances.
## Self-host ## Self-host
The client can be self hosted using the precompiled container images with an OCI The client can be self hosted using the precompiled container images with an OCI
@ -56,7 +39,7 @@ Our release process follows these guidelines:
- **Versioning:** We use Semantic Versioning (`Major.Minor.Patch`). - **Versioning:** We use Semantic Versioning (`Major.Minor.Patch`).
- **Stable Releases:** Published around the beginning of each month (e.g., - **Stable Releases:** Published around the beginning of each month (e.g.,
`v2.3.4`). `v2.6.1`).
- **Pre-releases:** A pre-release is typically issued mid-month for testing and - **Pre-releases:** A pre-release is typically issued mid-month for testing and
early adoption. early adoption.
- **Nightly Builds:** An experimental Docker image containing the latest - **Nightly Builds:** An experimental Docker image containing the latest
@ -106,6 +89,7 @@ instructions listed on the home page.
Install the dependencies. Install the dependencies.
```bash ```bash
cd packages/web &&
bun install bun install
``` ```
@ -141,8 +125,6 @@ reasons:
configuration, enhancing code quality and developer experience. configuration, enhancing code quality and developer experience.
- **Modern JavaScript**: First-class support for ESM imports, top-level await, - **Modern JavaScript**: First-class support for ESM imports, top-level await,
and other modern JavaScript features. and other modern JavaScript features.
- **All-in-One Tooling**: Built-in package manager, bundler, test runner, and
transpiler eliminate the need for multiple third-party tools.
- **Node.js Compatibility**: Drop-in replacement for Node.js with better - **Node.js Compatibility**: Drop-in replacement for Node.js with better
performance and built-in tooling. performance and built-in tooling.
- **Reproducible Builds**: Lockfile ensures consistent builds across all - **Reproducible Builds**: Lockfile ensures consistent builds across all

4
packages/web/package.json

@ -11,9 +11,7 @@
"bugs": { "bugs": {
"url": "https://github.com/meshtastic/web/issues" "url": "https://github.com/meshtastic/web/issues"
}, },
"simple-git-hooks": {
"pre-commit": "bun run check:fix"
},
"homepage": "https://meshtastic.org", "homepage": "https://meshtastic.org",
"scripts": { "scripts": {
"build": "bunx --bun vite build", "build": "bunx --bun vite build",

648
packages/web/src/components/CommandPalette/index.tsx

@ -1,11 +1,11 @@
import { Avatar } from "@components/UI/Avatar.tsx"; import { Avatar } from "@components/UI/Avatar.tsx";
import { import {
CommandDialog, CommandDialog,
CommandEmpty, CommandEmpty,
CommandGroup, CommandGroup,
CommandInput, CommandInput,
CommandItem, CommandItem,
CommandList, CommandList,
} from "@components/UI/Command.tsx"; } from "@components/UI/Command.tsx";
import { usePinnedItems } from "@core/hooks/usePinnedItems.ts"; import { usePinnedItems } from "@core/hooks/usePinnedItems.ts";
import { useAppStore } from "@core/stores/appStore.ts"; import { useAppStore } from "@core/stores/appStore.ts";
@ -14,345 +14,345 @@ import { cn } from "@core/utils/cn.ts";
import { useNavigate } from "@tanstack/react-router"; import { useNavigate } from "@tanstack/react-router";
import { useCommandState } from "cmdk"; import { useCommandState } from "cmdk";
import { import {
ArrowLeftRightIcon, ArrowLeftRightIcon,
BoxSelectIcon, BoxSelectIcon,
BugIcon, BugIcon,
CloudOff, CloudOff,
EraserIcon, EraserIcon,
FactoryIcon, FactoryIcon,
LayersIcon, LayersIcon,
LinkIcon, LinkIcon,
type LucideIcon, type LucideIcon,
MapIcon, MapIcon,
MessageSquareIcon, MessageSquareIcon,
Pin, Pin,
PlusIcon, PlusIcon,
PowerIcon, PowerIcon,
QrCodeIcon, QrCodeIcon,
RefreshCwIcon, RefreshCwIcon,
SettingsIcon, SettingsIcon,
SmartphoneIcon, SmartphoneIcon,
TrashIcon, TrashIcon,
UsersIcon, UsersIcon,
} from "lucide-react"; } from "lucide-react";
import { useEffect } from "react"; import { useEffect } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
export interface Group { export interface Group {
id: string; id: string;
label: string; label: string;
icon: LucideIcon; icon: LucideIcon;
commands: Command[]; commands: Command[];
} }
export interface Command { export interface Command {
label: string; label: string;
icon: LucideIcon; icon: LucideIcon;
action?: () => void; action?: () => void;
subItems?: SubItem[]; subItems?: SubItem[];
tags?: string[]; tags?: string[];
} }
export interface SubItem { export interface SubItem {
label: string; label: string;
icon: React.ReactNode; icon: React.ReactNode;
action: () => void; action: () => void;
} }
export const CommandPalette = () => { export const CommandPalette = () => {
const { const {
commandPaletteOpen, commandPaletteOpen,
setCommandPaletteOpen, setCommandPaletteOpen,
setConnectDialogOpen, setConnectDialogOpen,
setSelectedDevice, setSelectedDevice,
} = useAppStore(); } = useAppStore();
const { getDevices } = useDeviceStore(); const { getDevices } = useDeviceStore();
const { setDialogOpen, getNode, connection } = useDevice(); const { setDialogOpen, getNode, connection } = useDevice();
const { pinnedItems, togglePinnedItem } = usePinnedItems({ const { pinnedItems, togglePinnedItem } = usePinnedItems({
storageName: "pinnedCommandMenuGroups", storageName: "pinnedCommandMenuGroups",
}); });
const { t } = useTranslation("commandPalette"); const { t } = useTranslation("commandPalette");
const navigate = useNavigate({ from: "/" }); const navigate = useNavigate({ from: "/" });
const groups: Group[] = [ const groups: Group[] = [
{ {
id: "gotoGroup", id: "gotoGroup",
label: t("goto.label"), label: t("goto.label"),
icon: LinkIcon, icon: LinkIcon,
commands: [ commands: [
{ {
label: t("goto.command.messages"), label: t("goto.command.messages"),
icon: MessageSquareIcon, icon: MessageSquareIcon,
action() { action() {
navigate({ to: "/messages" }); navigate({ to: "/messages" });
}, },
}, },
{ {
label: t("goto.command.map"), label: t("goto.command.map"),
icon: MapIcon, icon: MapIcon,
action() { action() {
navigate({ to: "/map" }); navigate({ to: "/map" });
}, },
}, },
{ {
label: t("goto.command.config"), label: t("goto.command.config"),
icon: SettingsIcon, icon: SettingsIcon,
action() { action() {
navigate({ to: "/config" }); navigate({ to: "/config" });
}, },
tags: ["settings"], tags: ["settings"],
}, },
{ {
label: t("goto.command.channels"), label: t("goto.command.channels"),
icon: LayersIcon, icon: LayersIcon,
action() { action() {
navigate({ to: "/channels" }); navigate({ to: "/channels" });
}, },
}, },
{ {
label: t("goto.command.nodes"), label: t("goto.command.nodes"),
icon: UsersIcon, icon: UsersIcon,
action() { action() {
navigate({ to: "/nodes" }); navigate({ to: "/nodes" });
}, },
}, },
], ],
}, },
{ {
id: "manageGroup", id: "manageGroup",
label: t("manage.label"), label: t("manage.label"),
icon: SmartphoneIcon, icon: SmartphoneIcon,
commands: [ commands: [
{ {
label: t("manage.command.switchNode"), label: t("manage.command.switchNode"),
icon: ArrowLeftRightIcon, icon: ArrowLeftRightIcon,
subItems: getDevices().map((device) => ({ subItems: getDevices().map((device) => ({
label: label:
getNode(device.hardware.myNodeNum)?.user?.longName ?? getNode(device.hardware.myNodeNum)?.user?.longName ??
t("unknown.shortName"), t("unknown.shortName"),
icon: ( icon: (
<Avatar <Avatar
text={ text={
getNode(device.hardware.myNodeNum)?.user?.shortName ?? getNode(device.hardware.myNodeNum)?.user?.shortName ??
t("unknown.shortName") t("unknown.shortName")
} }
/> />
), ),
action() { action() {
setSelectedDevice(device.id); setSelectedDevice(device.id);
}, },
})), })),
}, },
{ {
label: t("manage.command.connectNewNode"), label: t("manage.command.connectNewNode"),
icon: PlusIcon, icon: PlusIcon,
action() { action() {
setConnectDialogOpen(true); setConnectDialogOpen(true);
}, },
}, },
], ],
}, },
{ {
id: "contextualGroup", id: "contextualGroup",
label: t("contextual.label"), label: t("contextual.label"),
icon: BoxSelectIcon, icon: BoxSelectIcon,
commands: [ commands: [
{ {
label: t("contextual.command.qrCode"), label: t("contextual.command.qrCode"),
icon: QrCodeIcon, icon: QrCodeIcon,
subItems: [ subItems: [
{ {
label: t("contextual.command.qrGenerator"), label: t("contextual.command.qrGenerator"),
icon: <QrCodeIcon size={16} />, icon: <QrCodeIcon size={16} />,
action() { action() {
setDialogOpen("QR", true); setDialogOpen("QR", true);
}, },
}, },
{ {
label: t("contextual.command.qrImport"), label: t("contextual.command.qrImport"),
icon: <QrCodeIcon size={16} />, icon: <QrCodeIcon size={16} />,
action() { action() {
setDialogOpen("import", true); setDialogOpen("import", true);
}, },
}, },
], ],
}, },
{ {
label: t("contextual.command.scheduleShutdown"), label: t("contextual.command.scheduleShutdown"),
icon: PowerIcon, icon: PowerIcon,
action() { action() {
setDialogOpen("shutdown", true); setDialogOpen("shutdown", true);
}, },
}, },
{ {
label: t("contextual.command.scheduleReboot"), label: t("contextual.command.scheduleReboot"),
icon: RefreshCwIcon, icon: RefreshCwIcon,
action() { action() {
setDialogOpen("reboot", true); setDialogOpen("reboot", true);
}, },
}, },
{ {
label: t("contextual.command.rebootToOtaMode"), label: t("contextual.command.rebootToOtaMode"),
icon: RefreshCwIcon, icon: RefreshCwIcon,
action() { action() {
setDialogOpen("rebootOTA", true); setDialogOpen("rebootOTA", true);
}, },
}, },
{ {
label: t("contextual.command.resetNodeDb"), label: t("contextual.command.resetNodeDb"),
icon: TrashIcon, icon: TrashIcon,
action() { action() {
connection?.resetNodes(); connection?.resetNodes();
}, },
}, },
{ {
label: t("contextual.command.disconnect"), label: t("contextual.command.disconnect"),
icon: CloudOff, icon: CloudOff,
action() { action() {
connection?.disconnect().catch((error) => { connection?.disconnect().catch((error) => {
console.error("Failed to disconnect:", error); console.error("Failed to disconnect:", error);
}); });
}, },
}, },
{ {
label: t("contextual.command.factoryResetDevice"), label: t("contextual.command.factoryResetDevice"),
icon: FactoryIcon, icon: FactoryIcon,
action() { action() {
connection?.factoryResetDevice(); connection?.factoryResetDevice();
}, },
}, },
{ {
label: t("contextual.command.factoryResetConfig"), label: t("contextual.command.factoryResetConfig"),
icon: FactoryIcon, icon: FactoryIcon,
action() { action() {
connection?.factoryResetConfig(); connection?.factoryResetConfig();
}, },
}, },
], ],
}, },
{ {
id: "debugGroup", id: "debugGroup",
label: t("debug.label"), label: t("debug.label"),
icon: BugIcon, icon: BugIcon,
commands: [ commands: [
{ {
label: t("debug.command.reconfigure"), label: t("debug.command.reconfigure"),
icon: RefreshCwIcon, icon: RefreshCwIcon,
action() { action() {
void connection?.configure(); void connection?.configure();
}, },
}, },
{ {
label: t("debug.command.clearAllStoredMessages"), label: t("debug.command.clearAllStoredMessages"),
icon: EraserIcon, icon: EraserIcon,
action() { action() {
setDialogOpen("deleteMessages", true); setDialogOpen("deleteMessages", true);
}, },
}, },
], ],
}, },
]; ];
const sortedGroups = [...groups].sort((a, b) => { const sortedGroups = [...groups].sort((a, b) => {
const aPinned = pinnedItems.includes(a.id) ? 1 : 0; const aPinned = pinnedItems.includes(a.id) ? 1 : 0;
const bPinned = pinnedItems.includes(b.id) ? 1 : 0; const bPinned = pinnedItems.includes(b.id) ? 1 : 0;
return bPinned - aPinned; return bPinned - aPinned;
}); });
useEffect(() => { useEffect(() => {
const handleKeydown = (e: KeyboardEvent) => { const handleKeydown = (e: KeyboardEvent) => {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) { if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
e.preventDefault(); e.preventDefault();
setCommandPaletteOpen(true); setCommandPaletteOpen(true);
} }
}; };
globalThis.addEventListener("keydown", handleKeydown); globalThis.addEventListener("keydown", handleKeydown);
return () => globalThis.removeEventListener("keydown", handleKeydown); return () => globalThis.removeEventListener("keydown", handleKeydown);
}, [setCommandPaletteOpen]); }, [setCommandPaletteOpen]);
return ( return (
<CommandDialog <CommandDialog
open={commandPaletteOpen} open={commandPaletteOpen}
onOpenChange={setCommandPaletteOpen} onOpenChange={setCommandPaletteOpen}
> >
<CommandInput placeholder={t("search.commandPalette")} /> <CommandInput placeholder={t("search.commandPalette")} />
<CommandList> <CommandList>
<CommandEmpty>{t("emptyState")}</CommandEmpty> <CommandEmpty>{t("emptyState")}</CommandEmpty>
{sortedGroups.map((group) => ( {sortedGroups.map((group) => (
<CommandGroup <CommandGroup
key={group.label} key={group.label}
heading={ heading={
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span>{group.label}</span> <span>{group.label}</span>
<button <button
type="button" type="button"
onClick={() => togglePinnedItem(group.id)} onClick={() => togglePinnedItem(group.id)}
className={cn( className={cn(
"transition-all duration-300 scale-100 cursor-pointer p-2 focus:*:data-label:opacity-100", "transition-all duration-300 scale-100 cursor-pointer p-2 focus:*:data-label:opacity-100",
)} )}
> >
<span <span
data-label data-label
className="transition-all block absolute w-full mb-auto mt-auto ml-0 mr-0 text-xs left-0 -top-5 opacity-0 rounded-lg" className="transition-all block absolute w-full mb-auto mt-auto ml-0 mr-0 text-xs left-0 -top-5 opacity-0 rounded-lg"
/> />
<Pin <Pin
size={16} size={16}
className={cn( className={cn(
"transition-opacity", "transition-opacity",
pinnedItems.includes(group.id) pinnedItems.includes(group.id)
? "opacity-100 text-red-500" ? "opacity-100 text-red-500"
: "opacity-40 hover:opacity-70", : "opacity-40 hover:opacity-70",
)} )}
/> />
</button> </button>
</div> </div>
} }
> >
{group.commands.map((command) => ( {group.commands.map((command) => (
<div key={command.label}> <div key={command.label}>
<CommandItem <CommandItem
onSelect={() => { onSelect={() => {
command.action?.(); command.action?.();
setCommandPaletteOpen(false); setCommandPaletteOpen(false);
}} }}
> >
<command.icon size={16} className="mr-2" /> <command.icon size={16} className="mr-2" />
{command.label} {command.label}
</CommandItem> </CommandItem>
{command.subItems?.map((subItem) => ( {command.subItems?.map((subItem) => (
<SubItem <SubItem
key={subItem.label} key={subItem.label}
label={subItem.label} label={subItem.label}
icon={subItem.icon} icon={subItem.icon}
action={subItem.action} action={subItem.action}
/> />
))} ))}
</div> </div>
))} ))}
</CommandGroup> </CommandGroup>
))} ))}
</CommandList> </CommandList>
</CommandDialog> </CommandDialog>
); );
}; };
const SubItem = ({ const SubItem = ({
label, label,
icon, icon,
action, action,
}: { }: {
label: string; label: string;
icon: React.ReactNode; icon: React.ReactNode;
action: () => void; action: () => void;
}) => { }) => {
const search = useCommandState((state) => state.search); const search = useCommandState((state) => state.search);
if (!search) { if (!search) {
return null; return null;
} }
return ( return (
<CommandItem onSelect={action}> <CommandItem onSelect={action}>
{icon} {icon}
{label} {label}
</CommandItem> </CommandItem>
); );
}; };

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

@ -1,11 +1,11 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />
interface ImportMetaEnv { interface ImportMetaEnv {
readonly env: { readonly env: {
readonly VITE_COMMIT_HASH: string; readonly VITE_COMMIT_HASH: string;
}; };
} }
interface ImportMeta { interface ImportMeta {
readonly env: ImportMetaEnv; readonly env: ImportMetaEnv;
} }

92
packages/web/vite.config.ts

@ -7,56 +7,56 @@ import { defineConfig } from "vite";
let hash = ""; let hash = "";
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) {
console.error("Error getting git hash:", error); console.error("Error getting git hash:", error);
hash = "DEV"; hash = "DEV";
} }
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';";
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
react(), react(),
tailwindcss(), tailwindcss(),
// VitePWA({ // VitePWA({
// registerType: "autoUpdate", // registerType: "autoUpdate",
// strategies: "generateSW", // strategies: "generateSW",
// devOptions: { // devOptions: {
// enabled: true, // enabled: true,
// }, // },
// workbox: { // workbox: {
// cleanupOutdatedCaches: true, // cleanupOutdatedCaches: true,
// sourcemap: true, // sourcemap: true,
// }, // },
// }), // }),
], ],
optimizeDeps: { optimizeDeps: {
include: ["react/jsx-runtime"], include: ["react/jsx-runtime"],
}, },
define: { define: {
"import.meta.env.VITE_COMMIT_HASH": JSON.stringify(hash), "import.meta.env.VITE_COMMIT_HASH": JSON.stringify(hash),
}, },
build: { build: {
emptyOutDir: true, emptyOutDir: true,
assetsDir: "./", assetsDir: "./",
}, },
resolve: { resolve: {
alias: { alias: {
"@app": path.resolve(process.cwd(), "./src"), "@app": path.resolve(process.cwd(), "./src"),
"@pages": path.resolve(process.cwd(), "./src/pages"), "@pages": path.resolve(process.cwd(), "./src/pages"),
"@components": path.resolve(process.cwd(), "./src/components"), "@components": path.resolve(process.cwd(), "./src/components"),
"@core": path.resolve(process.cwd(), "./src/core"), "@core": path.resolve(process.cwd(), "./src/core"),
"@layouts": path.resolve(process.cwd(), "./src/layouts"), "@layouts": path.resolve(process.cwd(), "./src/layouts"),
}, },
}, },
server: { server: {
port: 3000, port: 3000,
headers: { headers: {
"content-security-policy": CONTENT_SECURITY_POLICY, "content-security-policy": CONTENT_SECURITY_POLICY,
"Cross-Origin-Opener-Policy": "same-origin", "Cross-Origin-Opener-Policy": "same-origin",
"Cross-Origin-Embedder-Policy": "require-corp", "Cross-Origin-Embedder-Policy": "require-corp",
}, },
}, },
}); });

42
packages/web/vitest.config.ts

@ -7,25 +7,25 @@ import { enableMapSet } from "immer";
enableMapSet(); enableMapSet();
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
resolve: { resolve: {
alias: { alias: {
"@app": path.resolve(process.cwd(), "./src"), "@app": path.resolve(process.cwd(), "./src"),
"@public": path.resolve(process.cwd(), "./public"), "@public": path.resolve(process.cwd(), "./public"),
"@core": path.resolve(process.cwd(), "./src/core"), "@core": path.resolve(process.cwd(), "./src/core"),
"@pages": path.resolve(process.cwd(), "./src/pages"), "@pages": path.resolve(process.cwd(), "./src/pages"),
"@components": path.resolve(process.cwd(), "./src/components"), "@components": path.resolve(process.cwd(), "./src/components"),
"@layouts": path.resolve(process.cwd(), "./src/layouts"), "@layouts": path.resolve(process.cwd(), "./src/layouts"),
}, },
}, },
test: { test: {
environment: "happy-dom", environment: "happy-dom",
globals: true, globals: true,
mockReset: true, mockReset: true,
clearMocks: true, clearMocks: true,
restoreMocks: true, restoreMocks: true,
root: path.resolve(process.cwd(), "./src"), root: path.resolve(process.cwd(), "./src"),
include: ["**/*.{test,spec}.{ts,tsx}"], include: ["**/*.{test,spec}.{ts,tsx}"],
setupFiles: ["./src/tests/setup.ts"], setupFiles: ["./src/tests/setup.ts"],
}, },
}); });

128
scripts/build_npm_package.ts

@ -1,57 +1,57 @@
import { join } from "jsr:@std/path@1/join"; import { build, emptyDir } from "https://jsr.io/@deno/dnt/0.42.3/mod.ts";
import { build, emptyDir } from "@deno/dnt"; import { join } from "https://jsr.io/@std/path/1.1.1/mod.ts";
interface DenoJsonConfig { interface DenoJsonConfig {
name: string; name: string;
version: string; version: string;
description: string; description: string;
imports?: Record<string, string>; imports?: Record<string, string>;
exports?: Record<string, string>; exports?: Record<string, string>;
} }
async function getJson(filePath: string) { async function getJson(filePath: string) {
try { try {
return JSON.parse(await Deno.readTextFile(filePath)); return JSON.parse(await Deno.readTextFile(filePath));
} catch (e) { } catch (e) {
if (e instanceof Error) { if (e instanceof Error) {
throw new Error(`Error reading or parsing ${filePath}: ${e.message}`); throw new Error(`Error reading or parsing ${filePath}: ${e.message}`);
} }
} }
} }
if (Deno.args.length !== 1) { if (Deno.args.length !== 1) {
console.error("Usage: deno task build:npm <path-to-package>"); console.error("Usage: deno task build:npm <path-to-package>");
console.error("Example: deno task build:npm packages/core"); console.error("Example: deno task build:npm packages/core");
Deno.exit(1); Deno.exit(1);
} }
const packagePath = Deno.args[0]; const packagePath = Deno.args[0];
const denoJsonPath = join(packagePath, "deno.json"); const denoJsonPath = join(packagePath, "package.json");
const outDir = join(packagePath, "npm"); const outDir = join(packagePath, "npm");
// Read the deno.json file to get the package metadata. // Read the deno.json file to get the package metadata.
let jsonContent: DenoJsonConfig; let jsonContent: DenoJsonConfig;
try { try {
jsonContent = await getJson(denoJsonPath); jsonContent = await getJson(denoJsonPath);
} catch (error) { } catch (error) {
console.log(`Error reading or parsing ${denoJsonPath}:`, error); console.log(`Error reading or parsing ${denoJsonPath}:`, error);
if (error instanceof Deno.errors.NotFound) { if (error instanceof Deno.errors.NotFound) {
console.error(`Error: Config file not found at ${denoJsonPath}`); console.error(`Error: Config file not found at ${denoJsonPath}`);
} else { } else {
console.error(`Error reading or parsing ${denoJsonPath}:`, error); console.error(`Error reading or parsing ${denoJsonPath}:`, error);
} }
Deno.exit(1); Deno.exit(1);
} }
const { name, version, description } = jsonContent; const { name, version, description } = jsonContent;
if (!name || !version || !description) { if (!name || !version || !description) {
console.error( console.error(
`Error: 'name', 'version', and 'description' must be defined in ${denoJsonPath}`, `Error: 'name', 'version', and 'description' must be defined in ${denoJsonPath}`,
); );
Deno.exit(1); Deno.exit(1);
} }
console.log(`Building ${name}@${version} from ${packagePath}...`); console.log(`Building ${name}@${version} from ${packagePath}...`);
@ -60,40 +60,42 @@ console.log(`Building ${name}@${version} from ${packagePath}...`);
await emptyDir(outDir); await emptyDir(outDir);
try { try {
await build({ await build({
entryPoints: [join(packagePath, "mod.ts")], entryPoints: [join(packagePath, "mod.ts")],
outDir, outDir,
test: false, test: false,
shims: { esModule: true,
deno: true, declaration: false,
}, shims: {
package: { deno: true,
name, },
version, package: {
description, name,
license: "GPL-3.0-only", version,
repository: { description,
type: "git", license: "GPL-3.0-only",
url: "git+https://github.com/meshtastic/web.git", repository: {
}, type: "git",
bugs: { url: "git+https://github.com/meshtastic/web.git",
url: "https://github.com/meshtastic/web/issues", },
}, bugs: {
}, url: "https://github.com/meshtastic/web/issues",
compilerOptions: { },
lib: ["DOM", "ESNext"], },
}, compilerOptions: {
postBuild() { lib: ["DOM", "ESNext"],
Deno.copyFileSync("LICENSE", join(outDir, "LICENSE")); },
Deno.copyFileSync( postBuild() {
join(packagePath, "README.md"), Deno.copyFileSync("LICENSE", join(outDir, "LICENSE"));
join(outDir, "README.md"), Deno.copyFileSync(
); join(packagePath, "README.md"),
}, join(outDir, "README.md"),
}); );
},
});
} catch (error) { } catch (error) {
console.error(`Error building ${name}@${version}:`, error); console.error(`Error building ${name}@${version}:`, error);
Deno.exit(1); Deno.exit(1);
} }
console.log(`✅ Successfully built ${name}@${version} to ${outDir}`); console.log(`✅ Successfully built ${name}@${version} to ${outDir}`);

Loading…
Cancel
Save