Browse Source
* feat: move js repo into web monorepo * added readme * Update packages/core/README.md Co-authored-by: Copilot <[email protected]> * Update packages/transport-http/README.md Co-authored-by: Copilot <[email protected]> * Update packages/transport-web-bluetooth/README.md Co-authored-by: Copilot <[email protected]> --------- Co-authored-by: Copilot <[email protected]>pull/685/head
committed by
GitHub
34 changed files with 3249 additions and 24 deletions
@ -0,0 +1,37 @@ |
|||||
|
name: Pull Request |
||||
|
|
||||
|
on: |
||||
|
push: |
||||
|
paths: |
||||
|
- "packages/core" |
||||
|
- "packages/transport-deno" |
||||
|
- "packages/transport-http" |
||||
|
- "packages/transport-web-bluetooth" |
||||
|
- "packages/transport-web-serial" |
||||
|
branches: |
||||
|
- main |
||||
|
|
||||
|
jobs: |
||||
|
build: |
||||
|
runs-on: ubuntu-latest |
||||
|
|
||||
|
permissions: |
||||
|
contents: read |
||||
|
id-token: write |
||||
|
|
||||
|
steps: |
||||
|
- name: Checkout code |
||||
|
uses: actions/checkout@v4 |
||||
|
|
||||
|
- uses: denoland/setup-deno@v2 |
||||
|
with: |
||||
|
deno-version: v2.x |
||||
|
|
||||
|
- name: Check formatting |
||||
|
run: deno fmt --check |
||||
|
|
||||
|
- name: Check types |
||||
|
run: deno lint |
||||
|
|
||||
|
- name: Publish to JSR |
||||
|
run: npx jsr publish |
||||
@ -1,3 +0,0 @@ |
|||||
[submodule "src/core/connection"] |
|
||||
path = src/core/connection |
|
||||
url = https://github.com/meshtastic/js.git |
|
||||
@ -0,0 +1,158 @@ |
|||||
|
# Meshtastic Web Monorepo |
||||
|
|
||||
|
[](https://github.com/meshtastic/web/actions/workflows/ci.yml) |
||||
|
[](https://github.com/meshtastic/js/actions/workflows/ci.yml) |
||||
|
[](https://cla-assistant.io/meshtastic/web) |
||||
|
[](https://opencollective.com/meshtastic/) |
||||
|
[](https://vercel.com?utm_source=meshtastic&utm_campaign=oss) |
||||
|
|
||||
|
## Overview |
||||
|
|
||||
|
This monorepo consolidates the official [Meshtastic](https://meshtastic.org) web |
||||
|
interface and its supporting JavaScript libraries. It aims to provide a unified |
||||
|
development experience for interacting with Meshtastic devices. |
||||
|
|
||||
|
### Projects within this Monorepo (`packages/`) |
||||
|
|
||||
|
All projects are located within the `packages/` directory: |
||||
|
|
||||
|
- **`packages/web` (Meshtastic Web Client):** The official web interface, |
||||
|
designed to be hosted or served directly from a Meshtastic node. |
||||
|
- **[Hosted version](https://client.meshtastic.org)** |
||||
|
- **`packages/core`:** Core functionality for Meshtastic JS. |
||||
|
- **`packages/transport-deno`:** TCP Transport for the Deno runtime. |
||||
|
- **`packages/transport-http`:** HTTP Transport. |
||||
|
- **`packages/transport-web-bluetooth`:** Web Bluetooth Transport. |
||||
|
- **`packages/transport-web-serial`:** Web Serial Transport. |
||||
|
|
||||
|
All `Meshtastic JS` packages (core and transports) are published to |
||||
|
[JSR](https://jsr.io/@meshtastic). |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## Stats |
||||
|
|
||||
|
| Project | Repobeats | |
||||
|
| :-------------------- | :-------------------------------------------------------------------------------------------------------------------- | |
||||
|
| Meshtastic Web Client |  | |
||||
|
| Meshtastic JS |  | |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## Tech Stack |
||||
|
|
||||
|
This monorepo leverages the following technologies: |
||||
|
|
||||
|
- **Runtime:** Deno |
||||
|
- **Web Client:** React.js |
||||
|
- **Styling:** Tailwind CSS |
||||
|
- **Bundling:** Vite |
||||
|
- **Language:** TypeScript |
||||
|
- **Testing:** Vitest, React Testing Library |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## Getting Started |
||||
|
|
||||
|
### Prerequisites |
||||
|
|
||||
|
You'll need to have [Deno](https://deno.com/) installed to work with this |
||||
|
monorepo. Follow the installation instructions on their home page. |
||||
|
|
||||
|
### Development Setup |
||||
|
|
||||
|
1. **Clone the repository:** |
||||
|
```bash |
||||
|
git clone [https://github.com/meshtastic/meshtastic-web.git](https://github.com/meshtastic/meshtastic-web.git) |
||||
|
cd meshtastic-web |
||||
|
``` |
||||
|
2. **Install dependencies for all packages:** |
||||
|
```bash |
||||
|
deno i |
||||
|
``` |
||||
|
This command installs all necessary dependencies for all packages within the |
||||
|
monorepo. |
||||
|
|
||||
|
### Running Projects |
||||
|
|
||||
|
#### Meshtastic Web Client |
||||
|
|
||||
|
To start the development server for the web client: |
||||
|
|
||||
|
```bash |
||||
|
deno task dev --filter packages/web |
||||
|
``` |
||||
|
|
||||
|
This will typically run the web client on http://localhost:3000 and requires a |
||||
|
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 deno.json files. For |
||||
|
example, to run tests for a specific package: |
||||
|
|
||||
|
```bash |
||||
|
deno test packages/core |
||||
|
``` |
||||
|
|
||||
|
### Building All Projects |
||||
|
|
||||
|
To build all projects in this monorepo: |
||||
|
|
||||
|
```bash |
||||
|
deno task build --filter . |
||||
|
``` |
||||
|
|
||||
|
This will build both the web client and all JS packages. |
||||
|
|
||||
|
### Feedback |
||||
|
|
||||
|
If you encounter any issues with nightly builds, please report them in our |
||||
|
[issues tracker](https://github.com/meshtastic/web/issues). Your feedback helps |
||||
|
improve the stability of future releases |
||||
|
|
||||
|
### Why Deno? |
||||
|
|
||||
|
Meshtastic Web uses Deno as its development platform for several compelling |
||||
|
reasons: |
||||
|
|
||||
|
- **Built-in Security**: Deno's security-first approach requires explicit |
||||
|
permissions for file, network, and environment access, reducing vulnerability |
||||
|
risks. |
||||
|
- **TypeScript Support**: Native TypeScript support without additional |
||||
|
configuration, enhancing code quality and developer experience. |
||||
|
- **Modern JavaScript**: First-class support for ESM imports, top-level await, |
||||
|
and other modern JavaScript features. |
||||
|
- **Simplified Tooling**: Built-in formatter, linter, test runner, and bundler |
||||
|
eliminate the need for multiple third-party tools. |
||||
|
- **Reproducible Builds**: Lockfile ensures consistent builds across all |
||||
|
environments. |
||||
|
- **Web Standard APIs**: Uses browser-compatible APIs, making code more portable |
||||
|
between server and client environments. |
||||
|
|
||||
|
### Contributing |
||||
|
|
||||
|
We welcome contributions! Here’s how the deployment flow works for pull |
||||
|
requests: |
||||
|
|
||||
|
- **Preview Deployments:**\ |
||||
|
Every pull request automatically generates a preview deployment on Vercel. |
||||
|
This allows you and reviewers to easily preview changes before merging. |
||||
|
|
||||
|
- **Staging Environment (`client-test`):**\ |
||||
|
Once your PR is merged, your changes will be available on our staging site: |
||||
|
[client-test.meshtastic.org](https://client-test.meshtastic.org/).\ |
||||
|
This environment supports rapid feature iteration and testing without |
||||
|
impacting the production site. |
||||
|
|
||||
|
- **Production Releases:**\ |
||||
|
At regular intervals, stable and fully tested releases are promoted to our |
||||
|
production site: [client.meshtastic.org](https://client.meshtastic.org/).\ |
||||
|
This is the primary interface used by the public to connect with their |
||||
|
Meshtastic nodes. |
||||
|
|
||||
|
Please review our |
||||
|
[Contribution Guidelines](https://github.com/meshtastic/web/blob/main/CONTRIBUTING.md) |
||||
|
before submitting a pull request. We appreciate your help in making the project |
||||
|
better! |
||||
@ -1,7 +1,9 @@ |
|||||
{ |
{ |
||||
"version": "5", |
"version": "5", |
||||
"specifiers": { |
"specifiers": { |
||||
|
"jsr:@meshtastic/protobufs@^2.7.0": "2.7.0", |
||||
"jsr:@std/path@^1.1.0": "1.1.0", |
"jsr:@std/path@^1.1.0": "1.1.0", |
||||
|
"npm:@bufbuild/protobuf@^2.2.3": "2.5.2", |
||||
"npm:@bufbuild/protobuf@^2.2.5": "2.5.2", |
"npm:@bufbuild/protobuf@^2.2.5": "2.5.2", |
||||
"npm:@hookform/resolvers@^5.1.1": "[email protected][email protected][email protected]", |
"npm:@hookform/resolvers@^5.1.1": "[email protected][email protected][email protected]", |
||||
"npm:@jsr/[email protected]": "2.6.4", |
"npm:@jsr/[email protected]": "2.6.4", |
||||
@ -31,27 +33,31 @@ |
|||||
"npm:@tanstack/react-router@^1.120.15": "[email protected][email protected][email protected]", |
"npm:@tanstack/react-router@^1.120.15": "[email protected][email protected][email protected]", |
||||
"npm:@tanstack/router-cli@^1.121.37": "1.121.37", |
"npm:@tanstack/router-cli@^1.121.37": "1.121.37", |
||||
"npm:@tanstack/router-devtools@^1.120.15": "1.121.34_@[email protected][email protected][email protected][email protected][email protected][email protected][email protected]", |
"npm:@tanstack/router-devtools@^1.120.15": "1.121.34_@[email protected][email protected][email protected][email protected][email protected][email protected][email protected]", |
||||
"npm:@tanstack/router-plugin@^1.120.15": "1.121.37_@[email protected][email protected][email protected][email protected][email protected]__@[email protected][email protected]_@[email protected][email protected][email protected][email protected]_@[email protected]", |
"npm:@tanstack/router-plugin@^1.120.15": "1.121.37_@[email protected][email protected][email protected][email protected][email protected]__@[email protected][email protected]_@[email protected][email protected][email protected][email protected]_@[email protected]_@[email protected]", |
||||
"npm:@testing-library/jest-dom@^6.6.3": "6.6.3", |
"npm:@testing-library/jest-dom@^6.6.3": "6.6.3", |
||||
"npm:@testing-library/react@^16.3.0": "16.3.0_@[email protected]_@[email protected]_@[email protected]__@[email protected][email protected][email protected][email protected]", |
"npm:@testing-library/react@^16.3.0": "16.3.0_@[email protected]_@[email protected]_@[email protected]__@[email protected][email protected][email protected][email protected]", |
||||
"npm:@testing-library/user-event@^14.6.1": "14.6.1_@[email protected]", |
"npm:@testing-library/user-event@^14.6.1": "14.6.1_@[email protected]", |
||||
"npm:@turf/turf@^7.2.0": "7.2.0", |
"npm:@turf/turf@^7.2.0": "7.2.0", |
||||
"npm:@types/chrome@^0.0.318": "0.0.318", |
"npm:@types/chrome@^0.0.318": "0.0.318", |
||||
"npm:@types/js-cookie@^3.0.6": "3.0.6", |
"npm:@types/js-cookie@^3.0.6": "3.0.6", |
||||
|
"npm:@types/node@^22.13.10": "22.15.33", |
||||
"npm:@types/node@^24.0.4": "24.0.4", |
"npm:@types/node@^24.0.4": "24.0.4", |
||||
"npm:@types/react-dom@^19.1.3": "19.1.6_@[email protected]", |
"npm:@types/react-dom@^19.1.3": "19.1.6_@[email protected]", |
||||
"npm:@types/react@^19.1.2": "19.1.8", |
"npm:@types/react@^19.1.2": "19.1.8", |
||||
"npm:@types/serviceworker@^0.0.133": "0.0.133", |
"npm:@types/serviceworker@^0.0.133": "0.0.133", |
||||
"npm:@types/w3c-web-serial@*": "1.0.8", |
"npm:@types/w3c-web-serial@*": "1.0.8", |
||||
|
"npm:@types/w3c-web-serial@^1.0.7": "1.0.8", |
||||
"npm:@types/w3c-web-serial@^1.0.8": "1.0.8", |
"npm:@types/w3c-web-serial@^1.0.8": "1.0.8", |
||||
"npm:@types/web-bluetooth@*": "0.0.21", |
"npm:@types/web-bluetooth@*": "0.0.21", |
||||
|
"npm:@types/web-bluetooth@^0.0.20": "0.0.20", |
||||
"npm:@types/web-bluetooth@^0.0.21": "0.0.21", |
"npm:@types/web-bluetooth@^0.0.21": "0.0.21", |
||||
"npm:@vitejs/plugin-react@^4.4.1": "[email protected]__@[email protected][email protected]_@[email protected]_@[email protected]", |
"npm:@vitejs/plugin-react@^4.4.1": "[email protected]__@[email protected][email protected]_@[email protected]_@[email protected]_@[email protected]", |
||||
"npm:autoprefixer@^10.4.21": "[email protected]", |
"npm:autoprefixer@^10.4.21": "[email protected]", |
||||
"npm:base64-js@^1.5.1": "1.5.1", |
"npm:base64-js@^1.5.1": "1.5.1", |
||||
"npm:class-variance-authority@~0.7.1": "0.7.1", |
"npm:class-variance-authority@~0.7.1": "0.7.1", |
||||
"npm:clsx@^2.1.1": "2.1.1", |
"npm:clsx@^2.1.1": "2.1.1", |
||||
"npm:cmdk@^1.1.1": "[email protected][email protected][email protected]_@[email protected]_@[email protected]__@[email protected]", |
"npm:cmdk@^1.1.1": "[email protected][email protected][email protected]_@[email protected]_@[email protected]__@[email protected]", |
||||
|
"npm:crc@^4.3.2": "4.3.2", |
||||
"npm:crypto-random-string@5": "5.0.0", |
"npm:crypto-random-string@5": "5.0.0", |
||||
"npm:gzipper@^8.2.1": "8.2.1", |
"npm:gzipper@^8.2.1": "8.2.1", |
||||
"npm:happy-dom@^17.4.6": "17.6.3", |
"npm:happy-dom@^17.4.6": "17.6.3", |
||||
@ -73,19 +79,27 @@ |
|||||
"npm:react@^19.1.0": "19.1.0", |
"npm:react@^19.1.0": "19.1.0", |
||||
"npm:rfc4648@^1.5.4": "1.5.4", |
"npm:rfc4648@^1.5.4": "1.5.4", |
||||
"npm:simple-git-hooks@^2.13.0": "2.13.0", |
"npm:simple-git-hooks@^2.13.0": "2.13.0", |
||||
|
"npm:ste-simple-events@^3.0.11": "3.0.11", |
||||
"npm:tailwind-merge@^3.2.0": "3.3.1", |
"npm:tailwind-merge@^3.2.0": "3.3.1", |
||||
"npm:tailwindcss-animate@^1.0.7": "[email protected]", |
"npm:tailwindcss-animate@^1.0.7": "[email protected]", |
||||
"npm:tailwindcss@^4.1.5": "4.1.10", |
"npm:tailwindcss@^4.1.5": "4.1.10", |
||||
"npm:tar@^7.4.3": "7.4.3", |
"npm:tar@^7.4.3": "7.4.3", |
||||
"npm:testing-library@^0.0.2": "0.0.2_@[email protected]__@[email protected][email protected][email protected][email protected]_@[email protected][email protected][email protected]", |
"npm:testing-library@^0.0.2": "0.0.2_@[email protected]__@[email protected][email protected][email protected][email protected]_@[email protected][email protected][email protected]", |
||||
|
"npm:tslog@^4.9.3": "4.9.3", |
||||
"npm:typescript@^5.8.3": "5.8.3", |
"npm:typescript@^5.8.3": "5.8.3", |
||||
"npm:vite@*": "7.0.0_@[email protected][email protected]", |
"npm:vite@*": "7.0.0_@[email protected][email protected]_@[email protected]", |
||||
"npm:vite@7": "7.0.0_@[email protected][email protected]", |
"npm:vite@7": "7.0.0_@[email protected][email protected]_@[email protected]", |
||||
"npm:vitest@^3.2.4": "3.2.4_@[email protected][email protected][email protected]__@[email protected][email protected]", |
"npm:vitest@^3.2.4": "3.2.4_@[email protected][email protected][email protected]__@[email protected][email protected]_@[email protected]", |
||||
"npm:zod@^3.25.67": "3.25.67", |
"npm:zod@^3.25.67": "3.25.67", |
||||
"npm:[email protected]": "5.0.5_@[email protected][email protected][email protected]" |
"npm:[email protected]": "5.0.5_@[email protected][email protected][email protected]" |
||||
}, |
}, |
||||
"jsr": { |
"jsr": { |
||||
|
"@meshtastic/[email protected]": { |
||||
|
"integrity": "38357241bd8a7431c87366dbe12ce9e69f204ebb6ec23da12f7682765b6c8376", |
||||
|
"dependencies": [ |
||||
|
"npm:@bufbuild/protobuf@^2.2.3" |
||||
|
] |
||||
|
}, |
||||
"@std/[email protected]": { |
"@std/[email protected]": { |
||||
"integrity": "ddc94f8e3c275627281cbc23341df6b8bcc874d70374f75fec2533521e3d6886" |
"integrity": "ddc94f8e3c275627281cbc23341df6b8bcc874d70374f75fec2533521e3d6886" |
||||
} |
} |
||||
@ -1756,12 +1770,37 @@ |
|||||
"babel-dead-code-elimination", |
"babel-dead-code-elimination", |
||||
"chokidar", |
"chokidar", |
||||
"unplugin", |
"unplugin", |
||||
"vite", |
"vite@7.0.0_@[email protected][email protected]", |
||||
"zod" |
"zod" |
||||
], |
], |
||||
"optionalPeers": [ |
"optionalPeers": [ |
||||
"@tanstack/react-router", |
"@tanstack/react-router", |
||||
"vite" |
"[email protected]_@[email protected][email protected]" |
||||
|
] |
||||
|
}, |
||||
|
"@tanstack/[email protected]_@[email protected][email protected][email protected][email protected][email protected]__@[email protected][email protected]_@[email protected][email protected][email protected][email protected]_@[email protected]_@[email protected]": { |
||||
|
"integrity": "sha512-zrolQ1J53xDUdxdO6MLfvnpVINnkIfOnEDVeX3kwHKBGQ5zyGdbolVcVVrJIRYQS0SJoWesn8cf8j+z+u8nZtg==", |
||||
|
"dependencies": [ |
||||
|
"@babel/core", |
||||
|
"@babel/plugin-syntax-jsx", |
||||
|
"@babel/plugin-syntax-typescript", |
||||
|
"@babel/template", |
||||
|
"@babel/traverse", |
||||
|
"@babel/types", |
||||
|
"@tanstack/react-router", |
||||
|
"@tanstack/router-core", |
||||
|
"@tanstack/router-generator", |
||||
|
"@tanstack/router-utils", |
||||
|
"@tanstack/virtual-file-routes", |
||||
|
"babel-dead-code-elimination", |
||||
|
"chokidar", |
||||
|
"unplugin", |
||||
|
"[email protected]_@[email protected][email protected]_@[email protected]", |
||||
|
"zod" |
||||
|
], |
||||
|
"optionalPeers": [ |
||||
|
"@tanstack/react-router", |
||||
|
"[email protected]_@[email protected][email protected]_@[email protected]" |
||||
] |
] |
||||
}, |
}, |
||||
"@tanstack/[email protected]_@[email protected]": { |
"@tanstack/[email protected]_@[email protected]": { |
||||
@ -3262,10 +3301,16 @@ |
|||||
"@types/pbf" |
"@types/pbf" |
||||
] |
] |
||||
}, |
}, |
||||
|
"@types/[email protected]": { |
||||
|
"integrity": "sha512-wzoocdnnpSxZ+6CjW4ADCK1jVmd1S/J3ArNWfn8FDDQtRm8dkDg7TA+mvek2wNrfCgwuZxqEOiB9B1XCJ6+dbw==", |
||||
|
"dependencies": [ |
||||
|
"[email protected]" |
||||
|
] |
||||
|
}, |
||||
"@types/[email protected]": { |
"@types/[email protected]": { |
||||
"integrity": "sha512-ulyqAkrhnuNq9pB76DRBTkcS6YsmDALy6Ua63V8OhrOBgbcYt6IOdzpw5P1+dyRIyMerzLkeYWBeOXPpA9GMAA==", |
"integrity": "sha512-ulyqAkrhnuNq9pB76DRBTkcS6YsmDALy6Ua63V8OhrOBgbcYt6IOdzpw5P1+dyRIyMerzLkeYWBeOXPpA9GMAA==", |
||||
"dependencies": [ |
"dependencies": [ |
||||
"undici-types" |
"undici-types@7.8.0" |
||||
] |
] |
||||
}, |
}, |
||||
"@types/[email protected]": { |
"@types/[email protected]": { |
||||
@ -3295,6 +3340,9 @@ |
|||||
"@types/[email protected]": { |
"@types/[email protected]": { |
||||
"integrity": "sha512-QQOT+bxQJhRGXoZDZGLs3ksLud1dMNnMiSQtBA0w8KXvLpXX4oM4TZb6J0GgJ8UbCaHo5s9/4VQT8uXy9JER2A==" |
"integrity": "sha512-QQOT+bxQJhRGXoZDZGLs3ksLud1dMNnMiSQtBA0w8KXvLpXX4oM4TZb6J0GgJ8UbCaHo5s9/4VQT8uXy9JER2A==" |
||||
}, |
}, |
||||
|
"@types/[email protected]": { |
||||
|
"integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==" |
||||
|
}, |
||||
"@types/[email protected]": { |
"@types/[email protected]": { |
||||
"integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==" |
"integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==" |
||||
}, |
}, |
||||
@ -3326,7 +3374,19 @@ |
|||||
"@rolldown/pluginutils", |
"@rolldown/pluginutils", |
||||
"@types/babel__core", |
"@types/babel__core", |
||||
"react-refresh", |
"react-refresh", |
||||
"vite" |
"[email protected]_@[email protected][email protected]" |
||||
|
] |
||||
|
}, |
||||
|
"@vitejs/[email protected][email protected]__@[email protected][email protected]_@[email protected]_@[email protected]_@[email protected]": { |
||||
|
"integrity": "sha512-5Kgff+m8e2PB+9j51eGHEpn5kUzRKH2Ry0qGoe8ItJg7pqnkPrYPkDQZGgGmTa0EGarHrkjLvOdU3b1fzI8otQ==", |
||||
|
"dependencies": [ |
||||
|
"@babel/core", |
||||
|
"@babel/plugin-transform-react-jsx-self", |
||||
|
"@babel/plugin-transform-react-jsx-source", |
||||
|
"@rolldown/pluginutils", |
||||
|
"@types/babel__core", |
||||
|
"react-refresh", |
||||
|
"[email protected]_@[email protected][email protected]_@[email protected]" |
||||
] |
] |
||||
}, |
}, |
||||
"@vitest/[email protected]": { |
"@vitest/[email protected]": { |
||||
@ -3345,10 +3405,22 @@ |
|||||
"@vitest/spy", |
"@vitest/spy", |
||||
"estree-walker", |
"estree-walker", |
||||
"magic-string", |
"magic-string", |
||||
"vite" |
"[email protected]_@[email protected][email protected]" |
||||
|
], |
||||
|
"optionalPeers": [ |
||||
|
"[email protected]_@[email protected][email protected]" |
||||
|
] |
||||
|
}, |
||||
|
"@vitest/[email protected][email protected]__@[email protected][email protected]_@[email protected]_@[email protected]": { |
||||
|
"integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", |
||||
|
"dependencies": [ |
||||
|
"@vitest/spy", |
||||
|
"estree-walker", |
||||
|
"magic-string", |
||||
|
"[email protected]_@[email protected][email protected]_@[email protected]" |
||||
], |
], |
||||
"optionalPeers": [ |
"optionalPeers": [ |
||||
"vite" |
"vite@7.0.0_@[email protected][email protected]_@[email protected]" |
||||
] |
] |
||||
}, |
}, |
||||
"@vitest/[email protected]": { |
"@vitest/[email protected]": { |
||||
@ -4823,6 +4895,9 @@ |
|||||
"typewise-core" |
"typewise-core" |
||||
] |
] |
||||
}, |
}, |
||||
|
"[email protected]": { |
||||
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" |
||||
|
}, |
||||
"[email protected]": { |
"[email protected]": { |
||||
"integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==" |
"integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==" |
||||
}, |
}, |
||||
@ -4891,14 +4966,25 @@ |
|||||
"debug", |
"debug", |
||||
"es-module-lexer", |
"es-module-lexer", |
||||
"pathe", |
"pathe", |
||||
"vite" |
"[email protected]_@[email protected][email protected]" |
||||
|
], |
||||
|
"bin": true |
||||
|
}, |
||||
|
"[email protected]_@[email protected]_@[email protected]": { |
||||
|
"integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", |
||||
|
"dependencies": [ |
||||
|
"cac", |
||||
|
"debug", |
||||
|
"es-module-lexer", |
||||
|
"pathe", |
||||
|
"[email protected]_@[email protected][email protected]_@[email protected]" |
||||
], |
], |
||||
"bin": true |
"bin": true |
||||
}, |
}, |
||||
"[email protected]_@[email protected][email protected]": { |
"[email protected]_@[email protected][email protected]": { |
||||
"integrity": "sha512-ixXJB1YRgDIw2OszKQS9WxGHKwLdCsbQNkpJN171udl6szi/rIySHL6/Os3s2+oE4P/FLD4dxg4mD7Wust+u5g==", |
"integrity": "sha512-ixXJB1YRgDIw2OszKQS9WxGHKwLdCsbQNkpJN171udl6szi/rIySHL6/Os3s2+oE4P/FLD4dxg4mD7Wust+u5g==", |
||||
"dependencies": [ |
"dependencies": [ |
||||
"@types/node", |
"@types/node@24.0.4", |
||||
"esbuild", |
"esbuild", |
||||
"fdir", |
"fdir", |
||||
"[email protected]", |
"[email protected]", |
||||
@ -4910,7 +4996,26 @@ |
|||||
"fsevents" |
"fsevents" |
||||
], |
], |
||||
"optionalPeers": [ |
"optionalPeers": [ |
||||
"@types/node" |
"@types/[email protected]" |
||||
|
], |
||||
|
"bin": true |
||||
|
}, |
||||
|
"[email protected]_@[email protected][email protected]_@[email protected]": { |
||||
|
"integrity": "sha512-ixXJB1YRgDIw2OszKQS9WxGHKwLdCsbQNkpJN171udl6szi/rIySHL6/Os3s2+oE4P/FLD4dxg4mD7Wust+u5g==", |
||||
|
"dependencies": [ |
||||
|
"@types/[email protected]", |
||||
|
"esbuild", |
||||
|
"fdir", |
||||
|
"[email protected]", |
||||
|
"postcss", |
||||
|
"rollup", |
||||
|
"tinyglobby" |
||||
|
], |
||||
|
"optionalDependencies": [ |
||||
|
"fsevents" |
||||
|
], |
||||
|
"optionalPeers": [ |
||||
|
"@types/[email protected]" |
||||
], |
], |
||||
"bin": true |
"bin": true |
||||
}, |
}, |
||||
@ -4918,9 +5023,9 @@ |
|||||
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", |
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", |
||||
"dependencies": [ |
"dependencies": [ |
||||
"@types/chai", |
"@types/chai", |
||||
"@types/node", |
"@types/node@24.0.4", |
||||
"@vitest/expect", |
"@vitest/expect", |
||||
"@vitest/mocker", |
"@vitest/mocker@[email protected]__@[email protected][email protected]_@[email protected]", |
||||
"@vitest/pretty-format", |
"@vitest/pretty-format", |
||||
"@vitest/runner", |
"@vitest/runner", |
||||
"@vitest/snapshot", |
"@vitest/snapshot", |
||||
@ -4939,12 +5044,47 @@ |
|||||
"tinyglobby", |
"tinyglobby", |
||||
"tinypool", |
"tinypool", |
||||
"tinyrainbow", |
"tinyrainbow", |
||||
"vite", |
"vite@7.0.0_@[email protected][email protected]", |
||||
"vite-node", |
"vite-node@3.2.4_@[email protected]", |
||||
"why-is-node-running" |
"why-is-node-running" |
||||
], |
], |
||||
"optionalPeers": [ |
"optionalPeers": [ |
||||
"@types/node", |
"@types/[email protected]", |
||||
|
"happy-dom" |
||||
|
], |
||||
|
"bin": true |
||||
|
}, |
||||
|
"[email protected]_@[email protected][email protected][email protected]__@[email protected][email protected]_@[email protected]": { |
||||
|
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", |
||||
|
"dependencies": [ |
||||
|
"@types/chai", |
||||
|
"@types/[email protected]", |
||||
|
"@vitest/expect", |
||||
|
"@vitest/[email protected][email protected]__@[email protected][email protected]_@[email protected]_@[email protected]", |
||||
|
"@vitest/pretty-format", |
||||
|
"@vitest/runner", |
||||
|
"@vitest/snapshot", |
||||
|
"@vitest/spy", |
||||
|
"@vitest/utils", |
||||
|
"chai", |
||||
|
"debug", |
||||
|
"expect-type", |
||||
|
"happy-dom", |
||||
|
"magic-string", |
||||
|
"pathe", |
||||
|
"[email protected]", |
||||
|
"std-env", |
||||
|
"tinybench", |
||||
|
"tinyexec", |
||||
|
"tinyglobby", |
||||
|
"tinypool", |
||||
|
"tinyrainbow", |
||||
|
"[email protected]_@[email protected][email protected]_@[email protected]", |
||||
|
"[email protected]_@[email protected]_@[email protected]", |
||||
|
"why-is-node-running" |
||||
|
], |
||||
|
"optionalPeers": [ |
||||
|
"@types/[email protected]", |
||||
"happy-dom" |
"happy-dom" |
||||
], |
], |
||||
"bin": true |
"bin": true |
||||
@ -5053,7 +5193,29 @@ |
|||||
} |
} |
||||
}, |
}, |
||||
"workspace": { |
"workspace": { |
||||
|
"dependencies": [ |
||||
|
"jsr:@meshtastic/protobufs@^2.7.0", |
||||
|
"npm:@bufbuild/protobuf@^2.2.3", |
||||
|
"npm:@types/node@^22.13.10", |
||||
|
"npm:ste-simple-events@^3.0.11", |
||||
|
"npm:tslog@^4.9.3" |
||||
|
], |
||||
"members": { |
"members": { |
||||
|
"packages/core": { |
||||
|
"dependencies": [ |
||||
|
"npm:crc@^4.3.2" |
||||
|
] |
||||
|
}, |
||||
|
"packages/transport-web-bluetooth": { |
||||
|
"dependencies": [ |
||||
|
"npm:@types/web-bluetooth@^0.0.20" |
||||
|
] |
||||
|
}, |
||||
|
"packages/transport-web-serial": { |
||||
|
"dependencies": [ |
||||
|
"npm:@types/w3c-web-serial@^1.0.7" |
||||
|
] |
||||
|
}, |
||||
"packages/web": { |
"packages/web": { |
||||
"dependencies": [ |
"dependencies": [ |
||||
"jsr:@std/path@^1.1.0", |
"jsr:@std/path@^1.1.0", |
||||
|
|||||
@ -0,0 +1,26 @@ |
|||||
|
# @meshtastic/core |
||||
|
|
||||
|
[](https://jsr.io/@meshtastic/core) |
||||
|
[](https://github.com/meshtastic/js/actions/workflows/ci.yml) |
||||
|
[](https://cla-assistant.io/meshtastic/meshtastic.js) |
||||
|
[](https://opencollective.com/meshtastic/) |
||||
|
[](https://vercel.com?utm_source=meshtastic&utm_campaign=oss) |
||||
|
|
||||
|
## Overview |
||||
|
|
||||
|
`@meshtastic/core` Provides core functionality for interfacing with Meshtastic |
||||
|
devices. Installation instructions are available at |
||||
|
[JSR](https://jsr.io/@meshtastic/core) |
||||
|
|
||||
|
## Usage |
||||
|
|
||||
|
```ts |
||||
|
import { MeshDevice } from "@meshtastic/core"; |
||||
|
|
||||
|
// Transport if provided by one of the available transport adapters |
||||
|
const device = new MeshDevice(transport); |
||||
|
``` |
||||
|
|
||||
|
## Stats |
||||
|
|
||||
|
 |
||||
@ -0,0 +1,10 @@ |
|||||
|
{ |
||||
|
"name": "@meshtastic/core", |
||||
|
"version": "2.6.4", |
||||
|
"exports": { |
||||
|
".": "./mod.ts" |
||||
|
}, |
||||
|
"imports": { |
||||
|
"crc": "npm:crc@^4.3.2" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,5 @@ |
|||||
|
export { Constants } from "./src/constants.ts"; |
||||
|
export { MeshDevice } from "./src/meshDevice.ts"; |
||||
|
export * as Protobuf from "@meshtastic/protobufs"; |
||||
|
export * as Types from "./src/types.ts"; |
||||
|
export * as Utils from "./src/utils/mod.ts"; |
||||
@ -0,0 +1,10 @@ |
|||||
|
/** Broadcast destination number */ |
||||
|
const broadcastNum = 0xffffffff; |
||||
|
|
||||
|
/** Minimum device firmware version supported by this version of the library. */ |
||||
|
const minFwVer = 2.2; |
||||
|
|
||||
|
export const Constants = { |
||||
|
broadcastNum, |
||||
|
minFwVer, |
||||
|
}; |
||||
File diff suppressed because it is too large
@ -0,0 +1,128 @@ |
|||||
|
import type * as Protobuf from "@meshtastic/protobufs"; |
||||
|
|
||||
|
interface Packet { |
||||
|
type: "packet"; |
||||
|
data: Uint8Array; |
||||
|
} |
||||
|
|
||||
|
interface DebugLog { |
||||
|
type: "debug"; |
||||
|
data: string; |
||||
|
} |
||||
|
|
||||
|
export type DeviceOutput = Packet | DebugLog; |
||||
|
|
||||
|
export interface Transport { |
||||
|
toDevice: WritableStream<Uint8Array>; |
||||
|
fromDevice: ReadableStream<DeviceOutput>; |
||||
|
} |
||||
|
|
||||
|
export interface QueueItem { |
||||
|
id: number; |
||||
|
data: Uint8Array; |
||||
|
sent: boolean; |
||||
|
added: Date; |
||||
|
promise: Promise<number>; |
||||
|
} |
||||
|
|
||||
|
export interface HttpRetryConfig { |
||||
|
maxRetries: number; |
||||
|
initialDelayMs: number; |
||||
|
maxDelayMs: number; |
||||
|
backoffFactor: number; |
||||
|
} |
||||
|
|
||||
|
export enum DeviceStatusEnum { |
||||
|
DeviceRestarting = 1, |
||||
|
DeviceDisconnected = 2, |
||||
|
DeviceConnecting = 3, |
||||
|
DeviceReconnecting = 4, |
||||
|
DeviceConnected = 5, |
||||
|
DeviceConfiguring = 6, |
||||
|
DeviceConfigured = 7, |
||||
|
} |
||||
|
|
||||
|
export type LogEventPacket = LogEvent & { date: Date }; |
||||
|
|
||||
|
export type PacketDestination = "broadcast" | "direct"; |
||||
|
|
||||
|
export interface PacketMetadata<T> { |
||||
|
id: number; |
||||
|
rxTime: Date; |
||||
|
type: PacketDestination; |
||||
|
from: number; |
||||
|
to: number; |
||||
|
channel: ChannelNumber; |
||||
|
data: T; |
||||
|
} |
||||
|
|
||||
|
export enum EmitterScope { |
||||
|
MeshDevice = 1, |
||||
|
SerialConnection = 2, |
||||
|
NodeSerialConnection = 3, |
||||
|
BleConnection = 4, |
||||
|
HttpConnection = 5, |
||||
|
} |
||||
|
|
||||
|
export enum Emitter { |
||||
|
Constructor = 0, |
||||
|
SendText = 1, |
||||
|
SendWaypoint = 2, |
||||
|
SendPacket = 3, |
||||
|
SendRaw = 4, |
||||
|
SetConfig = 5, |
||||
|
SetModuleConfig = 6, |
||||
|
ConfirmSetConfig = 7, |
||||
|
SetOwner = 8, |
||||
|
SetChannel = 9, |
||||
|
ConfirmSetChannel = 10, |
||||
|
ClearChannel = 11, |
||||
|
GetChannel = 12, |
||||
|
GetAllChannels = 13, |
||||
|
GetConfig = 14, |
||||
|
GetModuleConfig = 15, |
||||
|
GetOwner = 16, |
||||
|
Configure = 17, |
||||
|
HandleFromRadio = 18, |
||||
|
HandleMeshPacket = 19, |
||||
|
Connect = 20, |
||||
|
Ping = 21, |
||||
|
ReadFromRadio = 22, |
||||
|
WriteToRadio = 23, |
||||
|
SetDebugMode = 24, |
||||
|
GetMetadata = 25, |
||||
|
ResetNodes = 26, |
||||
|
Shutdown = 27, |
||||
|
Reboot = 28, |
||||
|
RebootOta = 29, |
||||
|
FactoryReset = 30, |
||||
|
EnterDfuMode = 31, |
||||
|
RemoveNodeByNum = 32, |
||||
|
SetCannedMessages = 33, |
||||
|
} |
||||
|
|
||||
|
export interface LogEvent { |
||||
|
scope: EmitterScope; |
||||
|
emitter: Emitter; |
||||
|
message: string; |
||||
|
level: Protobuf.Mesh.LogRecord_Level; |
||||
|
packet?: Uint8Array; |
||||
|
} |
||||
|
|
||||
|
export enum ChannelNumber { |
||||
|
Primary = 0, |
||||
|
Channel1 = 1, |
||||
|
Channel2 = 2, |
||||
|
Channel3 = 3, |
||||
|
Channel4 = 4, |
||||
|
Channel5 = 5, |
||||
|
Channel6 = 6, |
||||
|
Admin = 7, |
||||
|
} |
||||
|
|
||||
|
export type Destination = number | "self" | "broadcast"; |
||||
|
|
||||
|
export interface PacketError { |
||||
|
id: number; |
||||
|
error: Protobuf.Mesh.Routing_Error; |
||||
|
} |
||||
@ -0,0 +1,467 @@ |
|||||
|
import { SimpleEventDispatcher } from "ste-simple-events"; |
||||
|
import type * as Protobuf from "@meshtastic/protobufs"; |
||||
|
import type { PacketMetadata } from "../types.ts"; |
||||
|
import type * as Types from "../types.ts"; |
||||
|
|
||||
|
export class EventSystem { |
||||
|
/** |
||||
|
* Fires when a new FromRadio message has been received from the device |
||||
|
* |
||||
|
* @event onLogEvent |
||||
|
*/ |
||||
|
public readonly onLogEvent: SimpleEventDispatcher< |
||||
|
Types.LogEventPacket |
||||
|
> = new SimpleEventDispatcher< |
||||
|
Types.LogEventPacket |
||||
|
>(); |
||||
|
|
||||
|
/** |
||||
|
* Fires when a new FromRadio message has been received from the device |
||||
|
* |
||||
|
* @event onFromRadio |
||||
|
*/ |
||||
|
public readonly onFromRadio: SimpleEventDispatcher< |
||||
|
Protobuf.Mesh.FromRadio |
||||
|
> = new SimpleEventDispatcher< |
||||
|
Protobuf.Mesh.FromRadio |
||||
|
>(); |
||||
|
|
||||
|
/** |
||||
|
* Fires when a new FromRadio message containing a Data packet has been |
||||
|
* received from the device |
||||
|
* |
||||
|
* @event onMeshPacket |
||||
|
*/ |
||||
|
public readonly onMeshPacket: SimpleEventDispatcher< |
||||
|
Protobuf.Mesh.MeshPacket |
||||
|
> = new SimpleEventDispatcher< |
||||
|
Protobuf.Mesh.MeshPacket |
||||
|
>(); |
||||
|
|
||||
|
/** |
||||
|
* Fires when a new MyNodeInfo message has been received from the device |
||||
|
* |
||||
|
* @event onMyNodeInfo |
||||
|
*/ |
||||
|
public readonly onMyNodeInfo: SimpleEventDispatcher< |
||||
|
Protobuf.Mesh.MyNodeInfo |
||||
|
> = new SimpleEventDispatcher< |
||||
|
Protobuf.Mesh.MyNodeInfo |
||||
|
>(); |
||||
|
|
||||
|
/** |
||||
|
* Fires when a new MeshPacket message containing a NodeInfo packet has been |
||||
|
* received from device |
||||
|
* |
||||
|
* @event onNodeInfoPacket |
||||
|
*/ |
||||
|
public readonly onNodeInfoPacket: SimpleEventDispatcher< |
||||
|
Protobuf.Mesh.NodeInfo |
||||
|
> = new SimpleEventDispatcher< |
||||
|
Protobuf.Mesh.NodeInfo |
||||
|
>(); |
||||
|
|
||||
|
/** |
||||
|
* Fires when a new Channel message is received |
||||
|
* |
||||
|
* @event onChannelPacket |
||||
|
*/ |
||||
|
public readonly onChannelPacket: SimpleEventDispatcher< |
||||
|
Protobuf.Channel.Channel |
||||
|
> = new SimpleEventDispatcher< |
||||
|
Protobuf.Channel.Channel |
||||
|
>(); |
||||
|
|
||||
|
/** |
||||
|
* Fires when a new Config message is received |
||||
|
* |
||||
|
* @event onConfigPacket |
||||
|
*/ |
||||
|
public readonly onConfigPacket: SimpleEventDispatcher< |
||||
|
Protobuf.Config.Config |
||||
|
> = new SimpleEventDispatcher< |
||||
|
Protobuf.Config.Config |
||||
|
>(); |
||||
|
|
||||
|
/** |
||||
|
* Fires when a new ModuleConfig message is received |
||||
|
* |
||||
|
* @event onModuleConfigPacket |
||||
|
*/ |
||||
|
public readonly onModuleConfigPacket: SimpleEventDispatcher< |
||||
|
Protobuf.ModuleConfig.ModuleConfig |
||||
|
> = new SimpleEventDispatcher< |
||||
|
Protobuf.ModuleConfig.ModuleConfig |
||||
|
>(); |
||||
|
|
||||
|
/** |
||||
|
* Fires when a new MeshPacket message containing a ATAK packet has been |
||||
|
* received from device |
||||
|
* |
||||
|
* @event onAtakPacket |
||||
|
*/ |
||||
|
public readonly onAtakPacket: SimpleEventDispatcher< |
||||
|
PacketMetadata<Uint8Array> |
||||
|
> = new SimpleEventDispatcher< |
||||
|
PacketMetadata<Uint8Array> |
||||
|
>(); |
||||
|
|
||||
|
/** |
||||
|
* Fires when a new MeshPacket message containing a Text packet has been |
||||
|
* received from device |
||||
|
* |
||||
|
* @event onMessagePacket |
||||
|
*/ |
||||
|
public readonly onMessagePacket: SimpleEventDispatcher< |
||||
|
PacketMetadata<string> |
||||
|
> = new SimpleEventDispatcher< |
||||
|
PacketMetadata<string> |
||||
|
>(); |
||||
|
|
||||
|
/** |
||||
|
* Fires when a new MeshPacket message containing a Remote Hardware packet has |
||||
|
* been received from device |
||||
|
* |
||||
|
* @event onRemoteHardwarePacket |
||||
|
*/ |
||||
|
public readonly onRemoteHardwarePacket: SimpleEventDispatcher< |
||||
|
PacketMetadata<Protobuf.RemoteHardware.HardwareMessage> |
||||
|
> = new SimpleEventDispatcher< |
||||
|
PacketMetadata<Protobuf.RemoteHardware.HardwareMessage> |
||||
|
>(); |
||||
|
|
||||
|
/** |
||||
|
* Fires when a new MeshPacket message containing a Position packet has been |
||||
|
* received from device |
||||
|
* |
||||
|
* @event onPositionPacket |
||||
|
*/ |
||||
|
public readonly onPositionPacket: SimpleEventDispatcher< |
||||
|
PacketMetadata<Protobuf.Mesh.Position> |
||||
|
> = new SimpleEventDispatcher< |
||||
|
PacketMetadata<Protobuf.Mesh.Position> |
||||
|
>(); |
||||
|
|
||||
|
/** |
||||
|
* Fires when a new MeshPacket message containing a User packet has been |
||||
|
* received from device |
||||
|
* |
||||
|
* @event onUserPacket |
||||
|
*/ |
||||
|
public readonly onUserPacket: SimpleEventDispatcher< |
||||
|
PacketMetadata<Protobuf.Mesh.User> |
||||
|
> = new SimpleEventDispatcher< |
||||
|
PacketMetadata<Protobuf.Mesh.User> |
||||
|
>(); |
||||
|
|
||||
|
/** |
||||
|
* Fires when a new MeshPacket message containing a Routing packet has been |
||||
|
* received from device |
||||
|
* |
||||
|
* @event onRoutingPacket |
||||
|
*/ |
||||
|
public readonly onRoutingPacket: SimpleEventDispatcher< |
||||
|
PacketMetadata<Protobuf.Mesh.Routing> |
||||
|
> = new SimpleEventDispatcher< |
||||
|
PacketMetadata<Protobuf.Mesh.Routing> |
||||
|
>(); |
||||
|
|
||||
|
/** |
||||
|
* Fires when the device receives a Metadata packet |
||||
|
* |
||||
|
* @event onDeviceMetadataPacket |
||||
|
*/ |
||||
|
public readonly onDeviceMetadataPacket: SimpleEventDispatcher< |
||||
|
PacketMetadata<Protobuf.Mesh.DeviceMetadata> |
||||
|
> = new SimpleEventDispatcher< |
||||
|
PacketMetadata<Protobuf.Mesh.DeviceMetadata> |
||||
|
>(); |
||||
|
|
||||
|
/** |
||||
|
* Fires when the device receives a Canned Message Module message packet |
||||
|
* |
||||
|
* @event onCannedMessageModulePacket |
||||
|
*/ |
||||
|
public readonly onCannedMessageModulePacket: SimpleEventDispatcher< |
||||
|
PacketMetadata<string> |
||||
|
> = new SimpleEventDispatcher< |
||||
|
PacketMetadata<string> |
||||
|
>(); |
||||
|
|
||||
|
/** |
||||
|
* Fires when a new MeshPacket message containing a Waypoint packet has been |
||||
|
* received from device |
||||
|
* |
||||
|
* @event onWaypointPacket |
||||
|
*/ |
||||
|
public readonly onWaypointPacket: SimpleEventDispatcher< |
||||
|
PacketMetadata<Protobuf.Mesh.Waypoint> |
||||
|
> = new SimpleEventDispatcher< |
||||
|
PacketMetadata<Protobuf.Mesh.Waypoint> |
||||
|
>(); |
||||
|
|
||||
|
/** |
||||
|
* Fires when a new MeshPacket message containing an Audio packet has been |
||||
|
* received from device |
||||
|
* |
||||
|
* @event onAudioPacket |
||||
|
*/ |
||||
|
public readonly onAudioPacket: SimpleEventDispatcher< |
||||
|
PacketMetadata<Uint8Array> |
||||
|
> = new SimpleEventDispatcher< |
||||
|
PacketMetadata<Uint8Array> |
||||
|
>(); |
||||
|
|
||||
|
/** |
||||
|
* Fires when a new MeshPacket message containing a Detection Sensor packet has been |
||||
|
* received from device |
||||
|
* |
||||
|
* @event onDetectionSensorPacket |
||||
|
*/ |
||||
|
public readonly onDetectionSensorPacket: SimpleEventDispatcher< |
||||
|
PacketMetadata<Uint8Array> |
||||
|
> = new SimpleEventDispatcher< |
||||
|
PacketMetadata<Uint8Array> |
||||
|
>(); |
||||
|
|
||||
|
/** |
||||
|
* Fires when a new MeshPacket message containing a Ping packet has been |
||||
|
* received from device |
||||
|
* |
||||
|
* @event onPingPacket |
||||
|
*/ |
||||
|
public readonly onPingPacket: SimpleEventDispatcher< |
||||
|
PacketMetadata<Uint8Array> |
||||
|
> = new SimpleEventDispatcher< |
||||
|
PacketMetadata<Uint8Array> |
||||
|
>(); |
||||
|
|
||||
|
/** |
||||
|
* Fires when a new MeshPacket message containing a IP Tunnel packet has been |
||||
|
* received from device |
||||
|
* |
||||
|
* @event onIpTunnelPacket |
||||
|
*/ |
||||
|
public readonly onIpTunnelPacket: SimpleEventDispatcher< |
||||
|
PacketMetadata<Uint8Array> |
||||
|
> = new SimpleEventDispatcher< |
||||
|
PacketMetadata<Uint8Array> |
||||
|
>(); |
||||
|
|
||||
|
/** |
||||
|
* Fires when a new MeshPacket message containing a Paxcounter packet has been |
||||
|
* received from device |
||||
|
* |
||||
|
* @event onPaxcounterPacket |
||||
|
*/ |
||||
|
public readonly onPaxcounterPacket: SimpleEventDispatcher< |
||||
|
PacketMetadata<Protobuf.PaxCount.Paxcount> |
||||
|
> = new SimpleEventDispatcher< |
||||
|
PacketMetadata<Protobuf.PaxCount.Paxcount> |
||||
|
>(); |
||||
|
|
||||
|
/** |
||||
|
* Fires when a new MeshPacket message containing a Serial packet has been |
||||
|
* received from device |
||||
|
* |
||||
|
* @event onSerialPacket |
||||
|
*/ |
||||
|
public readonly onSerialPacket: SimpleEventDispatcher< |
||||
|
PacketMetadata<Uint8Array> |
||||
|
> = new SimpleEventDispatcher< |
||||
|
PacketMetadata<Uint8Array> |
||||
|
>(); |
||||
|
|
||||
|
/** |
||||
|
* Fires when a new MeshPacket message containing a Store and Forward packet |
||||
|
* has been received from device |
||||
|
* |
||||
|
* @event onStoreForwardPacket |
||||
|
*/ |
||||
|
public readonly onStoreForwardPacket: SimpleEventDispatcher< |
||||
|
PacketMetadata<Uint8Array> |
||||
|
> = new SimpleEventDispatcher< |
||||
|
PacketMetadata<Uint8Array> |
||||
|
>(); |
||||
|
|
||||
|
/** |
||||
|
* Fires when a new MeshPacket message containing a Store and Forward packet |
||||
|
* has been received from device |
||||
|
* |
||||
|
* @event onRangeTestPacket |
||||
|
*/ |
||||
|
public readonly onRangeTestPacket: SimpleEventDispatcher< |
||||
|
PacketMetadata<Uint8Array> |
||||
|
> = new SimpleEventDispatcher< |
||||
|
PacketMetadata<Uint8Array> |
||||
|
>(); |
||||
|
|
||||
|
/** |
||||
|
* Fires when a new MeshPacket message containing a Telemetry packet has been |
||||
|
* received from device |
||||
|
* |
||||
|
* @event onTelemetryPacket |
||||
|
*/ |
||||
|
public readonly onTelemetryPacket: SimpleEventDispatcher< |
||||
|
PacketMetadata<Protobuf.Telemetry.Telemetry> |
||||
|
> = new SimpleEventDispatcher< |
||||
|
PacketMetadata<Protobuf.Telemetry.Telemetry> |
||||
|
>(); |
||||
|
|
||||
|
/** |
||||
|
* Fires when a new MeshPacket message containing a ZPS packet has been |
||||
|
* received from device |
||||
|
* |
||||
|
* @event onZPSPacket |
||||
|
*/ |
||||
|
public readonly onZpsPacket: SimpleEventDispatcher< |
||||
|
PacketMetadata<Uint8Array> |
||||
|
> = new SimpleEventDispatcher< |
||||
|
PacketMetadata<Uint8Array> |
||||
|
>(); |
||||
|
|
||||
|
/** |
||||
|
* Fires when a new MeshPacket message containing a Simulator packet has been |
||||
|
* received from device |
||||
|
* |
||||
|
* @event onSimulatorPacket |
||||
|
*/ |
||||
|
public readonly onSimulatorPacket: SimpleEventDispatcher< |
||||
|
PacketMetadata<Uint8Array> |
||||
|
> = new SimpleEventDispatcher< |
||||
|
PacketMetadata<Uint8Array> |
||||
|
>(); |
||||
|
|
||||
|
/** |
||||
|
* Fires when a new MeshPacket message containing a Trace Route packet has been |
||||
|
* received from device |
||||
|
* |
||||
|
* @event onTraceRoutePacket |
||||
|
*/ |
||||
|
public readonly onTraceRoutePacket: SimpleEventDispatcher< |
||||
|
PacketMetadata<Protobuf.Mesh.RouteDiscovery> |
||||
|
> = new SimpleEventDispatcher< |
||||
|
PacketMetadata<Protobuf.Mesh.RouteDiscovery> |
||||
|
>(); |
||||
|
|
||||
|
/** |
||||
|
* Fires when a new MeshPacket message containing a Neighbor Info packet has been |
||||
|
* received from device |
||||
|
* |
||||
|
* @event onNeighborInfoPacket |
||||
|
*/ |
||||
|
public readonly onNeighborInfoPacket: SimpleEventDispatcher< |
||||
|
PacketMetadata<Protobuf.Mesh.NeighborInfo> |
||||
|
> = new SimpleEventDispatcher< |
||||
|
PacketMetadata<Protobuf.Mesh.NeighborInfo> |
||||
|
>(); |
||||
|
|
||||
|
/** |
||||
|
* Fires when a new MeshPacket message containing an ATAK packet has been |
||||
|
* received from device |
||||
|
* |
||||
|
* @event onAtakPluginPacket |
||||
|
*/ |
||||
|
public readonly onAtakPluginPacket: SimpleEventDispatcher< |
||||
|
PacketMetadata<Uint8Array> |
||||
|
> = new SimpleEventDispatcher< |
||||
|
PacketMetadata<Uint8Array> |
||||
|
>(); |
||||
|
|
||||
|
/** |
||||
|
* Fires when a new MeshPacket message containing a Map Report packet has been |
||||
|
* received from device |
||||
|
* |
||||
|
* @event onMapReportPacket |
||||
|
*/ |
||||
|
public readonly onMapReportPacket: SimpleEventDispatcher< |
||||
|
PacketMetadata<Uint8Array> |
||||
|
> = new SimpleEventDispatcher< |
||||
|
PacketMetadata<Uint8Array> |
||||
|
>(); |
||||
|
|
||||
|
/** |
||||
|
* Fires when a new MeshPacket message containing a Private packet has been |
||||
|
* received from device |
||||
|
* |
||||
|
* @event onPrivatePacket |
||||
|
*/ |
||||
|
public readonly onPrivatePacket: SimpleEventDispatcher< |
||||
|
PacketMetadata<Uint8Array> |
||||
|
> = new SimpleEventDispatcher< |
||||
|
PacketMetadata<Uint8Array> |
||||
|
>(); |
||||
|
|
||||
|
/** |
||||
|
* Fires when a new MeshPacket message containing an ATAK Forwarder packet has been |
||||
|
* received from device |
||||
|
* |
||||
|
* @event onAtakForwarderPacket |
||||
|
*/ |
||||
|
public readonly onAtakForwarderPacket: SimpleEventDispatcher< |
||||
|
PacketMetadata<Uint8Array> |
||||
|
> = new SimpleEventDispatcher< |
||||
|
PacketMetadata<Uint8Array> |
||||
|
>(); |
||||
|
|
||||
|
/** |
||||
|
* Fires when the devices connection or configuration status changes |
||||
|
* |
||||
|
* @event onDeviceStatus |
||||
|
*/ |
||||
|
public readonly onDeviceStatus: SimpleEventDispatcher< |
||||
|
Types.DeviceStatusEnum |
||||
|
> = new SimpleEventDispatcher< |
||||
|
Types.DeviceStatusEnum |
||||
|
>(); |
||||
|
|
||||
|
/** |
||||
|
* Fires when a new FromRadio message containing a LogRecord packet has been |
||||
|
* received from device |
||||
|
* |
||||
|
* @event onLogRecord |
||||
|
*/ |
||||
|
public readonly onLogRecord: SimpleEventDispatcher< |
||||
|
Protobuf.Mesh.LogRecord |
||||
|
> = new SimpleEventDispatcher< |
||||
|
Protobuf.Mesh.LogRecord |
||||
|
>(); |
||||
|
|
||||
|
/** |
||||
|
* Fires when the device receives a meshPacket, returns a timestamp |
||||
|
* |
||||
|
* @event onMeshHeartbeat |
||||
|
*/ |
||||
|
public readonly onMeshHeartbeat: SimpleEventDispatcher<Date> = |
||||
|
new SimpleEventDispatcher<Date>(); |
||||
|
|
||||
|
/** |
||||
|
* Outputs any debug log data (currently serial connections only) |
||||
|
* |
||||
|
* @event onDeviceDebugLog |
||||
|
*/ |
||||
|
public readonly onDeviceDebugLog: SimpleEventDispatcher<Uint8Array> = |
||||
|
new SimpleEventDispatcher<Uint8Array>(); |
||||
|
|
||||
|
/** |
||||
|
* Outputs status of pending settings changes |
||||
|
* |
||||
|
* @event onpendingSettingsChange |
||||
|
*/ |
||||
|
public readonly onPendingSettingsChange: SimpleEventDispatcher< |
||||
|
boolean |
||||
|
> = new SimpleEventDispatcher< |
||||
|
boolean |
||||
|
>(); |
||||
|
|
||||
|
/** |
||||
|
* Fires when a QueueStatus message is generated |
||||
|
* |
||||
|
* @event onQueueStatus |
||||
|
*/ |
||||
|
public readonly onQueueStatus: SimpleEventDispatcher< |
||||
|
Protobuf.Mesh.QueueStatus |
||||
|
> = new SimpleEventDispatcher< |
||||
|
Protobuf.Mesh.QueueStatus |
||||
|
>(); |
||||
|
} |
||||
@ -0,0 +1,5 @@ |
|||||
|
export { EventSystem } from "./eventSystem.ts"; |
||||
|
export { Queue } from "./queue.ts"; |
||||
|
export { Xmodem } from "./xmodem.ts"; |
||||
|
export { toDeviceStream } from "./transform/toDevice.ts"; |
||||
|
export { fromDeviceStream } from "./transform/fromDevice.ts"; |
||||
@ -0,0 +1,119 @@ |
|||||
|
import { SimpleEventDispatcher } from "ste-simple-events"; |
||||
|
import { fromBinary } from "@bufbuild/protobuf"; |
||||
|
import * as Protobuf from "@meshtastic/protobufs"; |
||||
|
import type { PacketError, QueueItem } from "../types.ts"; |
||||
|
|
||||
|
export class Queue { |
||||
|
private queue: QueueItem[] = []; |
||||
|
private lock = false; |
||||
|
private ackNotifier = new SimpleEventDispatcher<number>(); |
||||
|
private errorNotifier = new SimpleEventDispatcher<PacketError>(); |
||||
|
private timeout: number; |
||||
|
|
||||
|
constructor() { |
||||
|
this.timeout = 60000; |
||||
|
} |
||||
|
|
||||
|
public getState(): QueueItem[] { |
||||
|
return this.queue; |
||||
|
} |
||||
|
|
||||
|
public clear(): void { |
||||
|
this.queue = []; |
||||
|
} |
||||
|
|
||||
|
public push(item: Omit<QueueItem, "promise" | "sent" | "added">): void { |
||||
|
const queueItem: QueueItem = { |
||||
|
...item, |
||||
|
sent: false, |
||||
|
added: new Date(), |
||||
|
promise: new Promise<number>((resolve, reject) => { |
||||
|
this.ackNotifier.subscribe((id) => { |
||||
|
if (item.id === id) { |
||||
|
this.remove(item.id); |
||||
|
resolve(id); |
||||
|
} |
||||
|
}); |
||||
|
this.errorNotifier.subscribe((e) => { |
||||
|
if (item.id === e.id) { |
||||
|
this.remove(item.id); |
||||
|
reject(e); |
||||
|
} |
||||
|
}); |
||||
|
setTimeout(() => { |
||||
|
if (this.queue.findIndex((qi) => qi.id === item.id) !== -1) { |
||||
|
this.remove(item.id); |
||||
|
const decoded = fromBinary(Protobuf.Mesh.ToRadioSchema, item.data); |
||||
|
console.warn( |
||||
|
`Packet ${item.id} of type ${decoded.payloadVariant.case} timed out`, |
||||
|
); |
||||
|
|
||||
|
reject({ |
||||
|
id: item.id, |
||||
|
error: Protobuf.Mesh.Routing_Error.TIMEOUT, |
||||
|
}); |
||||
|
} |
||||
|
}, this.timeout); |
||||
|
}), |
||||
|
}; |
||||
|
this.queue.push(queueItem); |
||||
|
} |
||||
|
|
||||
|
public remove(id: number): void { |
||||
|
if (this.lock) { |
||||
|
setTimeout(() => this.remove(id), 100); |
||||
|
return; |
||||
|
} |
||||
|
this.queue = this.queue.filter((item) => item.id !== id); |
||||
|
} |
||||
|
|
||||
|
public processAck(id: number): void { |
||||
|
this.ackNotifier.dispatch(id); |
||||
|
} |
||||
|
|
||||
|
public processError(e: PacketError): void { |
||||
|
console.error( |
||||
|
`Error received for packet ${e.id}: ${ |
||||
|
Protobuf.Mesh.Routing_Error[e.error] |
||||
|
}`,
|
||||
|
); |
||||
|
this.errorNotifier.dispatch(e); |
||||
|
} |
||||
|
|
||||
|
public wait(id: number): Promise<number> { |
||||
|
const queueItem = this.queue.find((qi) => qi.id === id); |
||||
|
if (!queueItem) { |
||||
|
throw new Error("Packet does not exist"); |
||||
|
} |
||||
|
return queueItem.promise; |
||||
|
} |
||||
|
|
||||
|
public async processQueue( |
||||
|
outputStream: WritableStream<Uint8Array>, |
||||
|
): Promise<void> { |
||||
|
if (this.lock) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
this.lock = true; |
||||
|
const writer = outputStream.getWriter(); |
||||
|
|
||||
|
try { |
||||
|
while (this.queue.filter((p) => !p.sent).length > 0) { |
||||
|
const item = this.queue.filter((p) => !p.sent)[0]; |
||||
|
if (item) { |
||||
|
await new Promise((resolve) => setTimeout(resolve, 200)); |
||||
|
try { |
||||
|
await writer.write(item.data); |
||||
|
item.sent = true; |
||||
|
} catch (error) { |
||||
|
console.error(`Error sending packet ${item.id}`, error); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} finally { |
||||
|
writer.releaseLock(); |
||||
|
this.lock = false; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,222 @@ |
|||||
|
import { fromBinary } from "@bufbuild/protobuf"; |
||||
|
import type { DeviceOutput } from "../../types.ts"; |
||||
|
import { Constants, Protobuf, Types } from "../../../mod.ts"; |
||||
|
import type { MeshDevice } from "../../../mod.ts"; |
||||
|
|
||||
|
export const decodePacket = (device: MeshDevice) => |
||||
|
new WritableStream<DeviceOutput>({ |
||||
|
write(chunk) { |
||||
|
switch (chunk.type) { |
||||
|
case "debug": { |
||||
|
break; |
||||
|
} |
||||
|
case "packet": { |
||||
|
const decodedMessage = fromBinary( |
||||
|
Protobuf.Mesh.FromRadioSchema, |
||||
|
chunk.data, |
||||
|
); |
||||
|
device.events.onFromRadio.dispatch(decodedMessage); |
||||
|
|
||||
|
/** @todo Add map here when `all=true` gets fixed. */ |
||||
|
switch (decodedMessage.payloadVariant.case) { |
||||
|
case "packet": { |
||||
|
device.handleMeshPacket(decodedMessage.payloadVariant.value); |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
case "myInfo": { |
||||
|
device.events.onMyNodeInfo.dispatch( |
||||
|
decodedMessage.payloadVariant.value, |
||||
|
); |
||||
|
device.log.info( |
||||
|
Types.Emitter[Types.Emitter.HandleFromRadio], |
||||
|
"📱 Received Node info for this device", |
||||
|
); |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
case "nodeInfo": { |
||||
|
device.log.info( |
||||
|
Types.Emitter[Types.Emitter.HandleFromRadio], |
||||
|
`📱 Received Node Info packet for node: ${decodedMessage.payloadVariant.value.num}`, |
||||
|
); |
||||
|
|
||||
|
device.events.onNodeInfoPacket.dispatch( |
||||
|
decodedMessage.payloadVariant.value, |
||||
|
); |
||||
|
|
||||
|
//TODO: HERE
|
||||
|
if (decodedMessage.payloadVariant.value.position) { |
||||
|
device.events.onPositionPacket.dispatch({ |
||||
|
id: decodedMessage.id, |
||||
|
rxTime: new Date(), |
||||
|
from: decodedMessage.payloadVariant.value.num, |
||||
|
to: decodedMessage.payloadVariant.value.num, |
||||
|
type: "direct", |
||||
|
channel: Types.ChannelNumber.Primary, |
||||
|
data: decodedMessage.payloadVariant.value.position, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
//TODO: HERE
|
||||
|
if (decodedMessage.payloadVariant.value.user) { |
||||
|
device.events.onUserPacket.dispatch({ |
||||
|
id: decodedMessage.id, |
||||
|
rxTime: new Date(), |
||||
|
from: decodedMessage.payloadVariant.value.num, |
||||
|
to: decodedMessage.payloadVariant.value.num, |
||||
|
type: "direct", |
||||
|
channel: Types.ChannelNumber.Primary, |
||||
|
data: decodedMessage.payloadVariant.value.user, |
||||
|
}); |
||||
|
} |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
case "config": { |
||||
|
if (decodedMessage.payloadVariant.value.payloadVariant.case) { |
||||
|
device.log.trace( |
||||
|
Types.Emitter[Types.Emitter.HandleFromRadio], |
||||
|
`💾 Received Config packet of variant: ${decodedMessage.payloadVariant.value.payloadVariant.case}`, |
||||
|
); |
||||
|
} else { |
||||
|
device.log.warn( |
||||
|
Types.Emitter[Types.Emitter.HandleFromRadio], |
||||
|
`⚠️ Received Config packet of variant: ${"UNK"}`, |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
device.events.onConfigPacket.dispatch( |
||||
|
decodedMessage.payloadVariant.value, |
||||
|
); |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
case "logRecord": { |
||||
|
device.log.trace( |
||||
|
Types.Emitter[Types.Emitter.HandleFromRadio], |
||||
|
"Received onLogRecord", |
||||
|
); |
||||
|
device.events.onLogRecord.dispatch( |
||||
|
decodedMessage.payloadVariant.value, |
||||
|
); |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
case "configCompleteId": { |
||||
|
if (decodedMessage.payloadVariant.value !== device.configId) { |
||||
|
device.log.error( |
||||
|
Types.Emitter[Types.Emitter.HandleFromRadio], |
||||
|
`❌ Invalid config id received from device, expected ${device.configId} but received ${decodedMessage.payloadVariant.value}`, |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
device.log.info( |
||||
|
Types.Emitter[Types.Emitter.HandleFromRadio], |
||||
|
`⚙️ Valid config id received from device: ${device.configId}`, |
||||
|
); |
||||
|
|
||||
|
device.updateDeviceStatus( |
||||
|
Types.DeviceStatusEnum.DeviceConfigured, |
||||
|
); |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
case "rebooted": { |
||||
|
device.configure().catch(() => { |
||||
|
// TODO: FIX, workaround for `wantConfigId` not getting acks.
|
||||
|
}); |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
case "moduleConfig": { |
||||
|
if (decodedMessage.payloadVariant.value.payloadVariant.case) { |
||||
|
device.log.trace( |
||||
|
Types.Emitter[Types.Emitter.HandleFromRadio], |
||||
|
`💾 Received Module Config packet of variant: ${decodedMessage.payloadVariant.value.payloadVariant.case}`, |
||||
|
); |
||||
|
} else { |
||||
|
device.log.warn( |
||||
|
Types.Emitter[Types.Emitter.HandleFromRadio], |
||||
|
"⚠️ Received Module Config packet of variant: UNK", |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
device.events.onModuleConfigPacket.dispatch( |
||||
|
decodedMessage.payloadVariant.value, |
||||
|
); |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
case "channel": { |
||||
|
device.log.trace( |
||||
|
Types.Emitter[Types.Emitter.HandleFromRadio], |
||||
|
`🔐 Received Channel: ${decodedMessage.payloadVariant.value.index}`, |
||||
|
); |
||||
|
|
||||
|
device.events.onChannelPacket.dispatch( |
||||
|
decodedMessage.payloadVariant.value, |
||||
|
); |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
case "queueStatus": { |
||||
|
device.log.trace( |
||||
|
Types.Emitter[Types.Emitter.HandleFromRadio], |
||||
|
`🚧 Received Queue Status: ${decodedMessage.payloadVariant.value}`, |
||||
|
); |
||||
|
|
||||
|
device.events.onQueueStatus.dispatch( |
||||
|
decodedMessage.payloadVariant.value, |
||||
|
); |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
case "xmodemPacket": { |
||||
|
device.xModem.handlePacket(decodedMessage.payloadVariant.value); |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
case "metadata": { |
||||
|
if ( |
||||
|
Number.parseFloat( |
||||
|
decodedMessage.payloadVariant.value.firmwareVersion, |
||||
|
) < Constants.minFwVer |
||||
|
) { |
||||
|
device.log.fatal( |
||||
|
Types.Emitter[Types.Emitter.HandleFromRadio], |
||||
|
`Device firmware outdated. Min supported: ${Constants.minFwVer} got : ${decodedMessage.payloadVariant.value.firmwareVersion}`, |
||||
|
); |
||||
|
} |
||||
|
device.log.debug( |
||||
|
Types.Emitter[Types.Emitter.GetMetadata], |
||||
|
"🏷️ Received metadata packet", |
||||
|
); |
||||
|
|
||||
|
device.events.onDeviceMetadataPacket.dispatch({ |
||||
|
id: decodedMessage.id, |
||||
|
rxTime: new Date(), |
||||
|
from: 0, |
||||
|
to: 0, |
||||
|
type: "direct", |
||||
|
channel: Types.ChannelNumber.Primary, |
||||
|
data: decodedMessage.payloadVariant.value, |
||||
|
}); |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
case "mqttClientProxyMessage": { |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
default: { |
||||
|
device.log.warn( |
||||
|
Types.Emitter[Types.Emitter.HandleFromRadio], |
||||
|
`⚠️ Unhandled payload variant: ${decodedMessage.payloadVariant.case}`, |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
}); |
||||
@ -0,0 +1,73 @@ |
|||||
|
import type { DeviceOutput } from "../../types.ts"; |
||||
|
|
||||
|
export const fromDeviceStream: () => TransformStream<Uint8Array, DeviceOutput> = |
||||
|
( |
||||
|
// onReleaseEvent: SimpleEventDispatcher<boolean>,
|
||||
|
) => { |
||||
|
let byteBuffer = new Uint8Array([]); |
||||
|
const textDecoder = new TextDecoder(); |
||||
|
return new TransformStream<Uint8Array, DeviceOutput>({ |
||||
|
transform(chunk: Uint8Array, controller): void { |
||||
|
// onReleaseEvent.subscribe(() => {
|
||||
|
// controller.terminate();
|
||||
|
// });
|
||||
|
byteBuffer = new Uint8Array([...byteBuffer, ...chunk]); |
||||
|
let processingExhausted = false; |
||||
|
while (byteBuffer.length !== 0 && !processingExhausted) { |
||||
|
const framingIndex = byteBuffer.findIndex((byte) => byte === 0x94); |
||||
|
const framingByte2 = byteBuffer[framingIndex + 1]; |
||||
|
if (framingByte2 === 0xc3) { |
||||
|
if (byteBuffer.subarray(0, framingIndex).length) { |
||||
|
controller.enqueue({ |
||||
|
type: "debug", |
||||
|
data: textDecoder.decode(byteBuffer.subarray(0, framingIndex)), |
||||
|
}); |
||||
|
byteBuffer = byteBuffer.subarray(framingIndex); |
||||
|
} |
||||
|
|
||||
|
const msb = byteBuffer[2]; |
||||
|
const lsb = byteBuffer[3]; |
||||
|
|
||||
|
if ( |
||||
|
msb !== undefined && |
||||
|
lsb !== undefined && |
||||
|
byteBuffer.length >= 4 + (msb << 8) + lsb |
||||
|
) { |
||||
|
const packet = byteBuffer.subarray(4, 4 + (msb << 8) + lsb); |
||||
|
|
||||
|
const malformedDetectorIndex = packet.findIndex( |
||||
|
(byte) => byte === 0x94, |
||||
|
); |
||||
|
if ( |
||||
|
malformedDetectorIndex !== -1 && |
||||
|
packet[malformedDetectorIndex + 1] === 0xc3 |
||||
|
) { |
||||
|
console.warn( |
||||
|
`⚠️ Malformed packet found, discarding: ${ |
||||
|
byteBuffer |
||||
|
.subarray(0, malformedDetectorIndex - 1) |
||||
|
.toString() |
||||
|
}`,
|
||||
|
); |
||||
|
|
||||
|
byteBuffer = byteBuffer.subarray(malformedDetectorIndex); |
||||
|
} else { |
||||
|
byteBuffer = byteBuffer.subarray(3 + (msb << 8) + lsb + 1); |
||||
|
|
||||
|
controller.enqueue({ |
||||
|
type: "packet", |
||||
|
data: packet, |
||||
|
}); |
||||
|
} |
||||
|
} else { |
||||
|
/** Only partioal message in buffer, wait for the rest */ |
||||
|
processingExhausted = true; |
||||
|
} |
||||
|
} else { |
||||
|
/** Message not complete, only 1 byte in buffer */ |
||||
|
processingExhausted = true; |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
}); |
||||
|
}; |
||||
@ -0,0 +1,16 @@ |
|||||
|
/** |
||||
|
* Pads packets with appropriate framing information before writing to the output stream. |
||||
|
*/ |
||||
|
export const toDeviceStream: TransformStream<Uint8Array, Uint8Array> = |
||||
|
new TransformStream<Uint8Array, Uint8Array>({ |
||||
|
transform(chunk: Uint8Array, controller): void { |
||||
|
const bufLen = chunk.length; |
||||
|
const header = new Uint8Array([ |
||||
|
0x94, |
||||
|
0xC3, |
||||
|
(bufLen >> 8) & 0xFF, |
||||
|
bufLen & 0xFF, |
||||
|
]); |
||||
|
controller.enqueue(new Uint8Array([...header, ...chunk])); |
||||
|
}, |
||||
|
}); |
||||
@ -0,0 +1,135 @@ |
|||||
|
import crc16ccitt from "crc/calculators/crc16ccitt"; |
||||
|
import { create, toBinary } from "@bufbuild/protobuf"; |
||||
|
import * as Protobuf from "@meshtastic/protobufs"; |
||||
|
|
||||
|
//if counter > 35 then reset counter/clear/error/reject promise
|
||||
|
type XmodemProps = (toRadio: Uint8Array, id?: number) => Promise<number>; |
||||
|
|
||||
|
export class Xmodem { |
||||
|
private sendRaw: XmodemProps; |
||||
|
private rxBuffer: Uint8Array[]; |
||||
|
private txBuffer: Uint8Array[]; |
||||
|
private textEncoder: TextEncoder; |
||||
|
private counter: number; |
||||
|
|
||||
|
constructor(sendRaw: XmodemProps) { |
||||
|
this.sendRaw = sendRaw; |
||||
|
this.rxBuffer = []; |
||||
|
this.txBuffer = []; |
||||
|
this.textEncoder = new TextEncoder(); |
||||
|
this.counter = 0; |
||||
|
} |
||||
|
|
||||
|
async downloadFile(filename: string): Promise<number> { |
||||
|
return await this.sendCommand( |
||||
|
Protobuf.Xmodem.XModem_Control.STX, |
||||
|
this.textEncoder.encode(filename), |
||||
|
0, |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
async uploadFile(filename: string, data: Uint8Array): Promise<number> { |
||||
|
for (let i = 0; i < data.length; i += 128) { |
||||
|
this.txBuffer.push(data.slice(i, i + 128)); |
||||
|
} |
||||
|
|
||||
|
return await this.sendCommand( |
||||
|
Protobuf.Xmodem.XModem_Control.SOH, |
||||
|
this.textEncoder.encode(filename), |
||||
|
0, |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
async sendCommand( |
||||
|
command: Protobuf.Xmodem.XModem_Control, |
||||
|
buffer?: Uint8Array, |
||||
|
sequence?: number, |
||||
|
crc16?: number, |
||||
|
): Promise<number> { |
||||
|
const toRadio = create(Protobuf.Mesh.ToRadioSchema, { |
||||
|
payloadVariant: { |
||||
|
case: "xmodemPacket", |
||||
|
value: { |
||||
|
buffer, |
||||
|
control: command, |
||||
|
seq: sequence, |
||||
|
crc16: crc16, |
||||
|
}, |
||||
|
}, |
||||
|
}); |
||||
|
return await this.sendRaw(toBinary(Protobuf.Mesh.ToRadioSchema, toRadio)); |
||||
|
} |
||||
|
|
||||
|
async handlePacket(packet: Protobuf.Xmodem.XModem): Promise<number> { |
||||
|
await new Promise((resolve) => setTimeout(resolve, 100)); |
||||
|
|
||||
|
switch (packet.control) { |
||||
|
case Protobuf.Xmodem.XModem_Control.NUL: { |
||||
|
// nothing
|
||||
|
break; |
||||
|
} |
||||
|
case Protobuf.Xmodem.XModem_Control.SOH: { |
||||
|
this.counter = packet.seq; |
||||
|
if (this.validateCrc16(packet)) { |
||||
|
this.rxBuffer[this.counter] = packet.buffer; |
||||
|
return this.sendCommand(Protobuf.Xmodem.XModem_Control.ACK); |
||||
|
} |
||||
|
return await this.sendCommand( |
||||
|
Protobuf.Xmodem.XModem_Control.NAK, |
||||
|
undefined, |
||||
|
packet.seq, |
||||
|
); |
||||
|
} |
||||
|
case Protobuf.Xmodem.XModem_Control.STX: { |
||||
|
break; |
||||
|
} |
||||
|
case Protobuf.Xmodem.XModem_Control.EOT: { |
||||
|
// end of transmission
|
||||
|
break; |
||||
|
} |
||||
|
case Protobuf.Xmodem.XModem_Control.ACK: { |
||||
|
this.counter++; |
||||
|
if (this.txBuffer[this.counter - 1]) { |
||||
|
return this.sendCommand( |
||||
|
Protobuf.Xmodem.XModem_Control.SOH, |
||||
|
this.txBuffer[this.counter - 1], |
||||
|
this.counter, |
||||
|
crc16ccitt(this.txBuffer[this.counter - 1] ?? new Uint8Array()), |
||||
|
); |
||||
|
} |
||||
|
if (this.counter === this.txBuffer.length + 1) { |
||||
|
return this.sendCommand(Protobuf.Xmodem.XModem_Control.EOT); |
||||
|
} |
||||
|
this.clear(); |
||||
|
break; |
||||
|
} |
||||
|
case Protobuf.Xmodem.XModem_Control.NAK: { |
||||
|
return this.sendCommand( |
||||
|
Protobuf.Xmodem.XModem_Control.SOH, |
||||
|
this.txBuffer[this.counter], |
||||
|
this.counter, |
||||
|
crc16ccitt(this.txBuffer[this.counter - 1] ?? new Uint8Array()), |
||||
|
); |
||||
|
} |
||||
|
case Protobuf.Xmodem.XModem_Control.CAN: { |
||||
|
this.clear(); |
||||
|
break; |
||||
|
} |
||||
|
case Protobuf.Xmodem.XModem_Control.CTRLZ: { |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return Promise.resolve(0); |
||||
|
} |
||||
|
|
||||
|
validateCrc16(packet: Protobuf.Xmodem.XModem): boolean { |
||||
|
return crc16ccitt(packet.buffer) === packet.crc16; |
||||
|
} |
||||
|
|
||||
|
clear() { |
||||
|
this.counter = 0; |
||||
|
this.rxBuffer = []; |
||||
|
this.txBuffer = []; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
# @meshtastic/transport-deno |
||||
|
|
||||
|
[](https://jsr.io/@meshtastic/transport-deno) |
||||
|
[](https://github.com/meshtastic/js/actions/workflows/ci.yml) |
||||
|
[](https://cla-assistant.io/meshtastic/meshtastic.js) |
||||
|
[](https://opencollective.com/meshtastic/) |
||||
|
[](https://vercel.com?utm_source=meshtastic&utm_campaign=oss) |
||||
|
|
||||
|
## Overview |
||||
|
|
||||
|
`@meshtastic/transport-deno` Provides TCP transport (Deno) for Meshtastic |
||||
|
devices. Installation instructions are avaliable at |
||||
|
[JSR](https://jsr.io/@meshtastic/transport-deno) |
||||
|
|
||||
|
## Usage |
||||
|
|
||||
|
```ts |
||||
|
import { MeshDevice } from "@meshtastic/core"; |
||||
|
import { TransportDeno } from "@meshtastic/transport-deno"; |
||||
|
|
||||
|
const transport = await TransportDeno.create("10.10.0.57"); |
||||
|
const device = new MeshDevice(transport); |
||||
|
``` |
||||
|
|
||||
|
## Stats |
||||
|
|
||||
|
 |
||||
@ -0,0 +1,7 @@ |
|||||
|
{ |
||||
|
"name": "@meshtastic/transport-deno", |
||||
|
"version": "0.1.1", |
||||
|
"exports": { |
||||
|
".": "./mod.ts" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1 @@ |
|||||
|
export { TransportDeno } from "./src/transport.ts"; |
||||
@ -0,0 +1,32 @@ |
|||||
|
import { Utils } from "@meshtastic/core"; |
||||
|
import type { Types } from "@meshtastic/core"; |
||||
|
|
||||
|
export class TransportDeno implements Types.Transport { |
||||
|
private _toDevice: WritableStream<Uint8Array>; |
||||
|
private _fromDevice: ReadableStream<Types.DeviceOutput>; |
||||
|
|
||||
|
public static async create(hostname: string): Promise<TransportDeno> { |
||||
|
const connection = await Deno.connect({ |
||||
|
hostname, |
||||
|
port: 4403, |
||||
|
}); |
||||
|
return new TransportDeno(connection); |
||||
|
} |
||||
|
|
||||
|
constructor(connection: Deno.Conn) { |
||||
|
Utils.toDeviceStream.readable.pipeTo(connection.writable); |
||||
|
|
||||
|
this._toDevice = Utils.toDeviceStream.writable; |
||||
|
this._fromDevice = connection.readable.pipeThrough( |
||||
|
Utils.fromDeviceStream(), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
get toDevice(): WritableStream<Uint8Array> { |
||||
|
return this._toDevice; |
||||
|
} |
||||
|
|
||||
|
get fromDevice(): ReadableStream<Types.DeviceOutput> { |
||||
|
return this._fromDevice; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
# @meshtastic/transport-http |
||||
|
|
||||
|
[](https://jsr.io/@meshtastic/transport-http) |
||||
|
[](https://github.com/meshtastic/js/actions/workflows/ci.yml) |
||||
|
[](https://cla-assistant.io/meshtastic/meshtastic.js) |
||||
|
[](https://opencollective.com/meshtastic/) |
||||
|
[](https://vercel.com?utm_source=meshtastic&utm_campaign=oss) |
||||
|
|
||||
|
## Overview |
||||
|
|
||||
|
`@meshtastic/transport-http` Provides HTTP(S) transport for Meshtastic devices. |
||||
|
Installation instructions are available at |
||||
|
[JSR](https://jsr.io/@meshtastic/transport-http) |
||||
|
|
||||
|
## Usage |
||||
|
|
||||
|
```ts |
||||
|
import { MeshDevice } from "@meshtastic/core"; |
||||
|
import { TransportHTTP } from "@meshtastic/transport-http"; |
||||
|
|
||||
|
const transport = await TransportHTTP.create("10.10.0.57"); |
||||
|
const device = new MeshDevice(transport); |
||||
|
``` |
||||
|
|
||||
|
## Stats |
||||
|
|
||||
|
 |
||||
@ -0,0 +1,7 @@ |
|||||
|
{ |
||||
|
"name": "@meshtastic/transport-http", |
||||
|
"version": "0.2.1", |
||||
|
"exports": { |
||||
|
".": "./mod.ts" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1 @@ |
|||||
|
export { TransportHTTP } from "./src/transport.ts"; |
||||
@ -0,0 +1,89 @@ |
|||||
|
import type { Types } from "@meshtastic/core"; |
||||
|
|
||||
|
export class TransportHTTP implements Types.Transport { |
||||
|
private _toDevice: WritableStream<Uint8Array>; |
||||
|
private _fromDevice: ReadableStream<Types.DeviceOutput>; |
||||
|
private url: string; |
||||
|
private receiveBatchRequests: boolean; |
||||
|
private fetchInterval: number; |
||||
|
|
||||
|
public static async create( |
||||
|
address: string, |
||||
|
tls?: boolean, |
||||
|
): Promise<TransportHTTP> { |
||||
|
const connectionUrl = `${tls ? "https" : "http"}://${address}`; |
||||
|
await fetch(`${connectionUrl}/json/report`); |
||||
|
await Promise.resolve(); |
||||
|
return new TransportHTTP(connectionUrl); |
||||
|
} |
||||
|
|
||||
|
constructor(url: string) { |
||||
|
this.url = url; |
||||
|
this.receiveBatchRequests = false; |
||||
|
this.fetchInterval = 3000; |
||||
|
|
||||
|
this._toDevice = new WritableStream<Uint8Array>({ |
||||
|
write: async (chunk) => { |
||||
|
await this.writeToRadio(chunk); |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
let controller: ReadableStreamDefaultController<Types.DeviceOutput>; |
||||
|
|
||||
|
this._fromDevice = new ReadableStream<Types.DeviceOutput>({ |
||||
|
start: (ctrl) => { |
||||
|
controller = ctrl; |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
setInterval(async () => { |
||||
|
await this.readFromRadio(controller); |
||||
|
}, this.fetchInterval); |
||||
|
} |
||||
|
|
||||
|
private async readFromRadio( |
||||
|
controller: ReadableStreamDefaultController<Types.DeviceOutput>, |
||||
|
): Promise<void> { |
||||
|
let readBuffer = new ArrayBuffer(1); |
||||
|
while (readBuffer.byteLength > 0) { |
||||
|
const response = await fetch( |
||||
|
`${this.url}/api/v1/fromradio?all=${ |
||||
|
this.receiveBatchRequests ? "true" : "false" |
||||
|
}`,
|
||||
|
{ |
||||
|
method: "GET", |
||||
|
headers: { |
||||
|
Accept: "application/x-protobuf", |
||||
|
}, |
||||
|
}, |
||||
|
); |
||||
|
|
||||
|
readBuffer = await response.arrayBuffer(); |
||||
|
|
||||
|
if (readBuffer.byteLength > 0) { |
||||
|
controller.enqueue({ |
||||
|
type: "packet", |
||||
|
data: new Uint8Array(readBuffer), |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private async writeToRadio(data: Uint8Array): Promise<void> { |
||||
|
await fetch(`${this.url}/api/v1/toradio`, { |
||||
|
method: "PUT", |
||||
|
headers: { |
||||
|
"Content-Type": "application/x-protobuf", |
||||
|
}, |
||||
|
body: data, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
get toDevice(): WritableStream<Uint8Array> { |
||||
|
return this._toDevice; |
||||
|
} |
||||
|
|
||||
|
get fromDevice(): ReadableStream<Types.DeviceOutput> { |
||||
|
return this._fromDevice; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,34 @@ |
|||||
|
# @meshtastic/transport-web-bluetooth |
||||
|
|
||||
|
[](https://jsr.io/@meshtastic/transport-web-bluetooth) |
||||
|
[](https://github.com/meshtastic/js/actions/workflows/ci.yml) |
||||
|
[](https://cla-assistant.io/meshtastic/meshtastic.js) |
||||
|
[](https://opencollective.com/meshtastic/) |
||||
|
[](https://vercel.com?utm_source=meshtastic&utm_campaign=oss) |
||||
|
|
||||
|
## Overview |
||||
|
|
||||
|
`@meshtastic/transport-web-bluetooth` Provides Web Bluetooth transport for |
||||
|
Meshtastic devices. Installation instructions are available at |
||||
|
[JSR](https://jsr.io/@meshtastic/transport-web-bluetooth) |
||||
|
|
||||
|
## Usage |
||||
|
|
||||
|
```ts |
||||
|
import { MeshDevice } from "@meshtastic/core"; |
||||
|
import { TransportWebBluetooth } from "@meshtastic/transport-web-bluetooth"; |
||||
|
|
||||
|
const transport = await TransportWebBluetooth.create(); |
||||
|
const device = new MeshDevice(transport); |
||||
|
``` |
||||
|
|
||||
|
## Stats |
||||
|
|
||||
|
 |
||||
|
|
||||
|
### Compatibility |
||||
|
|
||||
|
The Web Bluetooth API's have limited support in browsers, compatibility is |
||||
|
represented in the matrix below. |
||||
|
|
||||
|
 |
||||
@ -0,0 +1,15 @@ |
|||||
|
{ |
||||
|
"name": "@meshtastic/transport-web-bluetooth", |
||||
|
"version": "0.1.2", |
||||
|
"exports": { |
||||
|
".": "./mod.ts" |
||||
|
}, |
||||
|
"imports": { |
||||
|
"@types/web-bluetooth": "npm:@types/web-bluetooth@^0.0.20" |
||||
|
}, |
||||
|
"compilerOptions": { |
||||
|
"types": [ |
||||
|
"@types/web-bluetooth" |
||||
|
] |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1 @@ |
|||||
|
export { TransportWebBluetooth } from "./src/transport.ts"; |
||||
@ -0,0 +1,134 @@ |
|||||
|
import type { Types } from "@meshtastic/core"; |
||||
|
|
||||
|
export class TransportWebBluetooth implements Types.Transport { |
||||
|
private _toDevice: WritableStream<Uint8Array>; |
||||
|
private _fromDevice: ReadableStream<Types.DeviceOutput>; |
||||
|
private _fromDeviceController?: ReadableStreamDefaultController< |
||||
|
Types.DeviceOutput |
||||
|
>; |
||||
|
private _isFirstWrite = true; |
||||
|
|
||||
|
private toRadioCharacteristic: BluetoothRemoteGATTCharacteristic; |
||||
|
private fromRadioCharacteristic: BluetoothRemoteGATTCharacteristic; |
||||
|
private fromNumCharacteristic: BluetoothRemoteGATTCharacteristic; |
||||
|
|
||||
|
static ToRadioUuid = "f75c76d2-129e-4dad-a1dd-7866124401e7"; |
||||
|
static FromRadioUuid = "2c55e69e-4993-11ed-b878-0242ac120002"; |
||||
|
static FromNumUuid = "ed9da18c-a800-4f66-a670-aa7547e34453"; |
||||
|
static ServiceUuid = "6ba1b218-15a8-461f-9fa8-5dcae273eafd"; |
||||
|
|
||||
|
public static async create(): Promise<TransportWebBluetooth> { |
||||
|
const device = await navigator.bluetooth.requestDevice({ |
||||
|
filters: [{ services: [this.ServiceUuid] }], |
||||
|
}); |
||||
|
return await this.prepareConnection(device); |
||||
|
} |
||||
|
|
||||
|
public static async createFromDevice( |
||||
|
device: BluetoothDevice, |
||||
|
): Promise<TransportWebBluetooth> { |
||||
|
return await this.prepareConnection(device); |
||||
|
} |
||||
|
|
||||
|
public static async prepareConnection( |
||||
|
device: BluetoothDevice, |
||||
|
): Promise<TransportWebBluetooth> { |
||||
|
const gattServer = await device.gatt?.connect(); |
||||
|
|
||||
|
if (!gattServer) { |
||||
|
throw new Error("Failed to connect to GATT server"); |
||||
|
} |
||||
|
|
||||
|
const service = await gattServer.getPrimaryService(this.ServiceUuid); |
||||
|
|
||||
|
const toRadioCharacteristic = await service.getCharacteristic( |
||||
|
this.ToRadioUuid, |
||||
|
); |
||||
|
const fromRadioCharacteristic = await service.getCharacteristic( |
||||
|
this.FromRadioUuid, |
||||
|
); |
||||
|
const fromNumCharacteristic = await service.getCharacteristic( |
||||
|
this.FromNumUuid, |
||||
|
); |
||||
|
|
||||
|
if ( |
||||
|
!toRadioCharacteristic || !fromRadioCharacteristic || |
||||
|
!fromNumCharacteristic |
||||
|
) { |
||||
|
throw new Error("Failed to find required characteristics"); |
||||
|
} |
||||
|
|
||||
|
console.log("Connected to device", device.name); |
||||
|
|
||||
|
return new TransportWebBluetooth( |
||||
|
toRadioCharacteristic, |
||||
|
fromRadioCharacteristic, |
||||
|
fromNumCharacteristic, |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
constructor( |
||||
|
toRadioCharacteristic: BluetoothRemoteGATTCharacteristic, |
||||
|
fromRadioCharacteristic: BluetoothRemoteGATTCharacteristic, |
||||
|
fromNumCharacteristic: BluetoothRemoteGATTCharacteristic, |
||||
|
) { |
||||
|
this.toRadioCharacteristic = toRadioCharacteristic; |
||||
|
this.fromRadioCharacteristic = fromRadioCharacteristic; |
||||
|
this.fromNumCharacteristic = fromNumCharacteristic; |
||||
|
|
||||
|
this._fromDevice = new ReadableStream({ |
||||
|
start: (ctrl) => { |
||||
|
this._fromDeviceController = ctrl; |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
this._toDevice = new WritableStream({ |
||||
|
write: async (chunk) => { |
||||
|
await this.toRadioCharacteristic.writeValue(chunk); |
||||
|
|
||||
|
if (this._isFirstWrite && this._fromDeviceController) { |
||||
|
this._isFirstWrite = false; |
||||
|
setTimeout(() => { |
||||
|
this.readFromRadio(this._fromDeviceController!); |
||||
|
}, 50); |
||||
|
} |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
this.fromNumCharacteristic.addEventListener( |
||||
|
"characteristicvaluechanged", |
||||
|
() => { |
||||
|
if (this._fromDeviceController) { |
||||
|
this.readFromRadio(this._fromDeviceController); |
||||
|
} |
||||
|
}, |
||||
|
); |
||||
|
|
||||
|
this.fromNumCharacteristic.startNotifications(); |
||||
|
} |
||||
|
|
||||
|
get toDevice(): WritableStream<Uint8Array> { |
||||
|
return this._toDevice; |
||||
|
} |
||||
|
|
||||
|
get fromDevice(): ReadableStream<Types.DeviceOutput> { |
||||
|
return this._fromDevice; |
||||
|
} |
||||
|
|
||||
|
protected async readFromRadio( |
||||
|
controller: ReadableStreamDefaultController<Types.DeviceOutput>, |
||||
|
): Promise<void> { |
||||
|
let hasMoreData = true; |
||||
|
while (hasMoreData && this.fromRadioCharacteristic) { |
||||
|
const value = await this.fromRadioCharacteristic.readValue(); |
||||
|
if (value.byteLength === 0) { |
||||
|
hasMoreData = false; |
||||
|
continue; |
||||
|
} |
||||
|
controller.enqueue({ |
||||
|
type: "packet", |
||||
|
data: new Uint8Array(value.buffer), |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,34 @@ |
|||||
|
# @meshtastic/transport-web-serial |
||||
|
|
||||
|
[](https://jsr.io/@meshtastic/transport-web-serial) |
||||
|
[](https://github.com/meshtastic/js/actions/workflows/ci.yml) |
||||
|
[](https://cla-assistant.io/meshtastic/meshtastic.js) |
||||
|
[](https://opencollective.com/meshtastic/) |
||||
|
[](https://vercel.com?utm_source=meshtastic&utm_campaign=oss) |
||||
|
|
||||
|
## Overview |
||||
|
|
||||
|
`@meshtastic/transport-web-serial` Provides Web Serial transport for Meshtastic |
||||
|
devices. Installation instructions are avaliable at |
||||
|
[JSR](https://jsr.io/@meshtastic/transport-web-serial) |
||||
|
|
||||
|
## Usage |
||||
|
|
||||
|
```ts |
||||
|
import { MeshDevice } from "@meshtastic/core"; |
||||
|
import { TransportWebSerial } from "@meshtastic/transport-web-serial"; |
||||
|
|
||||
|
const transport = await TransportWebSerial.create(); |
||||
|
const device = new MeshDevice(transport); |
||||
|
``` |
||||
|
|
||||
|
## Stats |
||||
|
|
||||
|
 |
||||
|
|
||||
|
### Compatibility |
||||
|
|
||||
|
The Web Serial API's have limited support in browsers, compatibility is |
||||
|
represented in the matrix below. |
||||
|
|
||||
|
 |
||||
@ -0,0 +1,15 @@ |
|||||
|
{ |
||||
|
"name": "@meshtastic/transport-web-serial", |
||||
|
"version": "0.2.1", |
||||
|
"exports": { |
||||
|
".": "./mod.ts" |
||||
|
}, |
||||
|
"imports": { |
||||
|
"@types/w3c-web-serial": "npm:@types/w3c-web-serial@^1.0.7" |
||||
|
}, |
||||
|
"compilerOptions": { |
||||
|
"types": [ |
||||
|
"@types/w3c-web-serial" |
||||
|
] |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1 @@ |
|||||
|
export { TransportWebSerial } from "./src/transport.ts"; |
||||
@ -0,0 +1,42 @@ |
|||||
|
import { Utils } from "@meshtastic/core"; |
||||
|
import type { Types } from "@meshtastic/core"; |
||||
|
|
||||
|
export class TransportWebSerial implements Types.Transport { |
||||
|
private _toDevice: WritableStream<Uint8Array>; |
||||
|
private _fromDevice: ReadableStream<Types.DeviceOutput>; |
||||
|
|
||||
|
public static async create(baudRate?: number): Promise<TransportWebSerial> { |
||||
|
const port = await navigator.serial.requestPort(); |
||||
|
await port.open({ baudRate: baudRate || 115200 }); |
||||
|
return new TransportWebSerial(port); |
||||
|
} |
||||
|
|
||||
|
public static async createFromPort( |
||||
|
port: SerialPort, |
||||
|
baudRate?: number, |
||||
|
): Promise<TransportWebSerial> { |
||||
|
await port.open({ baudRate: baudRate || 115200 }); |
||||
|
return new TransportWebSerial(port); |
||||
|
} |
||||
|
|
||||
|
constructor(connection: SerialPort) { |
||||
|
if (!connection.readable || !connection.writable) { |
||||
|
throw new Error("Stream not accessible"); |
||||
|
} |
||||
|
|
||||
|
Utils.toDeviceStream.readable.pipeTo(connection.writable); |
||||
|
|
||||
|
this._toDevice = Utils.toDeviceStream.writable; |
||||
|
this._fromDevice = connection.readable.pipeThrough( |
||||
|
Utils.fromDeviceStream(), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
get toDevice(): WritableStream<Uint8Array> { |
||||
|
return this._toDevice; |
||||
|
} |
||||
|
|
||||
|
get fromDevice(): ReadableStream<Types.DeviceOutput> { |
||||
|
return this._fromDevice; |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue