Skip to content

Commit

Permalink
refactor: move listing ports into serial host binding
Browse files Browse the repository at this point in the history
  • Loading branch information
AlCalzone committed Dec 11, 2024
1 parent b9c7247 commit f6d447c
Show file tree
Hide file tree
Showing 10 changed files with 174 additions and 80 deletions.
5 changes: 5 additions & 0 deletions packages/serial/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@
"import": "./build/esm/index_mock.js",
"require": "./build/cjs/index_mock.js"
},
"./bindings/*": {
"@@dev": "./src/bindings/*.ts",
"import": "./build/esm/bindings/*.js",
"require": "./build/cjs/bindings/*.js"
},
"./package.json": "./package.json"
},
"keywords": [],
Expand Down
60 changes: 60 additions & 0 deletions packages/serial/src/bindings/node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { SerialPort } from "serialport";
import { type EnumeratedPort, type Serial } from "../serialport/Bindings.js";
import { createNodeSerialPortFactory } from "../serialport/NodeSerialPort.js";
import { createNodeSocketFactory } from "../serialport/NodeSocket.js";

/** An implementation of the Serial bindings for Node.js */
export const serial: Serial = {
createFactoryByPath(path) {
if (path.startsWith("tcp://")) {
const url = new URL(path);
return Promise.resolve(createNodeSocketFactory({
host: url.hostname,
port: parseInt(url.port),
}));
} else {
return Promise.resolve(createNodeSerialPortFactory(
path,
));
}
},

async list() {
// Put symlinks to the serial ports first if possible
const ret: EnumeratedPort[] = [];
if (os.platform() === "linux") {
const dir = "/dev/serial/by-id";
const symlinks = await fs.readdir(dir).catch(() => []);

for (const l of symlinks) {
try {
const fullPath = path.join(dir, l);
const target = path.join(
dir,
await fs.readlink(fullPath),
);
if (!target.startsWith("/dev/tty")) continue;

ret.push({
type: "link",
path: fullPath,
});
} catch {
// Ignore. The target might not exist or we might not have access.
}
}
}

// Then the actual serial ports
const ports = await SerialPort.list();
ret.push(...ports.map((port) => ({
type: "tty" as const,
path: port.path,
})));

return ret;
},
};
1 change: 1 addition & 0 deletions packages/serial/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export * from "./parsers/SerialAPIParser.js";
export * from "./parsers/ZWaveSerialFrame.js";
export * from "./parsers/ZnifferSerialFrame.js";
export * from "./plumbing/Faucet.js";
export type * from "./serialport/Bindings.js";
export * from "./serialport/LegacyBindingWrapper.js";
export * from "./serialport/NodeSerialPort.js";
export * from "./serialport/NodeSocket.js";
Expand Down
1 change: 1 addition & 0 deletions packages/serial/src/index_safe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export type { SerialLogContext } from "./log/Logger_safe.js";
export * from "./message/Constants.js";
export * from "./message/MessageHeaders.js";
export * from "./message/SuccessIndicator.js";
export type * from "./serialport/Bindings.js";
24 changes: 24 additions & 0 deletions packages/serial/src/serialport/Bindings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { type ZWaveSerialBindingFactory } from "./ZWaveSerialStream.js";

export type EnumeratedPort = {
type: "link";
path: string;
} | {
type: "tty";
path: string;
} | {
type: "socket";
path: string;
} | {
type: "custom";
factory: ZWaveSerialBindingFactory;
};

/** Abstractions to interact with serial ports on different platforms */
export interface Serial {
/** Create a binding factory from the given path, if supported by the platform */
createFactoryByPath?: (path: string) => Promise<ZWaveSerialBindingFactory>;

/** List the available serial ports, if supported by the platform */
list?: () => Promise<EnumeratedPort[]>;
}
6 changes: 0 additions & 6 deletions packages/shared/src/bindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,3 @@ export interface FileSystem
{}

export type Platform = "linux" | "darwin" | "win32" | "browser" | "other";

