Browse Source

feat: move js repo into web monorepo

pull/683/head
Dan Ditomaso 1 year ago
parent
commit
f28ed0bc54
  1. 37
      .github/workflows/js-ci.yml
  2. 3
      .gitmodules
  3. 15
      deno.json
  4. 200
      deno.lock
  5. 26
      packages/core/README.md
  6. 10
      packages/core/deno.json
  7. 5
      packages/core/mod.ts
  8. 10
      packages/core/src/constants.ts
  9. 1177
      packages/core/src/meshDevice.ts
  10. 128
      packages/core/src/types.ts
  11. 467
      packages/core/src/utils/eventSystem.ts
  12. 5
      packages/core/src/utils/mod.ts
  13. 119
      packages/core/src/utils/queue.ts
  14. 222
      packages/core/src/utils/transform/decodePacket.ts
  15. 73
      packages/core/src/utils/transform/fromDevice.ts
  16. 16
      packages/core/src/utils/transform/toDevice.ts
  17. 135
      packages/core/src/utils/xmodem.ts
  18. 27
      packages/transport-deno/README.md
  19. 7
      packages/transport-deno/deno.json
  20. 1
      packages/transport-deno/mod.ts
  21. 32
      packages/transport-deno/src/transport.ts
  22. 27
      packages/transport-http/README.md
  23. 7
      packages/transport-http/deno.json
  24. 1
      packages/transport-http/mod.ts
  25. 89
      packages/transport-http/src/transport.ts
  26. 34
      packages/transport-web-bluetooth/README.md
  27. 15
      packages/transport-web-bluetooth/deno.json
  28. 1
      packages/transport-web-bluetooth/mod.ts
  29. 134
      packages/transport-web-bluetooth/src/transport.ts
  30. 34
      packages/transport-web-serial/README.md
  31. 15
      packages/transport-web-serial/deno.json
  32. 1
      packages/transport-web-serial/mod.ts
  33. 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

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