From 66b839742a96c80f5618386248cc7a5d4b1408ed Mon Sep 17 00:00:00 2001 From: Dan Ditomaso Date: Wed, 9 Jul 2025 22:02:10 -0400 Subject: [PATCH] Add node-transport lib (#703) * feat: add node transport * updated readme * Update packages/transport-node/README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .gitignore | 3 +- deno.json | 1 + packages/transport-node/README.md | 28 +++++++++ packages/transport-node/deno.json | 8 +++ packages/transport-node/mod.ts | 1 + packages/transport-node/src/transport.ts | 77 ++++++++++++++++++++++++ 6 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 packages/transport-node/README.md create mode 100644 packages/transport-node/deno.json create mode 100644 packages/transport-node/mod.ts create mode 100644 packages/transport-node/src/transport.ts diff --git a/.gitignore b/.gitignore index 76c403ce..9a47b879 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ stats.html .vite dev-dist __screenshots__* -*.diff \ No newline at end of file +*.diff +npm/ \ No newline at end of file diff --git a/deno.json b/deno.json index 2957035a..46ddd09a 100644 --- a/deno.json +++ b/deno.json @@ -3,6 +3,7 @@ "./packages/web", "./packages/core", "./packages/transport-deno", + "./packages/transport-node", "./packages/transport-http", "./packages/transport-web-bluetooth", "./packages/transport-web-serial" diff --git a/packages/transport-node/README.md b/packages/transport-node/README.md new file mode 100644 index 00000000..b0b787f1 --- /dev/null +++ b/packages/transport-node/README.md @@ -0,0 +1,28 @@ +# @meshtastic/transport-node + +[![JSR](https://jsr.io/badges/@meshtastic/transport-node)](https://jsr.io/@meshtastic/transport-node) +[![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-node` Provides TCP transport (Node) for Meshtastic +devices. Installation instructions are available at +[JSR](https://jsr.io/@meshtastic/transport-node) +[NPM](https://www.npmjs.com/package/@meshtastic/transport-node) + +## Usage + +```ts +import { MeshDevice } from "@meshtastic/core"; +import { TransportNode } from "@meshtastic/transport-node"; + +const transport = await TransportNode.create("10.10.0.57"); +const device = new MeshDevice(transport); +``` + +## Stats + +![Alt](https://repobeats.axiom.co/api/embed/5330641586e92a2ec84676fedb98f6d4a7b25d69.svg "Repobeats analytics image") diff --git a/packages/transport-node/deno.json b/packages/transport-node/deno.json new file mode 100644 index 00000000..18f531a6 --- /dev/null +++ b/packages/transport-node/deno.json @@ -0,0 +1,8 @@ +{ + "name": "@meshtastic/transport-node", + "version": "0.0.1", + "description": "NodeJS-specific transport layer for Meshtastic web applications.", + "exports": { + ".": "./mod.ts" + } +} diff --git a/packages/transport-node/mod.ts b/packages/transport-node/mod.ts new file mode 100644 index 00000000..37b6657c --- /dev/null +++ b/packages/transport-node/mod.ts @@ -0,0 +1 @@ +export { TransportNode } from "./src/transport.ts"; diff --git a/packages/transport-node/src/transport.ts b/packages/transport-node/src/transport.ts new file mode 100644 index 00000000..1f984219 --- /dev/null +++ b/packages/transport-node/src/transport.ts @@ -0,0 +1,77 @@ +import { Utils } from "@meshtastic/core"; +import type { Types } from "@meshtastic/core"; +import { Socket } from "node:net"; +import { Readable, Writable } from "node:stream"; + +export class TransportNode implements Types.Transport { + private readonly _toDevice: WritableStream; + private readonly _fromDevice: ReadableStream; + + /** + * Creates and connects a new TransportNode instance. + * @param hostname - The IP address or hostname of the Meshtastic device. + * @param port - The port number for the TCP connection (defaults to 4403). + * @returns A promise that resolves with a connected TransportNode instance. + */ + public static create(hostname: string, port = 4403): Promise { + return new Promise((resolve, reject) => { + const socket = new Socket(); + + const onError = (err: Error) => { + socket.destroy(); + reject(err); + }; + + socket.once("error", onError); + + socket.connect(port, hostname, () => { + socket.removeListener("error", onError); + resolve(new TransportNode(socket)); + }); + }); + } + + /** + * Constructs a new TransportNode. + * @param connection - An active Node.js net.Socket connection. + */ + constructor(connection: Socket) { + connection.on("error", (err) => { + console.error("Socket connection error:", err); + }); + + const fromDeviceSource = Readable.toWeb(connection) as ReadableStream< + Uint8Array + >; + this._fromDevice = fromDeviceSource.pipeThrough(Utils.fromDeviceStream()); + + // Stream for data going FROM the application TO the Meshtastic device. + const toDeviceTransform = new TransformStream(); + this._toDevice = toDeviceTransform.writable; + + // The readable end of the transform is then piped to the Node.js socket. + // A similar assertion is needed here because `Writable.toWeb` also returns + // a generically typed stream (`WritableStream`). + toDeviceTransform.readable.pipeTo( + Writable.toWeb(connection) as WritableStream, + ) + .catch((err) => { + console.error("Error piping data to socket:", err); + connection.destroy(err as Error); + }); + } + + /** + * The WritableStream to send data to the Meshtastic device. + */ + public get toDevice(): WritableStream { + return this._toDevice; + } + + /** + * The ReadableStream to receive data from the Meshtastic device. + */ + public get fromDevice(): ReadableStream { + return this._fromDevice; + } +}