node-ch347 0.0.1
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/LICENSE +24 -0
- package/README.md +234 -0
- package/dist/config.d.ts +20 -0
- package/dist/config.js +110 -0
- package/dist/constants.d.ts +83 -0
- package/dist/constants.js +107 -0
- package/dist/flash.d.ts +134 -0
- package/dist/flash.js +597 -0
- package/dist/gpio.d.ts +67 -0
- package/dist/gpio.js +196 -0
- package/dist/index.d.ts +149 -0
- package/dist/index.js +222 -0
- package/dist/spi.d.ts +66 -0
- package/dist/spi.js +227 -0
- package/dist/types.d.ts +74 -0
- package/dist/types.js +45 -0
- package/dist/uart.d.ts +81 -0
- package/dist/uart.js +343 -0
- package/dist/usb.d.ts +80 -0
- package/dist/usb.js +422 -0
- package/package.json +49 -0
package/dist/usb.js
ADDED
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* CH347 USB Communication Layer
|
|
4
|
+
*/
|
|
5
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
6
|
+
if (k2 === undefined) k2 = k;
|
|
7
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
8
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
9
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
10
|
+
}
|
|
11
|
+
Object.defineProperty(o, k2, desc);
|
|
12
|
+
}) : (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
o[k2] = m[k];
|
|
15
|
+
}));
|
|
16
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
17
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
18
|
+
}) : function(o, v) {
|
|
19
|
+
o["default"] = v;
|
|
20
|
+
});
|
|
21
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
22
|
+
var ownKeys = function(o) {
|
|
23
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
24
|
+
var ar = [];
|
|
25
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
26
|
+
return ar;
|
|
27
|
+
};
|
|
28
|
+
return ownKeys(o);
|
|
29
|
+
};
|
|
30
|
+
return function (mod) {
|
|
31
|
+
if (mod && mod.__esModule) return mod;
|
|
32
|
+
var result = {};
|
|
33
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
34
|
+
__setModuleDefault(result, mod);
|
|
35
|
+
return result;
|
|
36
|
+
};
|
|
37
|
+
})();
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.CH347USB = void 0;
|
|
40
|
+
const usb = __importStar(require("usb"));
|
|
41
|
+
const fs = __importStar(require("fs"));
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
const constants_1 = require("./constants");
|
|
44
|
+
class CH347USB {
|
|
45
|
+
device = null;
|
|
46
|
+
interface = null;
|
|
47
|
+
epIn = null;
|
|
48
|
+
epOut = null;
|
|
49
|
+
isOpen = false;
|
|
50
|
+
/**
|
|
51
|
+
* Get the underlying USB device (for advanced operations)
|
|
52
|
+
*/
|
|
53
|
+
getDevice() {
|
|
54
|
+
return this.device;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* List all connected CH347 devices
|
|
58
|
+
*/
|
|
59
|
+
static listDevices() {
|
|
60
|
+
const devices = [];
|
|
61
|
+
const allDevices = usb.getDeviceList();
|
|
62
|
+
for (const device of allDevices) {
|
|
63
|
+
const desc = device.deviceDescriptor;
|
|
64
|
+
if (desc.idVendor === constants_1.CH347_VID &&
|
|
65
|
+
(desc.idProduct === constants_1.CH347_PID_SPI_I2C_UART ||
|
|
66
|
+
desc.idProduct === constants_1.CH347_PID_JTAG_I2C_UART)) {
|
|
67
|
+
const info = {
|
|
68
|
+
vendorId: desc.idVendor,
|
|
69
|
+
productId: desc.idProduct,
|
|
70
|
+
busNumber: device.busNumber,
|
|
71
|
+
deviceAddress: device.deviceAddress,
|
|
72
|
+
};
|
|
73
|
+
// Note: Getting string descriptors requires opening the device
|
|
74
|
+
// which may fail without proper permissions. We skip this for now
|
|
75
|
+
// and just return basic device info.
|
|
76
|
+
devices.push(info);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return devices;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Open connection to CH347 device
|
|
83
|
+
*/
|
|
84
|
+
async open(deviceIndex = 0) {
|
|
85
|
+
const devices = CH347USB.listDevices();
|
|
86
|
+
if (devices.length === 0) {
|
|
87
|
+
throw new Error('No CH347 device found');
|
|
88
|
+
}
|
|
89
|
+
if (deviceIndex >= devices.length) {
|
|
90
|
+
throw new Error(`Device index ${deviceIndex} out of range (${devices.length} devices found)`);
|
|
91
|
+
}
|
|
92
|
+
const targetDevice = devices[deviceIndex];
|
|
93
|
+
const allDevices = usb.getDeviceList();
|
|
94
|
+
// Find the matching device
|
|
95
|
+
this.device = allDevices.find((d) => d.busNumber === targetDevice.busNumber &&
|
|
96
|
+
d.deviceAddress === targetDevice.deviceAddress) ?? null;
|
|
97
|
+
if (!this.device) {
|
|
98
|
+
throw new Error('Failed to find target device');
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
this.device.open();
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
105
|
+
throw new Error(`Failed to open device: ${message}`);
|
|
106
|
+
}
|
|
107
|
+
// Claim interface 2 for SPI/I2C/GPIO
|
|
108
|
+
try {
|
|
109
|
+
this.interface = this.device.interface(constants_1.CH347_IFACE_SPI_I2C_GPIO);
|
|
110
|
+
// On Linux, we may need to detach the kernel driver
|
|
111
|
+
if (this.interface.isKernelDriverActive()) {
|
|
112
|
+
this.interface.detachKernelDriver();
|
|
113
|
+
}
|
|
114
|
+
this.interface.claim();
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
118
|
+
this.device.close();
|
|
119
|
+
throw new Error(`Failed to claim interface: ${message}`);
|
|
120
|
+
}
|
|
121
|
+
// Find endpoints
|
|
122
|
+
for (const endpoint of this.interface.endpoints) {
|
|
123
|
+
if (endpoint.address === constants_1.CH347_EP_IN) {
|
|
124
|
+
this.epIn = endpoint;
|
|
125
|
+
}
|
|
126
|
+
else if (endpoint.address === constants_1.CH347_EP_OUT) {
|
|
127
|
+
this.epOut = endpoint;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (!this.epIn || !this.epOut) {
|
|
131
|
+
this.close();
|
|
132
|
+
throw new Error('Failed to find USB endpoints');
|
|
133
|
+
}
|
|
134
|
+
this.isOpen = true;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Close connection
|
|
138
|
+
*/
|
|
139
|
+
close() {
|
|
140
|
+
if (this.interface) {
|
|
141
|
+
try {
|
|
142
|
+
this.interface.release(true);
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// Ignore
|
|
146
|
+
}
|
|
147
|
+
this.interface = null;
|
|
148
|
+
}
|
|
149
|
+
if (this.device) {
|
|
150
|
+
try {
|
|
151
|
+
this.device.close();
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
// Ignore
|
|
155
|
+
}
|
|
156
|
+
this.device = null;
|
|
157
|
+
}
|
|
158
|
+
this.epIn = null;
|
|
159
|
+
this.epOut = null;
|
|
160
|
+
this.isOpen = false;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Check if device is open
|
|
164
|
+
*/
|
|
165
|
+
isConnected() {
|
|
166
|
+
return this.isOpen;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Send data to device
|
|
170
|
+
*/
|
|
171
|
+
async write(data) {
|
|
172
|
+
if (!this.isOpen || !this.epOut) {
|
|
173
|
+
throw new Error('Device not open');
|
|
174
|
+
}
|
|
175
|
+
return new Promise((resolve, reject) => {
|
|
176
|
+
this.epOut.transfer(data, (err, actual) => {
|
|
177
|
+
if (err) {
|
|
178
|
+
reject(new Error(`USB write error: ${err.message}`));
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
resolve(actual ?? data.length);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Read data from device
|
|
188
|
+
*/
|
|
189
|
+
async read(length = constants_1.CH347_PACKET_SIZE, timeout = constants_1.CH347_TIMEOUT_MS) {
|
|
190
|
+
if (!this.isOpen || !this.epIn) {
|
|
191
|
+
throw new Error('Device not open');
|
|
192
|
+
}
|
|
193
|
+
return new Promise((resolve, reject) => {
|
|
194
|
+
const timeoutId = setTimeout(() => {
|
|
195
|
+
reject(new Error('USB read timeout'));
|
|
196
|
+
}, timeout);
|
|
197
|
+
this.epIn.transfer(length, (err, data) => {
|
|
198
|
+
clearTimeout(timeoutId);
|
|
199
|
+
if (err) {
|
|
200
|
+
reject(new Error(`USB read error: ${err.message}`));
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
resolve(data ?? Buffer.alloc(0));
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Write and then read response
|
|
210
|
+
*/
|
|
211
|
+
async transfer(outData, readLength = constants_1.CH347_PACKET_SIZE) {
|
|
212
|
+
await this.write(outData);
|
|
213
|
+
return this.read(readLength);
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Bulk write for large data transfers
|
|
217
|
+
*/
|
|
218
|
+
async bulkWrite(data, chunkSize = constants_1.CH347_PACKET_SIZE) {
|
|
219
|
+
let offset = 0;
|
|
220
|
+
while (offset < data.length) {
|
|
221
|
+
const chunk = data.subarray(offset, offset + chunkSize);
|
|
222
|
+
await this.write(chunk);
|
|
223
|
+
offset += chunkSize;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Bulk read for large data transfers
|
|
228
|
+
*/
|
|
229
|
+
async bulkRead(totalLength, chunkSize = constants_1.CH347_PACKET_SIZE) {
|
|
230
|
+
const chunks = [];
|
|
231
|
+
let remaining = totalLength;
|
|
232
|
+
while (remaining > 0) {
|
|
233
|
+
const readSize = Math.min(remaining, chunkSize);
|
|
234
|
+
const data = await this.read(readSize);
|
|
235
|
+
chunks.push(data);
|
|
236
|
+
remaining -= data.length;
|
|
237
|
+
// Break if we got less than expected (end of data)
|
|
238
|
+
if (data.length < readSize) {
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return Buffer.concat(chunks);
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* USB control transfer (for vendor-specific commands)
|
|
246
|
+
*/
|
|
247
|
+
async controlTransfer(bmRequestType, bRequest, wValue, wIndex, dataOrLength) {
|
|
248
|
+
if (!this.device) {
|
|
249
|
+
throw new Error('Device not open');
|
|
250
|
+
}
|
|
251
|
+
return new Promise((resolve, reject) => {
|
|
252
|
+
this.device.controlTransfer(bmRequestType, bRequest, wValue, wIndex, dataOrLength, (err, data) => {
|
|
253
|
+
if (err) {
|
|
254
|
+
reject(new Error(`Control transfer error: ${err.message}`));
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
resolve(data);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Get USB string descriptor
|
|
264
|
+
*/
|
|
265
|
+
async getStringDescriptor(index) {
|
|
266
|
+
if (!this.device) {
|
|
267
|
+
throw new Error('Device not open');
|
|
268
|
+
}
|
|
269
|
+
return new Promise((resolve, reject) => {
|
|
270
|
+
this.device.getStringDescriptor(index, (err, data) => {
|
|
271
|
+
if (err) {
|
|
272
|
+
reject(new Error(`Failed to get string descriptor: ${err.message}`));
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
resolve(data ?? '');
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Get device descriptor info including serial number index
|
|
282
|
+
*/
|
|
283
|
+
getDeviceDescriptor() {
|
|
284
|
+
return this.device?.deviceDescriptor ?? null;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Get the UART tty path for this CH347 device
|
|
288
|
+
* On Linux: scans /sys/class/tty for matching USB device
|
|
289
|
+
* On macOS: scans /dev for matching usbmodem device
|
|
290
|
+
*/
|
|
291
|
+
getUARTPath() {
|
|
292
|
+
if (!this.device) {
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
const busNumber = this.device.busNumber;
|
|
296
|
+
const deviceAddress = this.device.deviceAddress;
|
|
297
|
+
if (process.platform === 'linux') {
|
|
298
|
+
return this.findLinuxTTY(busNumber, deviceAddress);
|
|
299
|
+
}
|
|
300
|
+
else if (process.platform === 'darwin') {
|
|
301
|
+
return this.findMacOSTTY(busNumber, deviceAddress);
|
|
302
|
+
}
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Find TTY device on Linux by scanning sysfs
|
|
307
|
+
*/
|
|
308
|
+
findLinuxTTY(busNumber, deviceAddress) {
|
|
309
|
+
const ttyClassPath = '/sys/class/tty';
|
|
310
|
+
try {
|
|
311
|
+
const entries = fs.readdirSync(ttyClassPath);
|
|
312
|
+
for (const entry of entries) {
|
|
313
|
+
// Only check ttyACM devices (CDC ACM)
|
|
314
|
+
if (!entry.startsWith('ttyACM')) {
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
const devicePath = path.join(ttyClassPath, entry, 'device');
|
|
318
|
+
try {
|
|
319
|
+
// Resolve symlink to get the actual device path
|
|
320
|
+
const realPath = fs.realpathSync(devicePath);
|
|
321
|
+
// Parse USB device info from path
|
|
322
|
+
// Path looks like: /sys/devices/pci.../usb1/1-1/1-1:1.0/tty/ttyACM0
|
|
323
|
+
// We need to find the USB device part (e.g., 1-1) and check bus/dev
|
|
324
|
+
const usbDevMatch = realPath.match(/usb(\d+)\/[\d.-]+/);
|
|
325
|
+
if (!usbDevMatch) {
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
// Read busnum and devnum from the USB device directory
|
|
329
|
+
// Go up from the interface to the device
|
|
330
|
+
const interfaceMatch = realPath.match(/(\/sys\/devices\/.*\/usb\d+\/[\d.-]+):\d+\.\d+/);
|
|
331
|
+
if (!interfaceMatch) {
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
const usbDevicePath = interfaceMatch[1];
|
|
335
|
+
const busnumPath = path.join(usbDevicePath, 'busnum');
|
|
336
|
+
const devnumPath = path.join(usbDevicePath, 'devnum');
|
|
337
|
+
if (fs.existsSync(busnumPath) && fs.existsSync(devnumPath)) {
|
|
338
|
+
const busnum = parseInt(fs.readFileSync(busnumPath, 'utf8').trim(), 10);
|
|
339
|
+
const devnum = parseInt(fs.readFileSync(devnumPath, 'utf8').trim(), 10);
|
|
340
|
+
if (busnum === busNumber && devnum === deviceAddress) {
|
|
341
|
+
return `/dev/${entry}`;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
// Skip entries we can't read
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
// sysfs not available
|
|
353
|
+
}
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Find TTY device on macOS
|
|
358
|
+
* macOS names CDC ACM devices as /dev/tty.usbmodem* with location ID
|
|
359
|
+
*/
|
|
360
|
+
findMacOSTTY(_busNumber, _deviceAddress) {
|
|
361
|
+
try {
|
|
362
|
+
const entries = fs.readdirSync('/dev');
|
|
363
|
+
// Look for usbmodem devices
|
|
364
|
+
const usbmodemDevices = entries
|
|
365
|
+
.filter(e => e.startsWith('tty.usbmodem'))
|
|
366
|
+
.map(e => `/dev/${e}`);
|
|
367
|
+
if (usbmodemDevices.length === 0) {
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
// On macOS, the location ID is encoded in the device name
|
|
371
|
+
// Format: tty.usbmodem<locationID><suffix>
|
|
372
|
+
// We can try to match by using ioreg, but for simplicity,
|
|
373
|
+
// if there's only one CH347 device, return the first match
|
|
374
|
+
// For multiple devices, user may need to specify manually
|
|
375
|
+
// Try to use system_profiler to match (async would be better but keeping it sync)
|
|
376
|
+
try {
|
|
377
|
+
const { execSync } = require('child_process');
|
|
378
|
+
const output = execSync('system_profiler SPUSBDataType 2>/dev/null', { encoding: 'utf8' });
|
|
379
|
+
// Parse the output to find CH347 device location
|
|
380
|
+
const lines = output.split('\n');
|
|
381
|
+
let inCH347Section = false;
|
|
382
|
+
let locationId = '';
|
|
383
|
+
for (const line of lines) {
|
|
384
|
+
if (line.includes('CH347') || line.includes('1a86')) {
|
|
385
|
+
inCH347Section = true;
|
|
386
|
+
}
|
|
387
|
+
if (inCH347Section && line.includes('Location ID:')) {
|
|
388
|
+
const match = line.match(/Location ID:\s*0x([0-9a-fA-F]+)/);
|
|
389
|
+
if (match) {
|
|
390
|
+
locationId = match[1].toLowerCase();
|
|
391
|
+
break;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
if (locationId) {
|
|
396
|
+
// Match device name containing location ID
|
|
397
|
+
for (const dev of usbmodemDevices) {
|
|
398
|
+
// Location ID is often part of the device name
|
|
399
|
+
if (dev.toLowerCase().includes(locationId.substring(0, 4))) {
|
|
400
|
+
return dev;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
// system_profiler failed, fall through
|
|
407
|
+
}
|
|
408
|
+
// Fallback: return first usbmodem device if only one exists
|
|
409
|
+
if (usbmodemDevices.length === 1) {
|
|
410
|
+
return usbmodemDevices[0];
|
|
411
|
+
}
|
|
412
|
+
// Multiple devices, can't determine which one
|
|
413
|
+
return usbmodemDevices[0]; // Return first as best guess
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
// /dev not readable
|
|
417
|
+
}
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
exports.CH347USB = CH347USB;
|
|
422
|
+
//# sourceMappingURL=usb.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "node-ch347",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Node.js library for CH347 USB interface - GPIO, SPI flash programming, and UART",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"!dist/examples",
|
|
10
|
+
"!dist/**/*.map"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"dev": "tsc --watch",
|
|
15
|
+
"test": "node --test dist/**/*.test.js",
|
|
16
|
+
"prepublishOnly": "npm run build",
|
|
17
|
+
"example": "node dist/examples/basic.js"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"ch347",
|
|
21
|
+
"usb",
|
|
22
|
+
"gpio",
|
|
23
|
+
"spi",
|
|
24
|
+
"uart",
|
|
25
|
+
"flash",
|
|
26
|
+
"programmer",
|
|
27
|
+
"embedded",
|
|
28
|
+
"hardware"
|
|
29
|
+
],
|
|
30
|
+
"author": "",
|
|
31
|
+
"license": "Unlicense",
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"usb": "^2.14.0"
|
|
34
|
+
},
|
|
35
|
+
"optionalDependencies": {
|
|
36
|
+
"serialport": "^12.0.0"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/node": "^20.10.0",
|
|
40
|
+
"typescript": "^5.3.0"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=18.0.0"
|
|
44
|
+
},
|
|
45
|
+
"os": [
|
|
46
|
+
"linux",
|
|
47
|
+
"darwin"
|
|
48
|
+
]
|
|
49
|
+
}
|