/** Abstractions for a host system Z-Wave JS is running on */
export interface Host {
fs: FileSystem;
platform: Platform;
}
116 changes: 55 additions & 61 deletions packages/zwave-js/src/lib/driver/Driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ import {
import {
type BootloaderChunk,
BootloaderChunkType,
type EnumeratedPort,
FunctionType,
type HasNodeId,
Message,
Expand All @@ -131,8 +132,6 @@ import {
type ZWaveSerialPortImplementation,
type ZWaveSerialStream,
ZWaveSerialStreamFactory,
createNodeSerialPortFactory,
createNodeSocketFactory,
getDefaultPriority,
hasNodeId,
isSuccessIndicator,
Expand Down Expand Up @@ -165,6 +164,17 @@ import {
isSendDataTransmitReport,
isTransmitReport,
} from "@zwave-js/serial/serialapi";
import {
SendTestFrameRequest,
SendTestFrameTransmitReport,
} from "@zwave-js/serial/serialapi";
import {
type CommandRequest,
type ContainsCC,
containsCC,
containsSerializedCC,
isCommandRequest,
} from "@zwave-js/serial/serialapi";
import {
AsyncQueue,
Bytes,
Expand All @@ -182,18 +192,20 @@ import {
num2hex,
pick,
} from "@zwave-js/shared";
import {
type ReadFile,
type ReadFileSystemInfo,
} from "@zwave-js/shared/bindings";
import { distinct } from "alcalzone-shared/arrays";
import { wait } from "alcalzone-shared/async";
import {
type DeferredPromise,
createDeferredPromise,
} from "alcalzone-shared/deferred-promise";
import { isArray, isObject } from "alcalzone-shared/typeguards";
import fs from "node:fs/promises";
import os from "node:os";
import * as util from "node:util";
import path from "pathe";
import { SerialPort } from "serialport";
import { PACKAGE_NAME, PACKAGE_VERSION } from "../_version.js";
import { ZWaveController } from "../controller/Controller.js";
import { InclusionState, RemoveNodeReason } from "../controller/Inclusion.js";
import { DriverLogger } from "../log/Driver.js";
Expand All @@ -206,23 +218,6 @@ import {
type ZWaveNotificationCallback,
zWaveNodeEvents,
} from "../node/_Types.js";

import {
SendTestFrameRequest,
SendTestFrameTransmitReport,
} from "@zwave-js/serial/serialapi";
import {
type CommandRequest,
type ContainsCC,
containsCC,
containsSerializedCC,
isCommandRequest,
} from "@zwave-js/serial/serialapi";
import {
type ReadFile,
type ReadFileSystemInfo,
} from "@zwave-js/shared/bindings";
import { PACKAGE_NAME, PACKAGE_VERSION } from "../_version.js";
import { type ZWaveNodeBase } from "../node/mixins/00_Base.js";
import { type NodeWakeup } from "../node/mixins/30_Wakeup.js";
import { type NodeValues } from "../node/mixins/40_Values.js";
Expand Down Expand Up @@ -1226,44 +1221,38 @@ export class Driver extends TypedEventTarget<DriverEventCallbacks>
local?: boolean;
remote?: boolean;
} = {}): Promise<string[]> {
const symlinkedPorts: string[] = [];
const localPorts: string[] = [];
const remotePorts: string[] = [];
// FIXME: Move this into the serial bindings
if (local) {
// Put symlinks to the serial ports first if possible
if (os.platform() === "linux") {
const dir = "/dev/serial/by-id";
const symlinks = await fs.readdir(dir).catch(() => []);

for (const l of symlinks) {
try {
const fullPath = path.join(dir, l);
const target = path.join(
dir,
await fs.readlink(fullPath),
);
if (!target.startsWith("/dev/tty")) continue;
const ret: (EnumeratedPort & { path: string })[] = [];

symlinkedPorts.push(fullPath);
} catch {
// Ignore. The target might not exist or we might not have access.
}
}
}
// Ideally we'd use the host bindings used by the driver, but we can't access them in a static method

// Then the actual serial ports
const ports = await SerialPort.list();
localPorts.push(...ports.map((port) => port.path));
const bindings =
(await import("@zwave-js/serial/bindings/node")).serial;
if (local && typeof bindings.list === "function") {
for (const port of await bindings.list()) {
if (port.type === "custom") continue;
ret.push(port);
}
}
if (remote) {
const ports = await discoverRemoteSerialPorts();
if (ports) {
remotePorts.push(...ports.map((p) => p.port));
ret.push(...ports.map((p) => ({
type: "socket" as const,
path: p.port,
})));
}
}

return distinct([...symlinkedPorts, ...remotePorts, ...localPorts]);
const portOrder: EnumeratedPort["type"][] = ["link", "socket", "tty"];

ret.sort((a, b) => {
const typeA = portOrder.indexOf(a.type);
const typeB = portOrder.indexOf(b.type);
if (typeA !== typeB) return typeA - typeB;
return a.path.localeCompare(b.path);
});

return distinct(ret.map((p) => p.path));
}

/** Updates a subset of the driver options on the fly */
Expand Down Expand Up @@ -1341,6 +1330,8 @@ export class Driver extends TypedEventTarget<DriverEventCallbacks>
this.bindings = {
fs: this._options.host?.fs
?? (await import("@zwave-js/core/bindings/fs/node")).fs,
serial: this._options.host?.serial
?? (await import("@zwave-js/serial/bindings/node")).serial,
};

const spOpenPromise = createDeferredPromise();
Expand All @@ -1355,19 +1346,22 @@ export class Driver extends TypedEventTarget<DriverEventCallbacks>
// Open the serial port
let binding: ZWaveSerialBindingFactory;
if (typeof this.port === "string") {
if (this.port.startsWith("tcp://")) {
const url = new URL(this.port);
this.driverLog.print(`opening serial port ${this.port}`);
binding = createNodeSocketFactory({
host: url.hostname,
port: parseInt(url.port),
});
} else {
if (
typeof this.bindings.serial.createFactoryByPath === "function"
) {
this.driverLog.print(`opening serial port ${this.port}`);
binding = createNodeSerialPortFactory(
binding = await this.bindings.serial.createFactoryByPath(
this.port,
// this._options.testingHooks?.serialPortBinding,
);
} else {
spOpenPromise.reject(
new ZWaveError(
"This platform does not support creating a serial connection by path",
ZWaveErrorCodes.Driver_Failed,
),
);
void this.destroy();
return;
}
} else if (isZWaveSerialPortImplementation(this.port)) {
this.driverLog.print(
Expand Down
11 changes: 11 additions & 0 deletions packages/zwave-js/src/lib/driver/Host.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// FIXME: This should eventually live in @zwave-js/host

import { type Serial } from "@zwave-js/serial";
import { type FileSystem, type Platform } from "@zwave-js/shared/bindings";

/** Abstractions for a host system Z-Wave JS is running on */
export interface Host {
fs: FileSystem;
platform: Platform;
serial: Serial;
}
7 changes: 6 additions & 1 deletion packages/zwave-js/src/lib/driver/ZWaveOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type {
LongRangeChannel,
RFRegion,
} from "@zwave-js/core";
import { type ZWaveSerialStream } from "@zwave-js/serial";
import { type Serial, type ZWaveSerialStream } from "@zwave-js/serial";
import { type DeepPartial, type Expand } from "@zwave-js/shared";
import type { FileSystem } from "@zwave-js/shared/bindings";
import type {
Expand Down Expand Up @@ -132,6 +132,11 @@ export interface ZWaveOptions {
* reading or writing the cache, or loading device configuration files.
*/
fs?: FileSystem;

/**
* Specifies which bindings are used interact with serial ports.
*/
serial?: Serial;
};

storage: {
Expand Down
23 changes: 11 additions & 12 deletions packages/zwave-js/src/lib/zniffer/Zniffer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,6 @@ import {
ZnifferStartResponse,
ZnifferStopRequest,
ZnifferStopResponse,
createNodeSerialPortFactory,
createNodeSocketFactory,
isZWaveSerialPortImplementation,
wrapLegacySerialBinding,
} from "@zwave-js/serial";
Expand Down Expand Up @@ -355,23 +353,24 @@ export class Zniffer extends TypedEventTarget<ZnifferEventCallbacks> {
this.bindings = {
fs: this._options.host?.fs
?? (await import("@zwave-js/core/bindings/fs/node")).fs,
serial: this._options.host?.serial
?? (await import("@zwave-js/serial/bindings/node")).serial,
};

// Open the serial port
let binding: ZWaveSerialBindingFactory;
if (typeof this.port === "string") {
if (this.port.startsWith("tcp://")) {
const url = new URL(this.port);
this.znifferLog.print(`opening serial port ${this.port}`);
binding = createNodeSocketFactory({
host: url.hostname,
port: parseInt(url.port),
});
} else {
if (
typeof this.bindings.serial.createFactoryByPath === "function"
) {
this.znifferLog.print(`opening serial port ${this.port}`);
binding = createNodeSerialPortFactory(
binding = await this.bindings.serial.createFactoryByPath(
this.port,
// this._options.testingHooks?.serialPortBinding,
);
} else {
throw new ZWaveError(
"This platform does not support creating a serial connection by path",
ZWaveErrorCodes.Driver_Failed,
);
}
} else if (isZWaveSerialPortImplementation(this.port)) {
Expand Down

0 comments on commit f6d447c

Please sign in to comment.