@ya-modbus/transport 0.11.2 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/README.md +55 -0
- package/dist/cjs/src/index.d.ts +1 -0
- package/dist/cjs/src/index.d.ts.map +1 -1
- package/dist/cjs/src/index.js +4 -1
- package/dist/cjs/src/index.js.map +1 -1
- package/dist/cjs/src/serial-ports.d.ts +74 -0
- package/dist/cjs/src/serial-ports.d.ts.map +1 -0
- package/dist/cjs/src/serial-ports.js +152 -0
- package/dist/cjs/src/serial-ports.js.map +1 -0
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/src/index.d.ts +1 -0
- package/dist/esm/src/index.d.ts.map +1 -1
- package/dist/esm/src/index.js +1 -0
- package/dist/esm/src/index.js.map +1 -1
- package/dist/esm/src/serial-ports.d.ts +74 -0
- package/dist/esm/src/serial-ports.d.ts.map +1 -0
- package/dist/esm/src/serial-ports.js +147 -0
- package/dist/esm/src/serial-ports.js.map +1 -0
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# [0.12.0](https://github.com/groupsky/ya-modbus/compare/@ya-modbus/transport@0.11.2...@ya-modbus/transport@0.12.0) (2026-10-12)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
- **transport:** list serial ports with their USB ids and locations ([#596](https://github.com/groupsky/ya-modbus/issues/596)) ([0ad4a5a](https://github.com/groupsky/ya-modbus/commit/0ad4a5ae8577154f2b198eec3c019e24ca4b5ca3)), closes [#534](https://github.com/groupsky/ya-modbus/issues/534)
|
|
11
|
+
|
|
6
12
|
## [0.11.2](https://github.com/groupsky/ya-modbus/compare/@ya-modbus/transport@0.11.1...@ya-modbus/transport@0.11.2) (2026-10-12)
|
|
7
13
|
|
|
8
14
|
### Bug Fixes
|
package/README.md
CHANGED
|
@@ -207,6 +207,61 @@ When the first `open()` fails, you can pass its error instead of the client:
|
|
|
207
207
|
error, and opens the port later in the same way. `connection.error` is the error while the
|
|
208
208
|
connection is lost, and `undefined` while it works.
|
|
209
209
|
|
|
210
|
+
## Finding serial ports
|
|
211
|
+
|
|
212
|
+
`listSerialPorts()` lists the serial ports with their USB identity and their location (the
|
|
213
|
+
physical USB port):
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
import { listSerialPorts, matchSerialPorts } from '@ya-modbus/transport'
|
|
217
|
+
|
|
218
|
+
const ports = await listSerialPorts()
|
|
219
|
+
// [
|
|
220
|
+
// {
|
|
221
|
+
// path: '/dev/ttyUSB0',
|
|
222
|
+
// vendorId: '1a86',
|
|
223
|
+
// productId: '7523',
|
|
224
|
+
// locations: [
|
|
225
|
+
// 'pci-0000:00:1a.0-usb-0:1.3:1.0-port0',
|
|
226
|
+
// 'pci-0000:00:1a.0-usbv2-0:1.3:1.0-port0',
|
|
227
|
+
// ],
|
|
228
|
+
// },
|
|
229
|
+
// { path: '/dev/ttyS0', locations: [] },
|
|
230
|
+
// ]
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
- `vendorId` and `productId` are 4 lowercase hex digits. They are missing for ports that are not
|
|
234
|
+
USB adapters.
|
|
235
|
+
- `serialNumber` and `manufacturer` are there only when the adapter reports them, with the value
|
|
236
|
+
as the adapter reports it. Many CH340 adapters have no serial number.
|
|
237
|
+
- `locations` are the names of the links in `/dev/serial/by-path` to the port. Newer systemd
|
|
238
|
+
versions make two links for the same USB port (`...-usb-...` and `...-usbv2-...`). The list is
|
|
239
|
+
empty when the port has no link, for example on other systems than Linux.
|
|
240
|
+
|
|
241
|
+
On Linux the ports are read from `/sys/class/tty`. This starts no other program, so it also works
|
|
242
|
+
in a hardened service. On other systems the list comes from the `serialport` package (through
|
|
243
|
+
`modbus-serial`).
|
|
244
|
+
|
|
245
|
+
In a container, `/sys` shows all serial ports of the host, also the ones that are not passed into
|
|
246
|
+
the container. `/dev/serial/by-path` exists only when it is mounted into the container. Without
|
|
247
|
+
it, `locations` are empty.
|
|
248
|
+
|
|
249
|
+
`matchSerialPorts(ports, match)` returns the ports that match all given values. It does no I/O.
|
|
250
|
+
`vendorId` and `productId` are required. Leave out `serialNumber` or `location` to match any.
|
|
251
|
+
`location` is a name from `locations`, or the full `/dev/serial/by-path/...` path. Several
|
|
252
|
+
identical adapters without a serial number all match, unless `location` selects one of them:
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
const matches = matchSerialPorts(ports, {
|
|
256
|
+
vendorId: '1a86',
|
|
257
|
+
productId: '7523',
|
|
258
|
+
location: 'pci-0000:00:1a.0-usb-0:1.3:1.0-port0',
|
|
259
|
+
})
|
|
260
|
+
if (matches.length === 1) {
|
|
261
|
+
// matches[0].path is the port to open
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
210
265
|
## License
|
|
211
266
|
|
|
212
267
|
GPL-3.0-or-later
|
package/dist/cjs/src/index.d.ts
CHANGED
|
@@ -14,4 +14,5 @@ export { isConnectionLostError, ModbusConnection, type OpenClient } from './conn
|
|
|
14
14
|
export { TransportManager, type TransportManagerOptions, type TransportStats } from './manager.js';
|
|
15
15
|
export { MutexTransport } from './mutex-transport.js';
|
|
16
16
|
export { SlaveTransport } from './slave-transport.js';
|
|
17
|
+
export { listSerialPorts, matchSerialPorts, type SerialPortInfo, type SerialPortMatch, } from './serial-ports.js';
|
|
17
18
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,eAAe,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAA;AACpE,OAAO,EAAE,kBAAkB,EAAE,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAA;AACvE,OAAO,EAAE,kBAAkB,EAAE,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAA;AACvE,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAA;AACpE,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,cAAc,EAAE,KAAK,WAAW,EAAE,MAAM,YAAY,CAAA;AACrF,OAAO,EAAE,oBAAoB,EAAE,KAAK,WAAW,EAAE,MAAM,sBAAsB,CAAA;AAC7E,OAAO,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAC1F,OAAO,EAAE,gBAAgB,EAAE,KAAK,uBAAuB,EAAE,KAAK,cAAc,EAAE,MAAM,cAAc,CAAA;AAClG,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,eAAe,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAA;AACpE,OAAO,EAAE,kBAAkB,EAAE,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAA;AACvE,OAAO,EAAE,kBAAkB,EAAE,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAA;AACvE,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAA;AACpE,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,cAAc,EAAE,KAAK,WAAW,EAAE,MAAM,YAAY,CAAA;AACrF,OAAO,EAAE,oBAAoB,EAAE,KAAK,WAAW,EAAE,MAAM,sBAAsB,CAAA;AAC7E,OAAO,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAC1F,OAAO,EAAE,gBAAgB,EAAE,KAAK,uBAAuB,EAAE,KAAK,cAAc,EAAE,MAAM,cAAc,CAAA;AAClG,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AACrD,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,KAAK,cAAc,EACnB,KAAK,eAAe,GACrB,MAAM,mBAAmB,CAAA"}
|
package/dist/cjs/src/index.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* and transport pooling with mutex-based RTU bus serialization
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
-
exports.SlaveTransport = exports.MutexTransport = exports.TransportManager = exports.ModbusConnection = exports.isConnectionLostError = exports.normalizeModbusError = exports.RETRY_DELAY_MS = exports.MAX_RETRIES = exports.withRetry = exports.createModbusTransport = exports.createTCPTransport = exports.createRTUTransport = exports.createTransport = void 0;
|
|
9
|
+
exports.matchSerialPorts = exports.listSerialPorts = exports.SlaveTransport = exports.MutexTransport = exports.TransportManager = exports.ModbusConnection = exports.isConnectionLostError = exports.normalizeModbusError = exports.RETRY_DELAY_MS = exports.MAX_RETRIES = exports.withRetry = exports.createModbusTransport = exports.createTCPTransport = exports.createRTUTransport = exports.createTransport = void 0;
|
|
10
10
|
var factory_js_1 = require("./factory.js");
|
|
11
11
|
Object.defineProperty(exports, "createTransport", { enumerable: true, get: function () { return factory_js_1.createTransport; } });
|
|
12
12
|
var rtu_transport_js_1 = require("./rtu-transport.js");
|
|
@@ -30,4 +30,7 @@ var mutex_transport_js_1 = require("./mutex-transport.js");
|
|
|
30
30
|
Object.defineProperty(exports, "MutexTransport", { enumerable: true, get: function () { return mutex_transport_js_1.MutexTransport; } });
|
|
31
31
|
var slave_transport_js_1 = require("./slave-transport.js");
|
|
32
32
|
Object.defineProperty(exports, "SlaveTransport", { enumerable: true, get: function () { return slave_transport_js_1.SlaveTransport; } });
|
|
33
|
+
var serial_ports_js_1 = require("./serial-ports.js");
|
|
34
|
+
Object.defineProperty(exports, "listSerialPorts", { enumerable: true, get: function () { return serial_ports_js_1.listSerialPorts; } });
|
|
35
|
+
Object.defineProperty(exports, "matchSerialPorts", { enumerable: true, get: function () { return serial_ports_js_1.matchSerialPorts; } });
|
|
33
36
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;AAEH,2CAAoE;AAA3D,6GAAA,eAAe,OAAA;AACxB,uDAAuE;AAA9D,sHAAA,kBAAkB,OAAA;AAC3B,uDAAuE;AAA9D,sHAAA,kBAAkB,OAAA;AAC3B,2EAAoE;AAA3D,mIAAA,qBAAqB,OAAA;AAC9B,uCAAqF;AAA5E,qGAAA,SAAS,OAAA;AAAE,uGAAA,WAAW,OAAA;AAAE,0GAAA,cAAc,OAAA;AAC/C,2DAA6E;AAApE,0HAAA,oBAAoB,OAAA;AAC7B,iDAA0F;AAAjF,sHAAA,qBAAqB,OAAA;AAAE,iHAAA,gBAAgB,OAAA;AAChD,2CAAkG;AAAzF,8GAAA,gBAAgB,OAAA;AACzB,2DAAqD;AAA5C,oHAAA,cAAc,OAAA;AACvB,2DAAqD;AAA5C,oHAAA,cAAc,OAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;AAEH,2CAAoE;AAA3D,6GAAA,eAAe,OAAA;AACxB,uDAAuE;AAA9D,sHAAA,kBAAkB,OAAA;AAC3B,uDAAuE;AAA9D,sHAAA,kBAAkB,OAAA;AAC3B,2EAAoE;AAA3D,mIAAA,qBAAqB,OAAA;AAC9B,uCAAqF;AAA5E,qGAAA,SAAS,OAAA;AAAE,uGAAA,WAAW,OAAA;AAAE,0GAAA,cAAc,OAAA;AAC/C,2DAA6E;AAApE,0HAAA,oBAAoB,OAAA;AAC7B,iDAA0F;AAAjF,sHAAA,qBAAqB,OAAA;AAAE,iHAAA,gBAAgB,OAAA;AAChD,2CAAkG;AAAzF,8GAAA,gBAAgB,OAAA;AACzB,2DAAqD;AAA5C,oHAAA,cAAc,OAAA;AACvB,2DAAqD;AAA5C,oHAAA,cAAc,OAAA;AACvB,qDAK0B;AAJxB,kHAAA,eAAe,OAAA;AACf,mHAAA,gBAAgB,OAAA"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* List serial ports with their USB identity and physical USB port, and select
|
|
3
|
+
* ports by these values
|
|
4
|
+
*
|
|
5
|
+
* USB serial adapters get a new device name when they are plugged in again
|
|
6
|
+
* (`/dev/ttyUSB0` becomes `/dev/ttyUSB1`). The USB vendor ID, product ID and
|
|
7
|
+
* serial number identify an adapter. Identical adapters without a serial number
|
|
8
|
+
* differ only in the physical USB port, which is the name of their link in
|
|
9
|
+
* `/dev/serial/by-path` (the location).
|
|
10
|
+
*/
|
|
11
|
+
/** A serial port with its USB identity */
|
|
12
|
+
export interface SerialPortInfo {
|
|
13
|
+
/** Device path, for example `/dev/ttyUSB0` */
|
|
14
|
+
path: string;
|
|
15
|
+
/** USB vendor ID as 4 lowercase hex digits, for example `1a86` */
|
|
16
|
+
vendorId?: string;
|
|
17
|
+
/** USB product ID as 4 lowercase hex digits, for example `7523` */
|
|
18
|
+
productId?: string;
|
|
19
|
+
/** USB serial number, when the adapter has one */
|
|
20
|
+
serialNumber?: string;
|
|
21
|
+
/** Manufacturer name, when the adapter has one */
|
|
22
|
+
manufacturer?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Names of the links in `/dev/serial/by-path` to this port, for example
|
|
25
|
+
* `pci-0000:00:1a.0-usb-0:1.3:1.0-port0`. Newer systemd versions add a
|
|
26
|
+
* second link (`...-usbv2-...`) for the same USB port. Empty when the port
|
|
27
|
+
* has no such link (not a USB adapter, or not Linux).
|
|
28
|
+
*/
|
|
29
|
+
locations: string[];
|
|
30
|
+
}
|
|
31
|
+
/** Values to select ports by. Leave out `serialNumber` or `location` to match any. */
|
|
32
|
+
export interface SerialPortMatch {
|
|
33
|
+
/** USB vendor ID in hex, for example `1a86` or `0x1A86` */
|
|
34
|
+
vendorId: string;
|
|
35
|
+
/** USB product ID in hex, for example `7523` */
|
|
36
|
+
productId: string;
|
|
37
|
+
/** USB serial number (case-sensitive) */
|
|
38
|
+
serialNumber?: string;
|
|
39
|
+
/** Name of a link in `/dev/serial/by-path`, or the full path of the link */
|
|
40
|
+
location?: string;
|
|
41
|
+
}
|
|
42
|
+
/** A port as serialport's list or sysfs reports it */
|
|
43
|
+
export interface ListedSerialPort {
|
|
44
|
+
path: string;
|
|
45
|
+
vendorId?: string | undefined;
|
|
46
|
+
productId?: string | undefined;
|
|
47
|
+
serialNumber?: string | undefined;
|
|
48
|
+
manufacturer?: string | undefined;
|
|
49
|
+
}
|
|
50
|
+
/** System access of {@link listSerialPorts}, replaced in tests */
|
|
51
|
+
export interface SerialPortsDeps {
|
|
52
|
+
list: () => Promise<ListedSerialPort[]>;
|
|
53
|
+
readdir: (path: string) => Promise<string[]>;
|
|
54
|
+
realpath: (path: string) => Promise<string>;
|
|
55
|
+
readFile: (path: string) => Promise<string>;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* List the serial ports with their USB identity and locations
|
|
59
|
+
*
|
|
60
|
+
* On Linux the ports are read from sysfs (`/sys/class/tty`). This starts no
|
|
61
|
+
* other program, so it works in a hardened service. Without sysfs (other
|
|
62
|
+
* systems), the port list of serialport is used (through modbus-serial).
|
|
63
|
+
*
|
|
64
|
+
* @param deps - Replacements for the system access, for tests
|
|
65
|
+
*/
|
|
66
|
+
export declare function listSerialPorts(deps?: Partial<SerialPortsDeps>): Promise<SerialPortInfo[]>;
|
|
67
|
+
/**
|
|
68
|
+
* Select the ports that match all given values
|
|
69
|
+
*
|
|
70
|
+
* Returns every match, so the caller can tell a unique match from an
|
|
71
|
+
* ambiguous one. Ports without a USB vendor or product ID never match.
|
|
72
|
+
*/
|
|
73
|
+
export declare function matchSerialPorts(ports: readonly SerialPortInfo[], match: SerialPortMatch): SerialPortInfo[];
|
|
74
|
+
//# sourceMappingURL=serial-ports.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serial-ports.d.ts","sourceRoot":"","sources":["../../../src/serial-ports.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAOH,0CAA0C;AAC1C,MAAM,WAAW,cAAc;IAC7B,8CAA8C;IAC9C,IAAI,EAAE,MAAM,CAAA;IACZ,kEAAkE;IAClE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,mEAAmE;IACnE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,kDAAkD;IAClD,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,kDAAkD;IAClD,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB;;;;;OAKG;IACH,SAAS,EAAE,MAAM,EAAE,CAAA;CACpB;AAED,sFAAsF;AACtF,MAAM,WAAW,eAAe;IAC9B,2DAA2D;IAC3D,QAAQ,EAAE,MAAM,CAAA;IAChB,gDAAgD;IAChD,SAAS,EAAE,MAAM,CAAA;IACjB,yCAAyC;IACzC,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,sDAAsD;AACtD,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC7B,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC9B,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACjC,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAClC;AAED,kEAAkE;AAClE,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAAA;IACvC,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAA;IAC5C,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;IAC3C,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;CAC5C;AAaD;;;;;;;;GAQG;AACH,wBAAsB,eAAe,CACnC,IAAI,GAAE,OAAO,CAAC,eAAe,CAAM,GAClC,OAAO,CAAC,cAAc,EAAE,CAAC,CAI3B;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,SAAS,cAAc,EAAE,EAChC,KAAK,EAAE,eAAe,GACrB,cAAc,EAAE,CAelB"}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* List serial ports with their USB identity and physical USB port, and select
|
|
4
|
+
* ports by these values
|
|
5
|
+
*
|
|
6
|
+
* USB serial adapters get a new device name when they are plugged in again
|
|
7
|
+
* (`/dev/ttyUSB0` becomes `/dev/ttyUSB1`). The USB vendor ID, product ID and
|
|
8
|
+
* serial number identify an adapter. Identical adapters without a serial number
|
|
9
|
+
* differ only in the physical USB port, which is the name of their link in
|
|
10
|
+
* `/dev/serial/by-path` (the location).
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.listSerialPorts = listSerialPorts;
|
|
14
|
+
exports.matchSerialPorts = matchSerialPorts;
|
|
15
|
+
const tslib_1 = require("tslib");
|
|
16
|
+
const promises_1 = require("node:fs/promises");
|
|
17
|
+
const node_path_1 = require("node:path");
|
|
18
|
+
const modbus_serial_1 = tslib_1.__importDefault(require("modbus-serial"));
|
|
19
|
+
const BY_PATH_DIR = '/dev/serial/by-path';
|
|
20
|
+
const SYS_TTY_DIR = '/sys/class/tty';
|
|
21
|
+
const systemDeps = {
|
|
22
|
+
// Loads the optional serialport package only when called
|
|
23
|
+
list: () => modbus_serial_1.default.getPorts(),
|
|
24
|
+
readdir: (path) => (0, promises_1.readdir)(path),
|
|
25
|
+
realpath: (path) => (0, promises_1.realpath)(path),
|
|
26
|
+
readFile: (path) => (0, promises_1.readFile)(path, 'utf8'),
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* List the serial ports with their USB identity and locations
|
|
30
|
+
*
|
|
31
|
+
* On Linux the ports are read from sysfs (`/sys/class/tty`). This starts no
|
|
32
|
+
* other program, so it works in a hardened service. Without sysfs (other
|
|
33
|
+
* systems), the port list of serialport is used (through modbus-serial).
|
|
34
|
+
*
|
|
35
|
+
* @param deps - Replacements for the system access, for tests
|
|
36
|
+
*/
|
|
37
|
+
async function listSerialPorts(deps = {}) {
|
|
38
|
+
const system = { ...systemDeps, ...deps };
|
|
39
|
+
const [ports, locations] = await Promise.all([listPorts(system), readLocations(system)]);
|
|
40
|
+
return ports.map((port) => toPortInfo(port, locations.get(port.path) ?? []));
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Select the ports that match all given values
|
|
44
|
+
*
|
|
45
|
+
* Returns every match, so the caller can tell a unique match from an
|
|
46
|
+
* ambiguous one. Ports without a USB vendor or product ID never match.
|
|
47
|
+
*/
|
|
48
|
+
function matchSerialPorts(ports, match) {
|
|
49
|
+
const vendorId = normalizeUsbId(match.vendorId);
|
|
50
|
+
const productId = normalizeUsbId(match.productId);
|
|
51
|
+
const location = match.location?.startsWith(`${BY_PATH_DIR}/`)
|
|
52
|
+
? match.location.slice(BY_PATH_DIR.length + 1)
|
|
53
|
+
: match.location;
|
|
54
|
+
return ports.filter((port) => vendorId !== undefined &&
|
|
55
|
+
port.vendorId === vendorId &&
|
|
56
|
+
productId !== undefined &&
|
|
57
|
+
port.productId === productId &&
|
|
58
|
+
(match.serialNumber === undefined || port.serialNumber === match.serialNumber) &&
|
|
59
|
+
(location === undefined || port.locations.includes(location)));
|
|
60
|
+
}
|
|
61
|
+
/** `1A86`, `0x1a86` and `1a86` become `1a86`; `403` becomes `0403` */
|
|
62
|
+
function normalizeUsbId(id) {
|
|
63
|
+
const hex = id?.match(/^(?:0x)?([0-9a-f]{1,4})$/i)?.[1];
|
|
64
|
+
return hex?.toLowerCase().padStart(4, '0');
|
|
65
|
+
}
|
|
66
|
+
function toPortInfo(port, locations) {
|
|
67
|
+
const vendorId = normalizeUsbId(port.vendorId);
|
|
68
|
+
const productId = normalizeUsbId(port.productId);
|
|
69
|
+
return {
|
|
70
|
+
path: port.path,
|
|
71
|
+
...(vendorId ? { vendorId } : {}),
|
|
72
|
+
...(productId ? { productId } : {}),
|
|
73
|
+
...(port.serialNumber ? { serialNumber: port.serialNumber } : {}),
|
|
74
|
+
...(port.manufacturer ? { manufacturer: port.manufacturer } : {}),
|
|
75
|
+
locations,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
async function listPorts(system) {
|
|
79
|
+
try {
|
|
80
|
+
return await listSysfsPorts(system);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
if (!isNotFound(error))
|
|
84
|
+
throw error;
|
|
85
|
+
return system.list();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function isNotFound(error) {
|
|
89
|
+
return error.code === 'ENOENT';
|
|
90
|
+
}
|
|
91
|
+
/** Map each device path to the names of its links in /dev/serial/by-path */
|
|
92
|
+
async function readLocations(system) {
|
|
93
|
+
const locations = new Map();
|
|
94
|
+
let names;
|
|
95
|
+
try {
|
|
96
|
+
names = await system.readdir(BY_PATH_DIR);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
// udev creates the directory only while a USB serial adapter is plugged in
|
|
100
|
+
if (isNotFound(error))
|
|
101
|
+
return locations;
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
const sorted = [...names].sort();
|
|
105
|
+
const targets = await Promise.all(sorted.map((name) =>
|
|
106
|
+
// A link points nowhere when its adapter is unplugged while listing
|
|
107
|
+
system.realpath((0, node_path_1.join)(BY_PATH_DIR, name)).catch(() => undefined)));
|
|
108
|
+
sorted.forEach((name, index) => {
|
|
109
|
+
const target = targets[index];
|
|
110
|
+
if (target !== undefined)
|
|
111
|
+
locations.set(target, [...(locations.get(target) ?? []), name]);
|
|
112
|
+
});
|
|
113
|
+
return locations;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* List the ports in /sys/class/tty that belong to a device, and read the USB
|
|
117
|
+
* identity of the USB device above each one
|
|
118
|
+
*/
|
|
119
|
+
async function listSysfsPorts(system) {
|
|
120
|
+
const names = await system.readdir(SYS_TTY_DIR);
|
|
121
|
+
const ports = await Promise.all(names.map(async (name) => {
|
|
122
|
+
// Virtual terminals (tty0, console, ptmx) have no device
|
|
123
|
+
const device = await system.realpath((0, node_path_1.join)(SYS_TTY_DIR, name, 'device')).catch(() => undefined);
|
|
124
|
+
return device === undefined
|
|
125
|
+
? undefined
|
|
126
|
+
: { path: (0, node_path_1.join)('/dev', name), ...(await readUsbIdentity(system, device)) };
|
|
127
|
+
}));
|
|
128
|
+
return ports.filter((port) => port !== undefined);
|
|
129
|
+
}
|
|
130
|
+
/** Walk up from a tty device to the USB device, which has the idVendor file */
|
|
131
|
+
async function readUsbIdentity(system, device) {
|
|
132
|
+
for (let dir = device; dir !== (0, node_path_1.dirname)(dir); dir = (0, node_path_1.dirname)(dir)) {
|
|
133
|
+
const vendorId = await readAttribute(system, dir, 'idVendor');
|
|
134
|
+
if (vendorId === undefined)
|
|
135
|
+
continue;
|
|
136
|
+
return {
|
|
137
|
+
vendorId,
|
|
138
|
+
productId: await readAttribute(system, dir, 'idProduct'),
|
|
139
|
+
serialNumber: await readAttribute(system, dir, 'serial'),
|
|
140
|
+
manufacturer: await readAttribute(system, dir, 'manufacturer'),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
return {};
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Read a sysfs attribute without the newline that sysfs adds, or undefined
|
|
147
|
+
* when the device does not have it
|
|
148
|
+
*/
|
|
149
|
+
function readAttribute(system, dir, name) {
|
|
150
|
+
return system.readFile((0, node_path_1.join)(dir, name)).then((value) => value.replace(/\n$/, ''), () => undefined);
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=serial-ports.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serial-ports.js","sourceRoot":"","sources":["../../../src/serial-ports.ts"],"names":[],"mappings":"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"}
|