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 <175728472+Copilot@users.noreply.github.com> * Update packages/transport-http/README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update packages/transport-web-bluetooth/README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -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,8 +1,19 @@
|
|||||||
{
|
{
|
||||||
"workspace": [
|
"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",
|
"nodeModulesDir": "auto",
|
||||||
"lint": {
|
"lint": {
|
||||||
"exclude": [
|
"exclude": [
|
||||||
|
|||||||
@@ -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": "5.1.1_react-hook-form@7.58.1__react@19.1.0_react@19.1.0",
|
"npm:@hookform/resolvers@^5.1.1": "5.1.1_react-hook-form@7.58.1__react@19.1.0_react@19.1.0",
|
||||||
"npm:@jsr/meshtastic__core@2.6.4": "2.6.4",
|
"npm:@jsr/meshtastic__core@2.6.4": "2.6.4",
|
||||||
@@ -31,27 +33,31 @@
|
|||||||
"npm:@tanstack/react-router@^1.120.15": "1.121.34_react@19.1.0_react-dom@19.1.0__react@19.1.0",
|
"npm:@tanstack/react-router@^1.120.15": "1.121.34_react@19.1.0_react-dom@19.1.0__react@19.1.0",
|
||||||
"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_@tanstack+react-router@1.121.34__react@19.1.0__react-dom@19.1.0___react@19.1.0_react@19.1.0_react-dom@19.1.0__react@19.1.0",
|
"npm:@tanstack/router-devtools@^1.120.15": "1.121.34_@tanstack+react-router@1.121.34__react@19.1.0__react-dom@19.1.0___react@19.1.0_react@19.1.0_react-dom@19.1.0__react@19.1.0",
|
||||||
"npm:@tanstack/router-plugin@^1.120.15": "1.121.37_@tanstack+react-router@1.121.34__react@19.1.0__react-dom@19.1.0___react@19.1.0_vite@7.0.0__@types+node@24.0.4__picomatch@4.0.2_@babel+core@7.27.4_react@19.1.0_react-dom@19.1.0__react@19.1.0_@types+node@24.0.4",
|
"npm:@tanstack/router-plugin@^1.120.15": "1.121.37_@tanstack+react-router@1.121.34__react@19.1.0__react-dom@19.1.0___react@19.1.0_vite@7.0.0__@types+node@24.0.4__picomatch@4.0.2_@babel+core@7.27.4_react@19.1.0_react-dom@19.1.0__react@19.1.0_@types+node@24.0.4_@types+node@22.15.33",
|
||||||
"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_@testing-library+dom@10.4.0_@types+react@19.1.8_@types+react-dom@19.1.6__@types+react@19.1.8_react@19.1.0_react-dom@19.1.0__react@19.1.0",
|
"npm:@testing-library/react@^16.3.0": "16.3.0_@testing-library+dom@10.4.0_@types+react@19.1.8_@types+react-dom@19.1.6__@types+react@19.1.8_react@19.1.0_react-dom@19.1.0__react@19.1.0",
|
||||||
"npm:@testing-library/user-event@^14.6.1": "14.6.1_@testing-library+dom@10.4.0",
|
"npm:@testing-library/user-event@^14.6.1": "14.6.1_@testing-library+dom@10.4.0",
|
||||||
"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_@types+react@19.1.8",
|
"npm:@types/react-dom@^19.1.3": "19.1.6_@types+react@19.1.8",
|
||||||
"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": "4.6.0_vite@7.0.0__@types+node@24.0.4__picomatch@4.0.2_@babel+core@7.27.4_@types+node@24.0.4",
|
"npm:@vitejs/plugin-react@^4.4.1": "4.6.0_vite@7.0.0__@types+node@24.0.4__picomatch@4.0.2_@babel+core@7.27.4_@types+node@24.0.4_@types+node@22.15.33",
|
||||||
"npm:autoprefixer@^10.4.21": "10.4.21_postcss@8.5.6",
|
"npm:autoprefixer@^10.4.21": "10.4.21_postcss@8.5.6",
|
||||||
"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": "1.1.1_react@19.1.0_react-dom@19.1.0__react@19.1.0_@types+react@19.1.8_@types+react-dom@19.1.6__@types+react@19.1.8",
|
"npm:cmdk@^1.1.1": "1.1.1_react@19.1.0_react-dom@19.1.0__react@19.1.0_@types+react@19.1.8_@types+react-dom@19.1.6__@types+react@19.1.8",
|
||||||
|
"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": "1.0.7_tailwindcss@4.1.10",
|
"npm:tailwindcss-animate@^1.0.7": "1.0.7_tailwindcss@4.1.10",
|
||||||
"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_@angular+common@6.1.10__@angular+core@6.1.10___rxjs@6.6.7___zone.js@0.8.29__rxjs@6.6.7_@angular+core@6.1.10__rxjs@6.6.7__zone.js@0.8.29",
|
"npm:testing-library@^0.0.2": "0.0.2_@angular+common@6.1.10__@angular+core@6.1.10___rxjs@6.6.7___zone.js@0.8.29__rxjs@6.6.7_@angular+core@6.1.10__rxjs@6.6.7__zone.js@0.8.29",
|
||||||
|
"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_@types+node@24.0.4_picomatch@4.0.2",
|
"npm:vite@*": "7.0.0_@types+node@24.0.4_picomatch@4.0.2_@types+node@22.15.33",
|
||||||
"npm:vite@7": "7.0.0_@types+node@24.0.4_picomatch@4.0.2",
|
"npm:vite@7": "7.0.0_@types+node@24.0.4_picomatch@4.0.2_@types+node@22.15.33",
|
||||||
"npm:vitest@^3.2.4": "3.2.4_@types+node@24.0.4_happy-dom@17.6.3_vite@7.0.0__@types+node@24.0.4__picomatch@4.0.2",
|
"npm:vitest@^3.2.4": "3.2.4_@types+node@24.0.4_happy-dom@17.6.3_vite@7.0.0__@types+node@24.0.4__picomatch@4.0.2_@types+node@22.15.33",
|
||||||
"npm:zod@^3.25.67": "3.25.67",
|
"npm:zod@^3.25.67": "3.25.67",
|
||||||
"npm:zustand@5.0.5": "5.0.5_@types+react@19.1.8_immer@10.1.1_react@19.1.0"
|
"npm:zustand@5.0.5": "5.0.5_@types+react@19.1.8_immer@10.1.1_react@19.1.0"
|
||||||
},
|
},
|
||||||
"jsr": {
|
"jsr": {
|
||||||
|
"@meshtastic/protobufs@2.7.0": {
|
||||||
|
"integrity": "38357241bd8a7431c87366dbe12ce9e69f204ebb6ec23da12f7682765b6c8376",
|
||||||
|
"dependencies": [
|
||||||
|
"npm:@bufbuild/protobuf@^2.2.3"
|
||||||
|
]
|
||||||
|
},
|
||||||
"@std/path@1.1.0": {
|
"@std/path@1.1.0": {
|
||||||
"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_@types+node@24.0.4_picomatch@4.0.2",
|
||||||
"zod"
|
"zod"
|
||||||
],
|
],
|
||||||
"optionalPeers": [
|
"optionalPeers": [
|
||||||
"@tanstack/react-router",
|
"@tanstack/react-router",
|
||||||
"vite"
|
"vite@7.0.0_@types+node@24.0.4_picomatch@4.0.2"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"@tanstack/router-plugin@1.121.37_@tanstack+react-router@1.121.34__react@19.1.0__react-dom@19.1.0___react@19.1.0_vite@7.0.0__@types+node@24.0.4__picomatch@4.0.2_@babel+core@7.27.4_react@19.1.0_react-dom@19.1.0__react@19.1.0_@types+node@24.0.4_@types+node@22.15.33": {
|
||||||
|
"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",
|
||||||
|
"vite@7.0.0_@types+node@24.0.4_picomatch@4.0.2_@types+node@22.15.33",
|
||||||
|
"zod"
|
||||||
|
],
|
||||||
|
"optionalPeers": [
|
||||||
|
"@tanstack/react-router",
|
||||||
|
"vite@7.0.0_@types+node@24.0.4_picomatch@4.0.2_@types+node@22.15.33"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@tanstack/router-utils@1.121.21_@babel+core@7.27.4": {
|
"@tanstack/router-utils@1.121.21_@babel+core@7.27.4": {
|
||||||
@@ -3262,10 +3301,16 @@
|
|||||||
"@types/pbf"
|
"@types/pbf"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"@types/node@22.15.33": {
|
||||||
|
"integrity": "sha512-wzoocdnnpSxZ+6CjW4ADCK1jVmd1S/J3ArNWfn8FDDQtRm8dkDg7TA+mvek2wNrfCgwuZxqEOiB9B1XCJ6+dbw==",
|
||||||
|
"dependencies": [
|
||||||
|
"undici-types@6.21.0"
|
||||||
|
]
|
||||||
|
},
|
||||||
"@types/node@24.0.4": {
|
"@types/node@24.0.4": {
|
||||||
"integrity": "sha512-ulyqAkrhnuNq9pB76DRBTkcS6YsmDALy6Ua63V8OhrOBgbcYt6IOdzpw5P1+dyRIyMerzLkeYWBeOXPpA9GMAA==",
|
"integrity": "sha512-ulyqAkrhnuNq9pB76DRBTkcS6YsmDALy6Ua63V8OhrOBgbcYt6IOdzpw5P1+dyRIyMerzLkeYWBeOXPpA9GMAA==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"undici-types"
|
"undici-types@7.8.0"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@types/pbf@3.0.5": {
|
"@types/pbf@3.0.5": {
|
||||||
@@ -3295,6 +3340,9 @@
|
|||||||
"@types/w3c-web-serial@1.0.8": {
|
"@types/w3c-web-serial@1.0.8": {
|
||||||
"integrity": "sha512-QQOT+bxQJhRGXoZDZGLs3ksLud1dMNnMiSQtBA0w8KXvLpXX4oM4TZb6J0GgJ8UbCaHo5s9/4VQT8uXy9JER2A=="
|
"integrity": "sha512-QQOT+bxQJhRGXoZDZGLs3ksLud1dMNnMiSQtBA0w8KXvLpXX4oM4TZb6J0GgJ8UbCaHo5s9/4VQT8uXy9JER2A=="
|
||||||
},
|
},
|
||||||
|
"@types/web-bluetooth@0.0.20": {
|
||||||
|
"integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow=="
|
||||||
|
},
|
||||||
"@types/web-bluetooth@0.0.21": {
|
"@types/web-bluetooth@0.0.21": {
|
||||||
"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"
|
"vite@7.0.0_@types+node@24.0.4_picomatch@4.0.2"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"@vitejs/plugin-react@4.6.0_vite@7.0.0__@types+node@24.0.4__picomatch@4.0.2_@babel+core@7.27.4_@types+node@24.0.4_@types+node@22.15.33": {
|
||||||
|
"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",
|
||||||
|
"vite@7.0.0_@types+node@24.0.4_picomatch@4.0.2_@types+node@22.15.33"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@vitest/expect@3.2.4": {
|
"@vitest/expect@3.2.4": {
|
||||||
@@ -3345,10 +3405,22 @@
|
|||||||
"@vitest/spy",
|
"@vitest/spy",
|
||||||
"estree-walker",
|
"estree-walker",
|
||||||
"magic-string",
|
"magic-string",
|
||||||
"vite"
|
"vite@7.0.0_@types+node@24.0.4_picomatch@4.0.2"
|
||||||
],
|
],
|
||||||
"optionalPeers": [
|
"optionalPeers": [
|
||||||
"vite"
|
"vite@7.0.0_@types+node@24.0.4_picomatch@4.0.2"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"@vitest/mocker@3.2.4_vite@7.0.0__@types+node@24.0.4__picomatch@4.0.2_@types+node@24.0.4_@types+node@22.15.33": {
|
||||||
|
"integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==",
|
||||||
|
"dependencies": [
|
||||||
|
"@vitest/spy",
|
||||||
|
"estree-walker",
|
||||||
|
"magic-string",
|
||||||
|
"vite@7.0.0_@types+node@24.0.4_picomatch@4.0.2_@types+node@22.15.33"
|
||||||
|
],
|
||||||
|
"optionalPeers": [
|
||||||
|
"vite@7.0.0_@types+node@24.0.4_picomatch@4.0.2_@types+node@22.15.33"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@vitest/pretty-format@3.2.4": {
|
"@vitest/pretty-format@3.2.4": {
|
||||||
@@ -4823,6 +4895,9 @@
|
|||||||
"typewise-core"
|
"typewise-core"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"undici-types@6.21.0": {
|
||||||
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="
|
||||||
|
},
|
||||||
"undici-types@7.8.0": {
|
"undici-types@7.8.0": {
|
||||||
"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"
|
"vite@7.0.0_@types+node@24.0.4_picomatch@4.0.2"
|
||||||
|
],
|
||||||
|
"bin": true
|
||||||
|
},
|
||||||
|
"vite-node@3.2.4_@types+node@24.0.4_@types+node@22.15.33": {
|
||||||
|
"integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==",
|
||||||
|
"dependencies": [
|
||||||
|
"cac",
|
||||||
|
"debug",
|
||||||
|
"es-module-lexer",
|
||||||
|
"pathe",
|
||||||
|
"vite@7.0.0_@types+node@24.0.4_picomatch@4.0.2_@types+node@22.15.33"
|
||||||
],
|
],
|
||||||
"bin": true
|
"bin": true
|
||||||
},
|
},
|
||||||
"vite@7.0.0_@types+node@24.0.4_picomatch@4.0.2": {
|
"vite@7.0.0_@types+node@24.0.4_picomatch@4.0.2": {
|
||||||
"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",
|
||||||
"picomatch@4.0.2",
|
"picomatch@4.0.2",
|
||||||
@@ -4910,7 +4996,26 @@
|
|||||||
"fsevents"
|
"fsevents"
|
||||||
],
|
],
|
||||||
"optionalPeers": [
|
"optionalPeers": [
|
||||||
"@types/node"
|
"@types/node@24.0.4"
|
||||||
|
],
|
||||||
|
"bin": true
|
||||||
|
},
|
||||||
|
"vite@7.0.0_@types+node@24.0.4_picomatch@4.0.2_@types+node@22.15.33": {
|
||||||
|
"integrity": "sha512-ixXJB1YRgDIw2OszKQS9WxGHKwLdCsbQNkpJN171udl6szi/rIySHL6/Os3s2+oE4P/FLD4dxg4mD7Wust+u5g==",
|
||||||
|
"dependencies": [
|
||||||
|
"@types/node@22.15.33",
|
||||||
|
"esbuild",
|
||||||
|
"fdir",
|
||||||
|
"picomatch@4.0.2",
|
||||||
|
"postcss",
|
||||||
|
"rollup",
|
||||||
|
"tinyglobby"
|
||||||
|
],
|
||||||
|
"optionalDependencies": [
|
||||||
|
"fsevents"
|
||||||
|
],
|
||||||
|
"optionalPeers": [
|
||||||
|
"@types/node@22.15.33"
|
||||||
],
|
],
|
||||||
"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@3.2.4_vite@7.0.0__@types+node@24.0.4__picomatch@4.0.2_@types+node@24.0.4",
|
||||||
"@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_@types+node@24.0.4_picomatch@4.0.2",
|
||||||
"vite-node",
|
"vite-node@3.2.4_@types+node@24.0.4",
|
||||||
"why-is-node-running"
|
"why-is-node-running"
|
||||||
],
|
],
|
||||||
"optionalPeers": [
|
"optionalPeers": [
|
||||||
"@types/node",
|
"@types/node@24.0.4",
|
||||||
|
"happy-dom"
|
||||||
|
],
|
||||||
|
"bin": true
|
||||||
|
},
|
||||||
|
"vitest@3.2.4_@types+node@24.0.4_happy-dom@17.6.3_vite@7.0.0__@types+node@24.0.4__picomatch@4.0.2_@types+node@22.15.33": {
|
||||||
|
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
|
||||||
|
"dependencies": [
|
||||||
|
"@types/chai",
|
||||||
|
"@types/node@22.15.33",
|
||||||
|
"@vitest/expect",
|
||||||
|
"@vitest/mocker@3.2.4_vite@7.0.0__@types+node@24.0.4__picomatch@4.0.2_@types+node@24.0.4_@types+node@22.15.33",
|
||||||
|
"@vitest/pretty-format",
|
||||||
|
"@vitest/runner",
|
||||||
|
"@vitest/snapshot",
|
||||||
|
"@vitest/spy",
|
||||||
|
"@vitest/utils",
|
||||||
|
"chai",
|
||||||
|
"debug",
|
||||||
|
"expect-type",
|
||||||
|
"happy-dom",
|
||||||
|
"magic-string",
|
||||||
|
"pathe",
|
||||||
|
"picomatch@4.0.2",
|
||||||
|
"std-env",
|
||||||
|
"tinybench",
|
||||||
|
"tinyexec",
|
||||||
|
"tinyglobby",
|
||||||
|
"tinypool",
|
||||||
|
"tinyrainbow",
|
||||||
|
"vite@7.0.0_@types+node@24.0.4_picomatch@4.0.2_@types+node@22.15.33",
|
||||||
|
"vite-node@3.2.4_@types+node@24.0.4_@types+node@22.15.33",
|
||||||
|
"why-is-node-running"
|
||||||
|
],
|
||||||
|
"optionalPeers": [
|
||||||
|
"@types/node@22.15.33",
|
||||||
"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,
|
||||||
|
};
|
||||||
Executable
+1177
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user