From 0b9ebade386d2475682a392927c273f8dc3a9b72 Mon Sep 17 00:00:00 2001 From: Henri Bergius Date: Thu, 14 Aug 2025 21:56:34 -0400 Subject: [PATCH] Initial Node.js serial transport (#779) * Initial Node.js serial transport * Minor doc fixes * Add serialport to lockfile * Typo fix * Fix link: --- packages/transport-node-serial/README.md | 28 ++++++ packages/transport-node-serial/mod.ts | 1 + packages/transport-node-serial/package.json | 39 +++++++++ .../transport-node-serial/src/transport.ts | 87 +++++++++++++++++++ packages/transport-node-serial/tsconfig.json | 18 ++++ pnpm-lock.yaml | 9 ++ 6 files changed, 182 insertions(+) create mode 100644 packages/transport-node-serial/README.md create mode 100644 packages/transport-node-serial/mod.ts create mode 100644 packages/transport-node-serial/package.json create mode 100644 packages/transport-node-serial/src/transport.ts create mode 100644 packages/transport-node-serial/tsconfig.json diff --git a/packages/transport-node-serial/README.md b/packages/transport-node-serial/README.md new file mode 100644 index 00000000..39694a8b --- /dev/null +++ b/packages/transport-node-serial/README.md @@ -0,0 +1,28 @@ +# @meshtastic/transport-node-serial + +[![JSR](https://jsr.io/badges/@meshtastic/transport-node-serial)](https://jsr.io/@meshtastic/transport-node-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-noden-node` Provides Serial transport (Node) for Meshtastic +devices. Installation instructions are available at +[JSR](https://jsr.io/@meshtastic/transport-node-serial) +[NPM](https://www.npmjs.com/package/@meshtastic/transport-node-serial) + +## Usage + +```ts +import { MeshDevice } from "@meshtastic/core"; +import { TransportNodeSerial } from "@meshtastic/transport-node-serial"; + +const transport = await TransportNodeSerial.create("/dev/cu.usbserial-0001"); +const device = new MeshDevice(transport); +``` + +## Stats + +![Alt](https://repobeats.axiom.co/api/embed/5330641586e92a2ec84676fedb98f6d4a7b25d69.svg "Repobeats analytics image") diff --git a/packages/transport-node-serial/mod.ts b/packages/transport-node-serial/mod.ts new file mode 100644 index 00000000..5aef1978 --- /dev/null +++ b/packages/transport-node-serial/mod.ts @@ -0,0 +1 @@ +export { TransportNodeSerial } from "./src/transport.ts"; diff --git a/packages/transport-node-serial/package.json b/packages/transport-node-serial/package.json new file mode 100644 index 00000000..2da57d18 --- /dev/null +++ b/packages/transport-node-serial/package.json @@ -0,0 +1,39 @@ +{ + "name": "@meshtastic/transport-node-serial", + "version": "0.0.1", + "description": "NodeJS-specific serial transport layer for Meshtastic web applications.", + "exports": { + ".": "./dist/mod.mjs" + }, + "main": "./dist/mod.mjs", + "module": "./dist/mod.mjs", + "types": "./dist/mod.d.mts", + + "license": "GPL-3.0-only", + "tsdown": { + "entry": "mod.ts", + "dts": true, + "format": ["esm"], + "splitting": false, + "clean": true + }, + "files": [ + "package.json", + "README.md", + "LICENSE", + "dist" + ], + "scripts": { + "preinstall": "npx only-allow pnpm", + "prepack": "cp ../../LICENSE ./LICENSE", + "clean": "rm -rf dist LICENSE", + "build:npm": "tsdown", + "publish:npm": "pnpm clean && pnpm build:npm && pnpm publish --access public", + "prepare:jsr": "rm -rf dist && pnpm dlx pkg-to-jsr", + "publish:jsr": "pnpm run prepack && pnpm prepare:jsr && deno publish --allow-dirty --no-check" + }, + "dependencies": { + "@meshtastic/core": "workspace:*", + "serialport": "^13.0.0" + } +} diff --git a/packages/transport-node-serial/src/transport.ts b/packages/transport-node-serial/src/transport.ts new file mode 100644 index 00000000..02f439e2 --- /dev/null +++ b/packages/transport-node-serial/src/transport.ts @@ -0,0 +1,87 @@ +import { Readable, Writable } from "node:stream"; +import type { Types } from "@meshtastic/core"; +import { Utils } from "@meshtastic/core"; +import { SerialPort } from "serialport"; + +export class TransportNodeSerial implements Types.Transport { + private readonly _toDevice: WritableStream; + private readonly _fromDevice: ReadableStream; + private port: SerialPort | undefined; + + /** + * Creates and connects a new TransportNode instance. + * @param path - Path to the serial device + * @param baudRate - The port number for the TCP connection (defaults to 4403). + * @returns A promise that resolves with a connected TransportNode instance. + */ + public static create(path: string, baudRate = 115200): Promise { + return new Promise((resolve, reject) => { + const port = new SerialPort({ + path, + baudRate, + autoOpen: true, + }); + + const onError = (err: Error) => { + port.close(); + reject(err); + }; + + port.once("error", onError); + port.on("open", () => { + port.removeListener("error", onError); + resolve(new TransportNodeSerial(port)); + }); + }); + } + + /** + * Constructs a new TransportNode. + * @param port - An active Node.js SerialPort connection. + */ + constructor(port: SerialPort) { + this.port = port; + this.port.on("error", (err) => { + console.error("Serial port connection error:", err); + }); + + const fromDeviceSource = Readable.toWeb( + port, + ) as ReadableStream; + this._fromDevice = fromDeviceSource.pipeThrough(Utils.fromDeviceStream()); + + // Stream for data going FROM the application TO the Meshtastic device. + const toDeviceTransform = Utils.toDeviceStream; + this._toDevice = toDeviceTransform.writable; + + // The readable end of the transform is then piped to the Node.js SerialPort connection. + // A similar assertion is needed here because `Writable.toWeb` also returns + // a generically typed stream (`WritableStream`). + toDeviceTransform.readable + .pipeTo(Writable.toWeb(port) as WritableStream) + .catch((err) => { + console.error("Error piping data to serial port:", err); + this.port.close(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; + } + + disconnect() { + this.port.close(); + this.port = undefined; + return Promise.resolve(); + } +} diff --git a/packages/transport-node-serial/tsconfig.json b/packages/transport-node-serial/tsconfig.json new file mode 100644 index 00000000..8ec7c702 --- /dev/null +++ b/packages/transport-node-serial/tsconfig.json @@ -0,0 +1,18 @@ + { + "extends": "../../tsconfig.json", + "compilerOptions": { + "module": "ESNext", + "target": "ES2020", + "declaration": true, + "outDir": "./dist", + "moduleResolution": "bundler", + "emitDeclarationOnly": false, + "esModuleInterop": true, + }, + "include": ["src"] +} + + + + + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8859ac33..73e2d278 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -61,6 +61,15 @@ importers: specifier: workspace:* version: link:../core + packages/transport-node-serial: + dependencies: + '@meshtastic/core': + specifier: workspace:* + version: link:../core + 'serialport': + specifier: npm:serialport@^13.0.0 + version: 13.0.0 + packages/transport-web-bluetooth: dependencies: '@meshtastic/core':