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/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
+ }