Add transport status events (#790)
* Transport status events Add symbol docs Emit transport status events Transport test suite * Review fixes * Remove core dependency * HTTP transport use AbortSignal, error handling in TransportNode * Improve stream handling * Update packages/transport-web-serial/src/transport.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fix linting --------- Co-authored-by: philon- <philon-@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot
philon-
parent
449fb3ac36
commit
d7e32e9b03
@@ -1,17 +1,29 @@
|
||||
import { Readable, Writable } from "node:stream";
|
||||
import type { Types } from "@meshtastic/core";
|
||||
import { Utils } from "@meshtastic/core";
|
||||
import { Types, Utils } from "@meshtastic/core";
|
||||
import { SerialPort } from "serialport";
|
||||
|
||||
/**
|
||||
* Node.js Serial transport for Meshtastic.
|
||||
*
|
||||
* Implements {@link Types.Transport} on top of a Node `SerialPort`.
|
||||
* Use {@link TransportNodeSerial.create} for a convenient factory, or
|
||||
* `new TransportNodeSerial(port)` if you already have an open port.
|
||||
*/
|
||||
export class TransportNodeSerial implements Types.Transport {
|
||||
private readonly _toDevice: WritableStream<Uint8Array>;
|
||||
private readonly _fromDevice: ReadableStream<Types.DeviceOutput>;
|
||||
private fromDeviceController?: ReadableStreamDefaultController<Types.DeviceOutput>;
|
||||
private port: SerialPort | undefined;
|
||||
private pipePromise?: Promise<void>;
|
||||
private abortController: AbortController;
|
||||
private lastStatus: Types.DeviceStatusEnum =
|
||||
Types.DeviceStatusEnum.DeviceDisconnected;
|
||||
private closingByUser = false;
|
||||
|
||||
/**
|
||||
* 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).
|
||||
* @param baudRate - Baud rate for the serial connection (default is 115200).
|
||||
* @returns A promise that resolves with a connected TransportNode instance.
|
||||
*/
|
||||
public static create(
|
||||
@@ -46,23 +58,78 @@ export class TransportNodeSerial implements Types.Transport {
|
||||
this.port = port;
|
||||
this.port.on("error", (err) => {
|
||||
console.error("Serial port connection error:", err);
|
||||
this.emitStatus(Types.DeviceStatusEnum.DeviceDisconnected, "port-error");
|
||||
});
|
||||
this.port.on("close", () => {
|
||||
if (this.closingByUser) {
|
||||
return;
|
||||
}
|
||||
this.emitStatus(Types.DeviceStatusEnum.DeviceDisconnected, "port-closed");
|
||||
});
|
||||
|
||||
const fromDeviceSource = Readable.toWeb(port) as ReadableStream<Uint8Array>;
|
||||
this._fromDevice = fromDeviceSource.pipeThrough(Utils.fromDeviceStream());
|
||||
const transformed = fromDeviceSource.pipeThrough(Utils.fromDeviceStream());
|
||||
|
||||
this.abortController = new AbortController();
|
||||
const controller = this.abortController;
|
||||
|
||||
this._fromDevice = new ReadableStream<Types.DeviceOutput>({
|
||||
start: async (ctrl) => {
|
||||
this.fromDeviceController = ctrl;
|
||||
|
||||
this.emitStatus(Types.DeviceStatusEnum.DeviceConnecting);
|
||||
this.emitStatus(Types.DeviceStatusEnum.DeviceConnected);
|
||||
|
||||
const reader = transformed.getReader();
|
||||
try {
|
||||
while (true) {
|
||||
const { value, done } = await reader.read();
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
ctrl.enqueue(value);
|
||||
}
|
||||
ctrl.close();
|
||||
} catch (error) {
|
||||
if (this.closingByUser) {
|
||||
ctrl.close(); // graceful EOF on user
|
||||
} else {
|
||||
this.emitStatus(
|
||||
Types.DeviceStatusEnum.DeviceDisconnected,
|
||||
"read-error",
|
||||
);
|
||||
ctrl.error(
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
);
|
||||
}
|
||||
try {
|
||||
await transformed.cancel();
|
||||
} catch {}
|
||||
} finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Stream for data going FROM the application TO the Meshtastic device.
|
||||
const toDeviceTransform = Utils.toDeviceStream;
|
||||
this._toDevice = toDeviceTransform.writable;
|
||||
this._toDevice = Utils.toDeviceStream.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<any>`).
|
||||
toDeviceTransform.readable
|
||||
.pipeTo(Writable.toWeb(port) as WritableStream<Uint8Array>)
|
||||
.catch((err) => {
|
||||
console.error("Error piping data to serial port:", err);
|
||||
this.port.close(err as Error);
|
||||
this.pipePromise = Utils.toDeviceStream.readable
|
||||
.pipeTo(Writable.toWeb(port) as WritableStream<Uint8Array>, {
|
||||
signal: controller.signal,
|
||||
})
|
||||
.catch((error) => {
|
||||
if (controller.signal.aborted || this.closingByUser) {
|
||||
return;
|
||||
}
|
||||
console.error("Error piping data to serial port:", error);
|
||||
this.emitStatus(
|
||||
Types.DeviceStatusEnum.DeviceDisconnected,
|
||||
"write-error",
|
||||
);
|
||||
try {
|
||||
this.port?.close();
|
||||
} catch {}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -80,9 +147,35 @@ export class TransportNodeSerial implements Types.Transport {
|
||||
return this._fromDevice;
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.port.close();
|
||||
this.port = undefined;
|
||||
return Promise.resolve();
|
||||
/**
|
||||
* Disconnect from the serial port and emit `DeviceDisconnected("user")`.
|
||||
* Safe to call multiple times.
|
||||
*/
|
||||
async disconnect() {
|
||||
try {
|
||||
this.closingByUser = true;
|
||||
this.emitStatus(Types.DeviceStatusEnum.DeviceDisconnected, "user");
|
||||
|
||||
this.abortController?.abort();
|
||||
await this.pipePromise?.catch(() => {});
|
||||
|
||||
try {
|
||||
this.port?.close();
|
||||
} catch {}
|
||||
} finally {
|
||||
this.port = undefined;
|
||||
this.closingByUser = false;
|
||||
}
|
||||
}
|
||||
|
||||
private emitStatus(next: Types.DeviceStatusEnum, reason?: string): void {
|
||||
if (next === this.lastStatus) {
|
||||
return;
|
||||
}
|
||||
this.lastStatus = next;
|
||||
this.fromDeviceController?.enqueue({
|
||||
type: "status",
|
||||
data: { status: next, reason },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user