Browse Source

Feat/add js repo to monorepo (#683)

* 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
Dan Ditomaso 11 months ago
committed by GitHub
parent
commit
72edb1897f
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 37
      .github/workflows/js-ci.yml
  2. 3
      .gitmodules
  3. 158
      README.md
  4. 15
      deno.json
  5. 200
      deno.lock
  6. 26
      packages/core/README.md
  7. 10
      packages/core/deno.json
  8. 5
      packages/core/mod.ts
  9. 10
      packages/core/src/constants.ts
  10. 1177
      packages/core/src/meshDevice.ts
  11. 128
      packages/core/src/types.ts
  12. 467
      packages/core/src/utils/eventSystem.ts
  13. 5
      packages/core/src/utils/mod.ts
  14. 119
      packages/core/src/utils/queue.ts
  15. 222
      packages/core/src/utils/transform/decodePacket.ts
  16. 73
      packages/core/src/utils/transform/fromDevice.ts
  17. 16
      packages/core/src/utils/transform/toDevice.ts
  18. 135
      packages/core/src/utils/xmodem.ts
  19. 27
      packages/transport-deno/README.md
  20. 7
      packages/transport-deno/deno.json
  21. 1
      packages/transport-deno/mod.ts
  22. 32
      packages/transport-deno/src/transport.ts
  23. 27
      packages/transport-http/README.md
  24. 7
      packages/transport-http/deno.json
  25. 1
      packages/transport-http/mod.ts
  26. 89
      packages/transport-http/src/transport.ts
  27. 34
      packages/transport-web-bluetooth/README.md
  28. 15
      packages/transport-web-bluetooth/deno.json
  29. 1
      packages/transport-web-bluetooth/mod.ts
  30. 134
      packages/transport-web-bluetooth/src/transport.ts
  31. 34
      packages/transport-web-serial/README.md
  32. 15
      packages/transport-web-serial/deno.json
  33. 1
      packages/transport-web-serial/mod.ts
  34. 42
      packages/transport-web-serial/src/transport.ts

37
.github/workflows/js-ci.yml

@ -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

3
.gitmodules

@ -1,3 +0,0 @@
[submodule "src/core/connection"]
path = src/core/connection
url = https://github.com/meshtastic/js.git

158
README.md

@ -0,0 +1,158 @@
# Meshtastic Web Monorepo
[![CI](https://img.shields.io/github/actions/workflow/status/meshtastic/web/ci.yml?branch=main&label=Web%20CI&logo=github&color=yellow)](https://github.com/meshtastic/web/actions/workflows/ci.yml)
[![CI](https://img.shields.io/github/actions/workflow/status/meshtastic/js/ci.yml?branch=master&label=JS%20CI&logo=github&color=yellow)](https://github.com/meshtastic/js/actions/workflows/ci.yml)
[![CLA assistant](https://cla-assistant.io/readme/badge/meshtastic/web)](https://cla-assistant.io/meshtastic/web)
[![Fiscal Contributors](https://opencollective.com/meshtastic/tiers/badge.svg?label=Fiscal%20Contributors&color=deeppink)](https://opencollective.com/meshtastic/)
[![Vercel](https://img.shields.io/static/v1?label=Powered%20by&message=Vercel&style=flat&logo=vercel&color=000000)](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 | ![Alt](https://repobeats.axiom.co/api/embed/e5b062db986cb005d83e81724c00cb2b9cce8e4c.svg "Repobeats analytics image") |
| Meshtastic JS | ![Alt](https://repobeats.axiom.co/api/embed/5330641586e92a2ec84676fedb98f6d4a7b25d69.svg "Repobeats analytics image") |
---
## 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!

15
deno.json

@ -1,8 +1,19 @@
{
"workspace": [
"./packages/web"
"./packages/web",
"./packages/core",
"./packages/transport-deno",
"./packages/transport-http",
"./packages/transport-web-bluetooth",
"./packages/transport-web-serial"
],
"imports": {},
"imports": {
"@bufbuild/protobuf": "npm:@bufbuild/protobuf@^2.2.3",
"@meshtastic/protobufs": "jsr:@meshtastic/protobufs@^2.7.0",
"@types/node": "npm:@types/node@^22.13.10",
"ste-simple-events": "npm:ste-simple-events@^3.0.11",
"tslog": "npm:tslog@^4.9.3"
},
"nodeModulesDir": "auto",
"lint": {
"exclude": [

200
deno.lock

@ -1,7 +1,9 @@
{
"version": "5",
"specifiers": {
"jsr:@meshtastic/protobufs@^2.7.0": "2.7.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:@hookform/resolvers@^5.1.1": "[email protected][email protected][email protected]",
"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/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-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/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:@turf/turf@^7.2.0": "7.2.0",
"npm:@types/chrome@^0.0.318": "0.0.318",
"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/react-dom@^19.1.3": "19.1.6_@[email protected]",
"npm:@types/react@^19.1.2": "19.1.8",
"npm:@types/serviceworker@^0.0.133": "0.0.133",
"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/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:@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:base64-js@^1.5.1": "1.5.1",
"npm:class-variance-authority@~0.7.1": "0.7.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:crc@^4.3.2": "4.3.2",
"npm:crypto-random-string@5": "5.0.0",
"npm:gzipper@^8.2.1": "8.2.1",
"npm:happy-dom@^17.4.6": "17.6.3",
@ -73,19 +79,27 @@
"npm:react@^19.1.0": "19.1.0",
"npm:rfc4648@^1.5.4": "1.5.4",
"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:tailwindcss-animate@^1.0.7": "[email protected]",
"npm:tailwindcss@^4.1.5": "4.1.10",
"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:tslog@^4.9.3": "4.9.3",
"npm:typescript@^5.8.3": "5.8.3",
"npm:vite@*": "7.0.0_@[email protected][email protected]",
"npm:vite@7": "7.0.0_@[email protected][email protected]",
"npm:vitest@^3.2.4": "3.2.4_@[email protected][email protected][email protected]__@[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]_@[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:[email protected]": "5.0.5_@[email protected][email protected][email protected]"
},
"jsr": {
"@meshtastic/[email protected]": {
"integrity": "38357241bd8a7431c87366dbe12ce9e69f204ebb6ec23da12f7682765b6c8376",
"dependencies": [
"npm:@bufbuild/protobuf@^2.2.3"
]
},
"@std/[email protected]": {
"integrity": "ddc94f8e3c275627281cbc23341df6b8bcc874d70374f75fec2533521e3d6886"
}
@ -1756,12 +1770,37 @@
"babel-dead-code-elimination",
"chokidar",
"unplugin",
"vite",
"vite@7.0.0_@[email protected][email protected]",
"zod"
],
"optionalPeers": [
"@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]": {
@ -3262,10 +3301,16 @@
"@types/pbf"
]
},
"@types/[email protected]": {
"integrity": "sha512-wzoocdnnpSxZ+6CjW4ADCK1jVmd1S/J3ArNWfn8FDDQtRm8dkDg7TA+mvek2wNrfCgwuZxqEOiB9B1XCJ6+dbw==",
"dependencies": [
"[email protected]"
]
},
"@types/[email protected]": {
"integrity": "sha512-ulyqAkrhnuNq9pB76DRBTkcS6YsmDALy6Ua63V8OhrOBgbcYt6IOdzpw5P1+dyRIyMerzLkeYWBeOXPpA9GMAA==",
"dependencies": [
"undici-types"
"undici-types@7.8.0"
]
},
"@types/[email protected]": {
@ -3295,6 +3340,9 @@
"@types/[email protected]": {
"integrity": "sha512-QQOT+bxQJhRGXoZDZGLs3ksLud1dMNnMiSQtBA0w8KXvLpXX4oM4TZb6J0GgJ8UbCaHo5s9/4VQT8uXy9JER2A=="
},
"@types/[email protected]": {
"integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow=="
},
"@types/[email protected]": {
"integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA=="
},
@ -3326,7 +3374,19 @@
"@rolldown/pluginutils",
"@types/babel__core",
"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]": {
@ -3345,10 +3405,22 @@
"@vitest/spy",
"estree-walker",
"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": [
"vite"
"vite@7.0.0_@[email protected][email protected]_@[email protected]"
]
},
"@vitest/[email protected]": {
@ -4823,6 +4895,9 @@
"typewise-core"
]
},
"[email protected]": {
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="
},
"[email protected]": {
"integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="
},
@ -4891,14 +4966,25 @@
"debug",
"es-module-lexer",
"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
},
"[email protected]_@[email protected][email protected]": {
"integrity": "sha512-ixXJB1YRgDIw2OszKQS9WxGHKwLdCsbQNkpJN171udl6szi/rIySHL6/Os3s2+oE4P/FLD4dxg4mD7Wust+u5g==",
"dependencies": [
"@types/node",
"@types/node@24.0.4",
"esbuild",
"fdir",
"[email protected]",
@ -4910,7 +4996,26 @@
"fsevents"
],
"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
},
@ -4918,9 +5023,9 @@
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
"dependencies": [
"@types/chai",
"@types/node",
"@types/node@24.0.4",
"@vitest/expect",
"@vitest/mocker",
"@vitest/mocker@[email protected]__@[email protected][email protected]_@[email protected]",
"@vitest/pretty-format",
"@vitest/runner",
"@vitest/snapshot",
@ -4939,12 +5044,47 @@
"tinyglobby",
"tinypool",
"tinyrainbow",
"vite",
"vite-node",
"vite@7.0.0_@[email protected][email protected]",
"vite-node@3.2.4_@[email protected]",
"why-is-node-running"
],
"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"
],
"bin": true
@ -5053,7 +5193,29 @@
}
},
"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": {
"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": {
"dependencies": [
"jsr:@std/path@^1.1.0",

26
packages/core/README.md

@ -0,0 +1,26 @@
# @meshtastic/core
[![JSR](https://jsr.io/badges/@meshtastic/core)](https://jsr.io/@meshtastic/core)
[![CI](https://img.shields.io/github/actions/workflow/status/meshtastic/js/ci.yml?branch=master&label=actions&logo=github&color=yellow)](https://github.com/meshtastic/js/actions/workflows/ci.yml)
[![CLA assistant](https://cla-assistant.io/readme/badge/meshtastic/meshtastic.js)](https://cla-assistant.io/meshtastic/meshtastic.js)
[![Fiscal Contributors](https://opencollective.com/meshtastic/tiers/badge.svg?label=Fiscal%20Contributors&color=deeppink)](https://opencollective.com/meshtastic/)
[![Vercel](https://img.shields.io/static/v1?label=Powered%20by&message=Vercel&style=flat&logo=vercel&color=000000)](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
![Alt](https://repobeats.axiom.co/api/embed/5330641586e92a2ec84676fedb98f6d4a7b25d69.svg "Repobeats analytics image")

10
packages/core/deno.json

@ -0,0 +1,10 @@
{
"name": "@meshtastic/core",
"version": "2.6.4",
"exports": {
".": "./mod.ts"
},
"imports": {
"crc": "npm:crc@^4.3.2"
}
}

5
packages/core/mod.ts

@ -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";

10
packages/core/src/constants.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,
};

1177
packages/core/src/meshDevice.ts

File diff suppressed because it is too large

128
packages/core/src/types.ts

@ -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;
}

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

@ -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
>();
}

5
packages/core/src/utils/mod.ts

@ -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";

119
packages/core/src/utils/queue.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;
}
}
}

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

@ -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}`,
);
}
}
}
}
},
});

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

@ -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;
}
}
},
});
};

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

@ -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]));
},
});

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

@ -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 = [];
}
}

27
packages/transport-deno/README.md

@ -0,0 +1,27 @@
# @meshtastic/transport-deno
[![JSR](https://jsr.io/badges/@meshtastic/transport-deno)](https://jsr.io/@meshtastic/transport-deno)
[![CI](https://img.shields.io/github/actions/workflow/status/meshtastic/js/ci.yml?branch=master&label=actions&logo=github&color=yellow)](https://github.com/meshtastic/js/actions/workflows/ci.yml)
[![CLA assistant](https://cla-assistant.io/readme/badge/meshtastic/meshtastic.js)](https://cla-assistant.io/meshtastic/meshtastic.js)
[![Fiscal Contributors](https://opencollective.com/meshtastic/tiers/badge.svg?label=Fiscal%20Contributors&color=deeppink)](https://opencollective.com/meshtastic/)
[![Vercel](https://img.shields.io/static/v1?label=Powered%20by&message=Vercel&style=flat&logo=vercel&color=000000)](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
![Alt](https://repobeats.axiom.co/api/embed/5330641586e92a2ec84676fedb98f6d4a7b25d69.svg "Repobeats analytics image")

7
packages/transport-deno/deno.json

@ -0,0 +1,7 @@
{
"name": "@meshtastic/transport-deno",
"version": "0.1.1",
"exports": {
".": "./mod.ts"
}
}

1
packages/transport-deno/mod.ts

@ -0,0 +1 @@
export { TransportDeno } from "./src/transport.ts";

32
packages/transport-deno/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;
}
}

27
packages/transport-http/README.md

@ -0,0 +1,27 @@
# @meshtastic/transport-http
[![JSR](https://jsr.io/badges/@meshtastic/transport-http)](https://jsr.io/@meshtastic/transport-http)
[![CI](https://img.shields.io/github/actions/workflow/status/meshtastic/js/ci.yml?branch=master&label=actions&logo=github&color=yellow)](https://github.com/meshtastic/js/actions/workflows/ci.yml)
[![CLA assistant](https://cla-assistant.io/readme/badge/meshtastic/meshtastic.js)](https://cla-assistant.io/meshtastic/meshtastic.js)
[![Fiscal Contributors](https://opencollective.com/meshtastic/tiers/badge.svg?label=Fiscal%20Contributors&color=deeppink)](https://opencollective.com/meshtastic/)
[![Vercel](https://img.shields.io/static/v1?label=Powered%20by&message=Vercel&style=flat&logo=vercel&color=000000)](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
![Alt](https://repobeats.axiom.co/api/embed/5330641586e92a2ec84676fedb98f6d4a7b25d69.svg "Repobeats analytics image")

7
packages/transport-http/deno.json

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

1
packages/transport-http/mod.ts

@ -0,0 +1 @@
export { TransportHTTP } from "./src/transport.ts";

89
packages/transport-http/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;
}
}

34
packages/transport-web-bluetooth/README.md

@ -0,0 +1,34 @@
# @meshtastic/transport-web-bluetooth
[![JSR](https://jsr.io/badges/@meshtastic/transport-web-bluetooth)](https://jsr.io/@meshtastic/transport-web-bluetooth)
[![CI](https://img.shields.io/github/actions/workflow/status/meshtastic/js/ci.yml?branch=master&label=actions&logo=github&color=yellow)](https://github.com/meshtastic/js/actions/workflows/ci.yml)
[![CLA assistant](https://cla-assistant.io/readme/badge/meshtastic/meshtastic.js)](https://cla-assistant.io/meshtastic/meshtastic.js)
[![Fiscal Contributors](https://opencollective.com/meshtastic/tiers/badge.svg?label=Fiscal%20Contributors&color=deeppink)](https://opencollective.com/meshtastic/)
[![Vercel](https://img.shields.io/static/v1?label=Powered%20by&message=Vercel&style=flat&logo=vercel&color=000000)](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
![Alt](https://repobeats.axiom.co/api/embed/5330641586e92a2ec84676fedb98f6d4a7b25d69.svg "Repobeats analytics image")
### Compatibility
The Web Bluetooth API's have limited support in browsers, compatibility is
represented in the matrix below.
![Web Bluetooth compatability matrix](https://caniuse.bitsofco.de/image/web-bluetooth.png)

15
packages/transport-web-bluetooth/deno.json

@ -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"
]
}
}

1
packages/transport-web-bluetooth/mod.ts

@ -0,0 +1 @@
export { TransportWebBluetooth } from "./src/transport.ts";

134
packages/transport-web-bluetooth/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),
});
}
}
}

34
packages/transport-web-serial/README.md

@ -0,0 +1,34 @@
# @meshtastic/transport-web-serial
[![JSR](https://jsr.io/badges/@meshtastic/transport-web-serial)](https://jsr.io/@meshtastic/transport-web-serial)
[![CI](https://img.shields.io/github/actions/workflow/status/meshtastic/js/ci.yml?branch=master&label=actions&logo=github&color=yellow)](https://github.com/meshtastic/js/actions/workflows/ci.yml)
[![CLA assistant](https://cla-assistant.io/readme/badge/meshtastic/meshtastic.js)](https://cla-assistant.io/meshtastic/meshtastic.js)
[![Fiscal Contributors](https://opencollective.com/meshtastic/tiers/badge.svg?label=Fiscal%20Contributors&color=deeppink)](https://opencollective.com/meshtastic/)
[![Vercel](https://img.shields.io/static/v1?label=Powered%20by&message=Vercel&style=flat&logo=vercel&color=000000)](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
![Alt](https://repobeats.axiom.co/api/embed/5330641586e92a2ec84676fedb98f6d4a7b25d69.svg "Repobeats analytics image")
### Compatibility
The Web Serial API's have limited support in browsers, compatibility is
represented in the matrix below.
![Web Serial compatability matrix](https://caniuse.bitsofco.de/image/web-serial.png)

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

@ -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"
]
}
}

1
packages/transport-web-serial/mod.ts

@ -0,0 +1 @@
export { TransportWebSerial } from "./src/transport.ts";

42
packages/transport-web-serial/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…
Cancel
Save