esp32tool 1.1.9 → 1.3.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/.nojekyll +0 -0
- package/README.md +100 -6
- package/apple-touch-icon.png +0 -0
- package/build-electron-cli.cjs +177 -0
- package/build-single-binary.cjs +295 -0
- package/css/light.css +11 -0
- package/css/style.css +261 -41
- package/dist/cli.d.ts +17 -0
- package/dist/cli.js +458 -0
- package/dist/console.d.ts +15 -0
- package/dist/console.js +237 -0
- package/dist/const.d.ts +99 -0
- package/dist/const.js +129 -8
- package/dist/esp_loader.d.ts +244 -22
- package/dist/esp_loader.js +1960 -251
- package/dist/index.d.ts +2 -1
- package/dist/index.js +37 -4
- package/dist/node-usb-adapter.d.ts +47 -0
- package/dist/node-usb-adapter.js +725 -0
- package/dist/stubs/index.d.ts +1 -2
- package/dist/stubs/index.js +4 -0
- package/dist/util/console-color.d.ts +19 -0
- package/dist/util/console-color.js +272 -0
- package/dist/util/line-break-transformer.d.ts +5 -0
- package/dist/util/line-break-transformer.js +17 -0
- package/dist/web/index.js +1 -1
- package/electron/cli-main.cjs +74 -0
- package/electron/main.cjs +338 -0
- package/electron/main.js +7 -2
- package/favicon.ico +0 -0
- package/fix-cli-imports.cjs +127 -0
- package/generate-icons.sh +89 -0
- package/icons/icon-128.png +0 -0
- package/icons/icon-144.png +0 -0
- package/icons/icon-152.png +0 -0
- package/icons/icon-192.png +0 -0
- package/icons/icon-384.png +0 -0
- package/icons/icon-512.png +0 -0
- package/icons/icon-72.png +0 -0
- package/icons/icon-96.png +0 -0
- package/index.html +143 -73
- package/install-android.html +411 -0
- package/js/console.js +269 -0
- package/js/modules/esptool.js +1 -1
- package/js/script.js +750 -175
- package/js/util/console-color.js +282 -0
- package/js/util/line-break-transformer.js +19 -0
- package/js/webusb-serial.js +1017 -0
- package/license.md +1 -1
- package/manifest.json +89 -0
- package/package.cli.json +29 -0
- package/package.json +35 -24
- package/screenshots/desktop.png +0 -0
- package/screenshots/mobile.png +0 -0
- package/src/cli.ts +618 -0
- package/src/console.ts +278 -0
- package/src/const.ts +165 -8
- package/src/esp_loader.ts +2354 -302
- package/src/index.ts +69 -3
- package/src/node-usb-adapter.ts +924 -0
- package/src/stubs/index.ts +4 -1
- package/src/util/console-color.ts +290 -0
- package/src/util/line-break-transformer.ts +20 -0
- package/sw.js +155 -0
|
@@ -0,0 +1,924 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node.js USB Adapter
|
|
3
|
+
*
|
|
4
|
+
* Uses node-usb to directly control USB-Serial chips (CP2102, CH340, FTDI, etc.)
|
|
5
|
+
* This provides the same level of control as WebUSB and avoids node-serialport issues
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Logger } from "./const";
|
|
9
|
+
import type { Device, InEndpoint, OutEndpoint } from "usb";
|
|
10
|
+
|
|
11
|
+
export interface NodeUSBPort {
|
|
12
|
+
readable: ReadableStream<Uint8Array> | null;
|
|
13
|
+
writable: WritableStream<Uint8Array> | null;
|
|
14
|
+
isWebUSB?: boolean; // Flag to indicate this behaves like WebUSB
|
|
15
|
+
|
|
16
|
+
open(options: { baudRate: number }): Promise<void>;
|
|
17
|
+
close(): Promise<void>;
|
|
18
|
+
|
|
19
|
+
setSignals(signals: {
|
|
20
|
+
dataTerminalReady?: boolean;
|
|
21
|
+
requestToSend?: boolean;
|
|
22
|
+
break?: boolean;
|
|
23
|
+
}): Promise<void>;
|
|
24
|
+
|
|
25
|
+
setBaudRate(baudRate: number): Promise<void>; // Add baudrate change support
|
|
26
|
+
|
|
27
|
+
getSignals(): Promise<{
|
|
28
|
+
dataCarrierDetect: boolean;
|
|
29
|
+
clearToSend: boolean;
|
|
30
|
+
ringIndicator: boolean;
|
|
31
|
+
dataSetReady: boolean;
|
|
32
|
+
}>;
|
|
33
|
+
|
|
34
|
+
getInfo(): {
|
|
35
|
+
usbVendorId?: number;
|
|
36
|
+
usbProductId?: number;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Create a Web Serial API compatible port from node-usb Device
|
|
42
|
+
*/
|
|
43
|
+
export function createNodeUSBAdapter(
|
|
44
|
+
device: Device,
|
|
45
|
+
logger: Logger,
|
|
46
|
+
): NodeUSBPort {
|
|
47
|
+
let readableStream: ReadableStream<Uint8Array> | null = null;
|
|
48
|
+
let writableStream: WritableStream<Uint8Array> | null = null;
|
|
49
|
+
let interfaceNumber: number | null = null;
|
|
50
|
+
let controlInterface: number | null = null;
|
|
51
|
+
let endpointIn: InEndpoint | null = null;
|
|
52
|
+
let endpointOut: OutEndpoint | null = null;
|
|
53
|
+
let endpointInNumber: number | null = null;
|
|
54
|
+
let endpointOutNumber: number | null = null;
|
|
55
|
+
// readLoopRunning tracked internally by stream
|
|
56
|
+
|
|
57
|
+
// Track current signal states
|
|
58
|
+
let currentDTR: boolean = false;
|
|
59
|
+
let currentRTS: boolean = false;
|
|
60
|
+
|
|
61
|
+
const vendorId = device.deviceDescriptor.idVendor;
|
|
62
|
+
const productId = device.deviceDescriptor.idProduct;
|
|
63
|
+
|
|
64
|
+
const adapter: NodeUSBPort = {
|
|
65
|
+
isWebUSB: true, // Mark this as WebUSB-like behavior for reset strategy selection
|
|
66
|
+
|
|
67
|
+
get readable() {
|
|
68
|
+
return readableStream;
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
get writable() {
|
|
72
|
+
return writableStream;
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
async open(options: { baudRate: number }) {
|
|
76
|
+
const baudRate = options.baudRate;
|
|
77
|
+
logger.log(`Opening USB device at ${baudRate} baud...`);
|
|
78
|
+
|
|
79
|
+
// Open device
|
|
80
|
+
try {
|
|
81
|
+
device.open();
|
|
82
|
+
} catch (err: any) {
|
|
83
|
+
throw new Error(`Failed to open USB device: ${err.message}`);
|
|
84
|
+
}
|
|
85
|
+
// Select configuration
|
|
86
|
+
try {
|
|
87
|
+
if (device.configDescriptor?.bConfigurationValue !== 1) {
|
|
88
|
+
device.setConfiguration(1);
|
|
89
|
+
}
|
|
90
|
+
} catch (err) {
|
|
91
|
+
// Already configured
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Find bulk IN/OUT interface
|
|
95
|
+
const config = device.configDescriptor;
|
|
96
|
+
if (!config) {
|
|
97
|
+
throw new Error("No configuration descriptor");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Find suitable interface with bulk endpoints
|
|
101
|
+
for (const iface of config.interfaces) {
|
|
102
|
+
for (const alt of iface) {
|
|
103
|
+
let hasIn = false;
|
|
104
|
+
let hasOut = false;
|
|
105
|
+
let inEpNum: number | null = null;
|
|
106
|
+
let outEpNum: number | null = null;
|
|
107
|
+
|
|
108
|
+
for (const ep of alt.endpoints) {
|
|
109
|
+
const epDesc = ep as any;
|
|
110
|
+
if (epDesc.bmAttributes === 2 || epDesc.transferType === 2) {
|
|
111
|
+
// Bulk transfer
|
|
112
|
+
const dir = epDesc.bEndpointAddress & 0x80 ? "in" : "out";
|
|
113
|
+
if (dir === "in" && !hasIn) {
|
|
114
|
+
hasIn = true;
|
|
115
|
+
inEpNum = epDesc.bEndpointAddress;
|
|
116
|
+
} else if (dir === "out" && !hasOut) {
|
|
117
|
+
hasOut = true;
|
|
118
|
+
outEpNum = epDesc.bEndpointAddress;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (hasIn && hasOut) {
|
|
124
|
+
interfaceNumber = iface[0].bInterfaceNumber;
|
|
125
|
+
endpointInNumber = inEpNum;
|
|
126
|
+
endpointOutNumber = outEpNum;
|
|
127
|
+
logger.debug(
|
|
128
|
+
`Found interface ${interfaceNumber} with IN=0x${inEpNum?.toString(16)}, OUT=0x${outEpNum?.toString(16)}`,
|
|
129
|
+
);
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (interfaceNumber !== null) break;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (interfaceNumber === null || !endpointInNumber || !endpointOutNumber) {
|
|
137
|
+
throw new Error("No suitable USB interface found");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Claim interface
|
|
141
|
+
const usbInterface = device.interface(interfaceNumber);
|
|
142
|
+
|
|
143
|
+
// Detach kernel driver if active (Linux/macOS)
|
|
144
|
+
try {
|
|
145
|
+
if (usbInterface.isKernelDriverActive()) {
|
|
146
|
+
usbInterface.detachKernelDriver();
|
|
147
|
+
}
|
|
148
|
+
} catch (err) {
|
|
149
|
+
// Ignore - may not be supported on all platforms
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
usbInterface.claim();
|
|
153
|
+
|
|
154
|
+
controlInterface = interfaceNumber;
|
|
155
|
+
|
|
156
|
+
// Get the actual endpoints from the claimed interface
|
|
157
|
+
const endpoints = usbInterface.endpoints;
|
|
158
|
+
logger.debug(
|
|
159
|
+
`Found ${endpoints.length} endpoints on interface ${interfaceNumber}`,
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
// Find endpoints by address
|
|
163
|
+
endpointIn = endpoints.find(
|
|
164
|
+
(ep: any) => ep.address === endpointInNumber,
|
|
165
|
+
) as InEndpoint;
|
|
166
|
+
endpointOut = endpoints.find(
|
|
167
|
+
(ep: any) => ep.address === endpointOutNumber,
|
|
168
|
+
) as OutEndpoint;
|
|
169
|
+
|
|
170
|
+
if (!endpointIn || !endpointOut) {
|
|
171
|
+
throw new Error(
|
|
172
|
+
`Could not find endpoints: IN=0x${endpointInNumber?.toString(16)}, OUT=0x${endpointOutNumber?.toString(16)}`,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
logger.debug(
|
|
177
|
+
`Endpoints ready: IN=0x${endpointIn.address.toString(16)}, OUT=0x${endpointOut.address.toString(16)}`,
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
// Initialize chip-specific settings
|
|
181
|
+
try {
|
|
182
|
+
await initializeChip(device, vendorId, productId, baudRate, logger);
|
|
183
|
+
} catch (err: any) {
|
|
184
|
+
logger.error(`Failed to initialize chip: ${err.message}`);
|
|
185
|
+
throw err;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// For CP2102: Clear any pending data
|
|
189
|
+
if (vendorId === 0x10c4) {
|
|
190
|
+
try {
|
|
191
|
+
// Clear halt on endpoints
|
|
192
|
+
await new Promise<void>((resolve, reject) => {
|
|
193
|
+
device.controlTransfer(
|
|
194
|
+
0x02, // Clear Feature, Endpoint
|
|
195
|
+
0x01, // ENDPOINT_HALT
|
|
196
|
+
0,
|
|
197
|
+
endpointIn!.address,
|
|
198
|
+
Buffer.alloc(0),
|
|
199
|
+
(err) => {
|
|
200
|
+
if (err) logger.debug(`Clear halt IN failed: ${err.message}`);
|
|
201
|
+
resolve();
|
|
202
|
+
},
|
|
203
|
+
);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
await new Promise<void>((resolve, reject) => {
|
|
207
|
+
device.controlTransfer(
|
|
208
|
+
0x02, // Clear Feature, Endpoint
|
|
209
|
+
0x01, // ENDPOINT_HALT
|
|
210
|
+
0,
|
|
211
|
+
endpointOut!.address,
|
|
212
|
+
Buffer.alloc(0),
|
|
213
|
+
(err) => {
|
|
214
|
+
if (err) logger.debug(`Clear halt OUT failed: ${err.message}`);
|
|
215
|
+
resolve();
|
|
216
|
+
},
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
} catch (err) {
|
|
220
|
+
// Ignore
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Wait for chip to be ready
|
|
225
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
226
|
+
|
|
227
|
+
// Create streams
|
|
228
|
+
createStreams();
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
async close() {
|
|
232
|
+
// Stop polling and remove event listeners BEFORE cancelling streams
|
|
233
|
+
if (endpointIn) {
|
|
234
|
+
try {
|
|
235
|
+
endpointIn.stopPoll();
|
|
236
|
+
endpointIn.removeAllListeners();
|
|
237
|
+
} catch (err) {
|
|
238
|
+
// Ignore
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (readableStream) {
|
|
243
|
+
try {
|
|
244
|
+
await readableStream.cancel();
|
|
245
|
+
} catch (err) {
|
|
246
|
+
// Ignore
|
|
247
|
+
}
|
|
248
|
+
readableStream = null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (writableStream) {
|
|
252
|
+
try {
|
|
253
|
+
await writableStream.close();
|
|
254
|
+
} catch (err) {
|
|
255
|
+
// Ignore
|
|
256
|
+
}
|
|
257
|
+
writableStream = null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Small delay to let any pending callbacks complete
|
|
261
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
262
|
+
|
|
263
|
+
if (interfaceNumber !== null) {
|
|
264
|
+
try {
|
|
265
|
+
const usbInterface = device.interface(interfaceNumber);
|
|
266
|
+
usbInterface.release(true, () => {});
|
|
267
|
+
} catch (err) {
|
|
268
|
+
// Ignore
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
device.close();
|
|
274
|
+
} catch (err) {
|
|
275
|
+
// Ignore
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
async setSignals(signals: {
|
|
280
|
+
dataTerminalReady?: boolean;
|
|
281
|
+
requestToSend?: boolean;
|
|
282
|
+
break?: boolean;
|
|
283
|
+
}) {
|
|
284
|
+
// Preserve current state for unspecified signals
|
|
285
|
+
const dtr =
|
|
286
|
+
signals.dataTerminalReady !== undefined
|
|
287
|
+
? signals.dataTerminalReady
|
|
288
|
+
: currentDTR;
|
|
289
|
+
const rts =
|
|
290
|
+
signals.requestToSend !== undefined
|
|
291
|
+
? signals.requestToSend
|
|
292
|
+
: currentRTS;
|
|
293
|
+
|
|
294
|
+
currentDTR = dtr;
|
|
295
|
+
currentRTS = rts;
|
|
296
|
+
|
|
297
|
+
// logger.log(`Setting signals: DTR=${dtr}, RTS=${rts} (CP2102: GPIO0=${dtr ? 'LOW' : 'HIGH'}, EN=${rts ? 'LOW' : 'HIGH'})`);
|
|
298
|
+
|
|
299
|
+
// CP2102 (Silicon Labs VID: 0x10c4)
|
|
300
|
+
if (vendorId === 0x10c4) {
|
|
301
|
+
await setSignalsCP2102(device, dtr, rts);
|
|
302
|
+
}
|
|
303
|
+
// CH340 (WCH VID: 0x1a86, but not CH343 PID: 0x55d3)
|
|
304
|
+
else if (vendorId === 0x1a86 && productId !== 0x55d3) {
|
|
305
|
+
await setSignalsCH340(device, dtr, rts);
|
|
306
|
+
}
|
|
307
|
+
// FTDI (VID: 0x0403)
|
|
308
|
+
else if (vendorId === 0x0403) {
|
|
309
|
+
await setSignalsFTDI(device, dtr, rts);
|
|
310
|
+
}
|
|
311
|
+
// CDC/ACM (CH343, Native USB, etc.)
|
|
312
|
+
else {
|
|
313
|
+
await setSignalsCDC(device, dtr, rts, controlInterface || 0);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Match WebUSB timing - 50ms delay is critical for bootloader entry
|
|
317
|
+
// This ensures signals are stable before next operation
|
|
318
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
319
|
+
},
|
|
320
|
+
|
|
321
|
+
async setBaudRate(baudRate: number) {
|
|
322
|
+
// CP2102 (Silicon Labs VID: 0x10c4)
|
|
323
|
+
if (vendorId === 0x10c4) {
|
|
324
|
+
// logger.debug(`[USB] CP2102: Setting baudrate to ${baudRate}...`);
|
|
325
|
+
const baudrateBuffer = Buffer.alloc(4);
|
|
326
|
+
baudrateBuffer.writeUInt32LE(baudRate, 0);
|
|
327
|
+
await controlTransferOut(
|
|
328
|
+
device,
|
|
329
|
+
{
|
|
330
|
+
requestType: "vendor",
|
|
331
|
+
recipient: "interface",
|
|
332
|
+
request: 0x1e, // IFC_SET_BAUDRATE
|
|
333
|
+
value: 0,
|
|
334
|
+
index: 0,
|
|
335
|
+
},
|
|
336
|
+
baudrateBuffer,
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
// CH340 (WCH VID: 0x1a86, but not CH343 PID: 0x55d3)
|
|
340
|
+
else if (vendorId === 0x1a86 && productId !== 0x55d3) {
|
|
341
|
+
const CH341_BAUDBASE_FACTOR = 1532620800;
|
|
342
|
+
const CH341_BAUDBASE_DIVMAX = 3;
|
|
343
|
+
|
|
344
|
+
let factor = Math.floor(CH341_BAUDBASE_FACTOR / baudRate);
|
|
345
|
+
let divisor = CH341_BAUDBASE_DIVMAX;
|
|
346
|
+
|
|
347
|
+
while (factor > 0xfff0 && divisor > 0) {
|
|
348
|
+
factor >>= 3;
|
|
349
|
+
divisor--;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
factor = 0x10000 - factor;
|
|
353
|
+
const a = (factor & 0xff00) | divisor;
|
|
354
|
+
const b = factor & 0xff;
|
|
355
|
+
|
|
356
|
+
await controlTransferOut(device, {
|
|
357
|
+
requestType: "vendor",
|
|
358
|
+
recipient: "device",
|
|
359
|
+
request: 0x9a,
|
|
360
|
+
value: 0x1312,
|
|
361
|
+
index: a,
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
await controlTransferOut(device, {
|
|
365
|
+
requestType: "vendor",
|
|
366
|
+
recipient: "device",
|
|
367
|
+
request: 0x9a,
|
|
368
|
+
value: 0x0f2c,
|
|
369
|
+
index: b,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
// FTDI (VID: 0x0403)
|
|
373
|
+
else if (vendorId === 0x0403) {
|
|
374
|
+
const baseClock = 3000000;
|
|
375
|
+
const divisor = baseClock / baudRate;
|
|
376
|
+
const integerPart = Math.floor(divisor);
|
|
377
|
+
const fractionalPart = divisor - integerPart;
|
|
378
|
+
|
|
379
|
+
let subInteger;
|
|
380
|
+
if (fractionalPart < 0.0625) subInteger = 0;
|
|
381
|
+
else if (fractionalPart < 0.1875) subInteger = 1;
|
|
382
|
+
else if (fractionalPart < 0.3125) subInteger = 2;
|
|
383
|
+
else if (fractionalPart < 0.4375) subInteger = 3;
|
|
384
|
+
else if (fractionalPart < 0.5625) subInteger = 4;
|
|
385
|
+
else if (fractionalPart < 0.6875) subInteger = 5;
|
|
386
|
+
else if (fractionalPart < 0.8125) subInteger = 6;
|
|
387
|
+
else subInteger = 7;
|
|
388
|
+
|
|
389
|
+
const value =
|
|
390
|
+
(integerPart & 0xff) |
|
|
391
|
+
((subInteger & 0x07) << 14) |
|
|
392
|
+
(((integerPart >> 8) & 0x3f) << 8);
|
|
393
|
+
const index = (integerPart >> 14) & 0x03;
|
|
394
|
+
|
|
395
|
+
await controlTransferOut(device, {
|
|
396
|
+
requestType: "vendor",
|
|
397
|
+
recipient: "device",
|
|
398
|
+
request: 0x03,
|
|
399
|
+
value: value,
|
|
400
|
+
index: index,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
// CDC/ACM (CH343, Native USB, etc.)
|
|
404
|
+
else {
|
|
405
|
+
const lineCoding = Buffer.alloc(7);
|
|
406
|
+
lineCoding.writeUInt32LE(baudRate, 0);
|
|
407
|
+
lineCoding[4] = 0x00; // 1 stop bit
|
|
408
|
+
lineCoding[5] = 0x00; // No parity
|
|
409
|
+
lineCoding[6] = 0x08; // 8 data bits
|
|
410
|
+
|
|
411
|
+
await controlTransferOut(
|
|
412
|
+
device,
|
|
413
|
+
{
|
|
414
|
+
requestType: "class",
|
|
415
|
+
recipient: "interface",
|
|
416
|
+
request: 0x20,
|
|
417
|
+
value: 0,
|
|
418
|
+
index: controlInterface || 0,
|
|
419
|
+
},
|
|
420
|
+
lineCoding,
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
},
|
|
424
|
+
|
|
425
|
+
async getSignals() {
|
|
426
|
+
// Not implemented for USB - return dummy values
|
|
427
|
+
return {
|
|
428
|
+
dataCarrierDetect: false,
|
|
429
|
+
clearToSend: false,
|
|
430
|
+
ringIndicator: false,
|
|
431
|
+
dataSetReady: false,
|
|
432
|
+
};
|
|
433
|
+
},
|
|
434
|
+
|
|
435
|
+
getInfo() {
|
|
436
|
+
return {
|
|
437
|
+
usbVendorId: vendorId,
|
|
438
|
+
usbProductId: productId,
|
|
439
|
+
};
|
|
440
|
+
},
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
function createStreams() {
|
|
444
|
+
if (!endpointIn || !endpointOut) {
|
|
445
|
+
throw new Error("Endpoints not configured");
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Start polling immediately (not in ReadableStream.start)
|
|
449
|
+
try {
|
|
450
|
+
endpointIn.startPoll(2, 64);
|
|
451
|
+
} catch (err: any) {
|
|
452
|
+
logger.error(`Failed to start poll: ${err.message}`);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// ReadableStream for incoming data
|
|
456
|
+
readableStream = new ReadableStream({
|
|
457
|
+
start(controller) {
|
|
458
|
+
endpointIn!.on("data", (data: Buffer) => {
|
|
459
|
+
try {
|
|
460
|
+
if (data.length > 0) {
|
|
461
|
+
controller.enqueue(new Uint8Array(data));
|
|
462
|
+
}
|
|
463
|
+
} catch (err: any) {
|
|
464
|
+
logger.error(`USB RX handler error: ${err.message}`);
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
endpointIn!.on("error", (err: Error) => {
|
|
469
|
+
try {
|
|
470
|
+
logger.error(`USB read error: ${err.message}`);
|
|
471
|
+
// Don't close on error, just log it
|
|
472
|
+
} catch (e) {
|
|
473
|
+
// Ignore errors in error handler
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
endpointIn!.on("end", () => {
|
|
478
|
+
try {
|
|
479
|
+
controller.close();
|
|
480
|
+
} catch (err) {
|
|
481
|
+
// Ignore errors when closing controller
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
},
|
|
485
|
+
|
|
486
|
+
cancel() {
|
|
487
|
+
if (endpointIn) {
|
|
488
|
+
try {
|
|
489
|
+
endpointIn.stopPoll();
|
|
490
|
+
endpointIn.removeAllListeners();
|
|
491
|
+
} catch (err) {
|
|
492
|
+
// Ignore
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
},
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
// WritableStream for outgoing data
|
|
499
|
+
writableStream = new WritableStream({
|
|
500
|
+
async write(chunk: Uint8Array) {
|
|
501
|
+
return new Promise((resolve, reject) => {
|
|
502
|
+
if (!endpointOut) {
|
|
503
|
+
reject(new Error("Endpoint not configured"));
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
endpointOut.transfer(Buffer.from(chunk), (err) => {
|
|
508
|
+
if (err) {
|
|
509
|
+
logger.error(`USB TX error: ${err.message}`);
|
|
510
|
+
reject(err);
|
|
511
|
+
} else {
|
|
512
|
+
resolve();
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
},
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return adapter;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Chip-specific initialization functions
|
|
524
|
+
|
|
525
|
+
async function initializeChip(
|
|
526
|
+
device: Device,
|
|
527
|
+
vendorId: number,
|
|
528
|
+
productId: number,
|
|
529
|
+
baudRate: number,
|
|
530
|
+
logger: Logger,
|
|
531
|
+
): Promise<void> {
|
|
532
|
+
// CP2102 (Silicon Labs)
|
|
533
|
+
if (vendorId === 0x10c4) {
|
|
534
|
+
logger.debug("Initializing CP2102...");
|
|
535
|
+
|
|
536
|
+
// Step 1: Enable UART
|
|
537
|
+
logger.debug("CP2102: Enabling UART interface...");
|
|
538
|
+
await controlTransferOut(device, {
|
|
539
|
+
requestType: "vendor",
|
|
540
|
+
recipient: "device",
|
|
541
|
+
request: 0x00, // IFC_ENABLE
|
|
542
|
+
value: 0x01, // UART_ENABLE
|
|
543
|
+
index: 0x00,
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
// Step 2: Set line control (8N1)
|
|
547
|
+
logger.debug("CP2102: Setting line control (8N1)...");
|
|
548
|
+
await controlTransferOut(device, {
|
|
549
|
+
requestType: "vendor",
|
|
550
|
+
recipient: "device",
|
|
551
|
+
request: 0x03, // SET_LINE_CTL
|
|
552
|
+
value: 0x0800, // 8 data bits, no parity, 1 stop bit
|
|
553
|
+
index: 0x00,
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
// Step 3: Set DTR/RTS
|
|
557
|
+
logger.debug("CP2102: Setting DTR/RTS...");
|
|
558
|
+
await controlTransferOut(device, {
|
|
559
|
+
requestType: "vendor",
|
|
560
|
+
recipient: "device",
|
|
561
|
+
request: 0x07, // SET_MHS
|
|
562
|
+
value: 0x03 | 0x0100 | 0x0200, // DTR=1, RTS=1 with masks
|
|
563
|
+
index: 0x00,
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
// Step 4: Set baudrate
|
|
567
|
+
logger.debug(`CP2102: Setting baudrate to ${baudRate}...`);
|
|
568
|
+
const baudrateBuffer = Buffer.alloc(4);
|
|
569
|
+
baudrateBuffer.writeUInt32LE(baudRate, 0);
|
|
570
|
+
await controlTransferOut(
|
|
571
|
+
device,
|
|
572
|
+
{
|
|
573
|
+
requestType: "vendor",
|
|
574
|
+
recipient: "interface",
|
|
575
|
+
request: 0x1e, // IFC_SET_BAUDRATE
|
|
576
|
+
value: 0,
|
|
577
|
+
index: 0,
|
|
578
|
+
},
|
|
579
|
+
baudrateBuffer,
|
|
580
|
+
);
|
|
581
|
+
|
|
582
|
+
logger.debug("CP2102: Initialization complete");
|
|
583
|
+
}
|
|
584
|
+
// CH340 (WCH)
|
|
585
|
+
else if (vendorId === 0x1a86 && productId !== 0x55d3) {
|
|
586
|
+
logger.debug("Initializing CH340...");
|
|
587
|
+
|
|
588
|
+
// Initialize
|
|
589
|
+
await controlTransferOut(device, {
|
|
590
|
+
requestType: "vendor",
|
|
591
|
+
recipient: "device",
|
|
592
|
+
request: 0xa1,
|
|
593
|
+
value: 0x0000,
|
|
594
|
+
index: 0x0000,
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
// Set baudrate
|
|
598
|
+
const CH341_BAUDBASE_FACTOR = 1532620800;
|
|
599
|
+
const CH341_BAUDBASE_DIVMAX = 3;
|
|
600
|
+
|
|
601
|
+
let factor = Math.floor(CH341_BAUDBASE_FACTOR / baudRate);
|
|
602
|
+
let divisor = CH341_BAUDBASE_DIVMAX;
|
|
603
|
+
|
|
604
|
+
while (factor > 0xfff0 && divisor > 0) {
|
|
605
|
+
factor >>= 3;
|
|
606
|
+
divisor--;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
factor = 0x10000 - factor;
|
|
610
|
+
const a = (factor & 0xff00) | divisor;
|
|
611
|
+
const b = factor & 0xff;
|
|
612
|
+
|
|
613
|
+
await controlTransferOut(device, {
|
|
614
|
+
requestType: "vendor",
|
|
615
|
+
recipient: "device",
|
|
616
|
+
request: 0x9a,
|
|
617
|
+
value: 0x1312,
|
|
618
|
+
index: a,
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
await controlTransferOut(device, {
|
|
622
|
+
requestType: "vendor",
|
|
623
|
+
recipient: "device",
|
|
624
|
+
request: 0x9a,
|
|
625
|
+
value: 0x0f2c,
|
|
626
|
+
index: b,
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
// Set handshake
|
|
630
|
+
await controlTransferOut(device, {
|
|
631
|
+
requestType: "vendor",
|
|
632
|
+
recipient: "device",
|
|
633
|
+
request: 0xa4,
|
|
634
|
+
value: ~((1 << 5) | (1 << 6)) & 0xffff,
|
|
635
|
+
index: 0x0000,
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
// FTDI
|
|
639
|
+
else if (vendorId === 0x0403) {
|
|
640
|
+
logger.debug("Initializing FTDI...");
|
|
641
|
+
|
|
642
|
+
// Reset
|
|
643
|
+
await controlTransferOut(device, {
|
|
644
|
+
requestType: "vendor",
|
|
645
|
+
recipient: "device",
|
|
646
|
+
request: 0x00,
|
|
647
|
+
value: 0x00,
|
|
648
|
+
index: 0x00,
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
// Set flow control
|
|
652
|
+
await controlTransferOut(device, {
|
|
653
|
+
requestType: "vendor",
|
|
654
|
+
recipient: "device",
|
|
655
|
+
request: 0x02,
|
|
656
|
+
value: 0x00,
|
|
657
|
+
index: 0x00,
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
// Set data characteristics (8N1)
|
|
661
|
+
await controlTransferOut(device, {
|
|
662
|
+
requestType: "vendor",
|
|
663
|
+
recipient: "device",
|
|
664
|
+
request: 0x04,
|
|
665
|
+
value: 0x0008,
|
|
666
|
+
index: 0x00,
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
// Set baudrate
|
|
670
|
+
const baseClock = 3000000;
|
|
671
|
+
const divisor = baseClock / baudRate;
|
|
672
|
+
const integerPart = Math.floor(divisor);
|
|
673
|
+
const fractionalPart = divisor - integerPart;
|
|
674
|
+
|
|
675
|
+
let subInteger;
|
|
676
|
+
if (fractionalPart < 0.0625) subInteger = 0;
|
|
677
|
+
else if (fractionalPart < 0.1875) subInteger = 1;
|
|
678
|
+
else if (fractionalPart < 0.3125) subInteger = 2;
|
|
679
|
+
else if (fractionalPart < 0.4375) subInteger = 3;
|
|
680
|
+
else if (fractionalPart < 0.5625) subInteger = 4;
|
|
681
|
+
else if (fractionalPart < 0.6875) subInteger = 5;
|
|
682
|
+
else if (fractionalPart < 0.8125) subInteger = 6;
|
|
683
|
+
else subInteger = 7;
|
|
684
|
+
|
|
685
|
+
const value =
|
|
686
|
+
(integerPart & 0xff) |
|
|
687
|
+
((subInteger & 0x07) << 14) |
|
|
688
|
+
(((integerPart >> 8) & 0x3f) << 8);
|
|
689
|
+
const index = (integerPart >> 14) & 0x03;
|
|
690
|
+
|
|
691
|
+
await controlTransferOut(device, {
|
|
692
|
+
requestType: "vendor",
|
|
693
|
+
recipient: "device",
|
|
694
|
+
request: 0x03,
|
|
695
|
+
value: value,
|
|
696
|
+
index: index,
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
// Set DTR/RTS
|
|
700
|
+
await controlTransferOut(device, {
|
|
701
|
+
requestType: "vendor",
|
|
702
|
+
recipient: "device",
|
|
703
|
+
request: 0x01,
|
|
704
|
+
value: 0x0303,
|
|
705
|
+
index: 0x00,
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
// CDC/ACM
|
|
709
|
+
else {
|
|
710
|
+
logger.debug("Initializing CDC/ACM...");
|
|
711
|
+
|
|
712
|
+
// Set line coding
|
|
713
|
+
const lineCoding = Buffer.alloc(7);
|
|
714
|
+
lineCoding.writeUInt32LE(baudRate, 0);
|
|
715
|
+
lineCoding[4] = 0x00; // 1 stop bit
|
|
716
|
+
lineCoding[5] = 0x00; // No parity
|
|
717
|
+
lineCoding[6] = 0x08; // 8 data bits
|
|
718
|
+
|
|
719
|
+
await controlTransferOut(
|
|
720
|
+
device,
|
|
721
|
+
{
|
|
722
|
+
requestType: "class",
|
|
723
|
+
recipient: "interface",
|
|
724
|
+
request: 0x20,
|
|
725
|
+
value: 0,
|
|
726
|
+
index: 0,
|
|
727
|
+
},
|
|
728
|
+
lineCoding,
|
|
729
|
+
);
|
|
730
|
+
|
|
731
|
+
// Set control line state
|
|
732
|
+
await controlTransferOut(device, {
|
|
733
|
+
requestType: "class",
|
|
734
|
+
recipient: "interface",
|
|
735
|
+
request: 0x22,
|
|
736
|
+
value: 0x03,
|
|
737
|
+
index: 0,
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// Signal setting functions
|
|
743
|
+
|
|
744
|
+
async function setSignalsCP2102(
|
|
745
|
+
device: Device,
|
|
746
|
+
dtr: boolean,
|
|
747
|
+
rts: boolean,
|
|
748
|
+
): Promise<void> {
|
|
749
|
+
// CP2102 uses vendor-specific request 0x07 (SET_MHS)
|
|
750
|
+
// Bit 0: DTR value, Bit 1: RTS value
|
|
751
|
+
// Bit 8: DTR mask (MUST be set to change DTR)
|
|
752
|
+
// Bit 9: RTS mask (MUST be set to change RTS)
|
|
753
|
+
|
|
754
|
+
let value = 0;
|
|
755
|
+
value |= dtr ? 1 : 0; // DTR value
|
|
756
|
+
value |= rts ? 2 : 0; // RTS value
|
|
757
|
+
value |= 0x100; // DTR mask (ALWAYS set)
|
|
758
|
+
value |= 0x200; // RTS mask (ALWAYS set)
|
|
759
|
+
|
|
760
|
+
await controlTransferOut(device, {
|
|
761
|
+
requestType: "vendor",
|
|
762
|
+
recipient: "device",
|
|
763
|
+
request: 0x07,
|
|
764
|
+
value: value,
|
|
765
|
+
index: 0x00,
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
async function setSignalsCH340(
|
|
770
|
+
device: Device,
|
|
771
|
+
dtr: boolean,
|
|
772
|
+
rts: boolean,
|
|
773
|
+
): Promise<void> {
|
|
774
|
+
const value = ~((dtr ? 1 << 5 : 0) | (rts ? 1 << 6 : 0)) & 0xffff;
|
|
775
|
+
|
|
776
|
+
await controlTransferOut(device, {
|
|
777
|
+
requestType: "vendor",
|
|
778
|
+
recipient: "device",
|
|
779
|
+
request: 0xa4,
|
|
780
|
+
value: value,
|
|
781
|
+
index: 0,
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
async function setSignalsFTDI(
|
|
786
|
+
device: Device,
|
|
787
|
+
dtr: boolean,
|
|
788
|
+
rts: boolean,
|
|
789
|
+
): Promise<void> {
|
|
790
|
+
let value = 0;
|
|
791
|
+
value |= dtr ? 1 : 0;
|
|
792
|
+
value |= rts ? 2 : 0;
|
|
793
|
+
value |= 0x0100 | 0x0200; // Masks
|
|
794
|
+
|
|
795
|
+
await controlTransferOut(device, {
|
|
796
|
+
requestType: "vendor",
|
|
797
|
+
recipient: "device",
|
|
798
|
+
request: 0x01,
|
|
799
|
+
value: value,
|
|
800
|
+
index: 0x00,
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
async function setSignalsCDC(
|
|
805
|
+
device: Device,
|
|
806
|
+
dtr: boolean,
|
|
807
|
+
rts: boolean,
|
|
808
|
+
interfaceNumber: number,
|
|
809
|
+
): Promise<void> {
|
|
810
|
+
let value = 0;
|
|
811
|
+
value |= dtr ? 1 : 0;
|
|
812
|
+
value |= rts ? 2 : 0;
|
|
813
|
+
|
|
814
|
+
await controlTransferOut(device, {
|
|
815
|
+
requestType: "class",
|
|
816
|
+
recipient: "interface",
|
|
817
|
+
request: 0x22,
|
|
818
|
+
value: value,
|
|
819
|
+
index: interfaceNumber,
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// Helper function for control transfers
|
|
824
|
+
|
|
825
|
+
interface ControlTransferParams {
|
|
826
|
+
requestType: "standard" | "class" | "vendor";
|
|
827
|
+
recipient: "device" | "interface" | "endpoint" | "other";
|
|
828
|
+
request: number;
|
|
829
|
+
value: number;
|
|
830
|
+
index: number;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
async function controlTransferOut(
|
|
834
|
+
device: Device,
|
|
835
|
+
params: ControlTransferParams,
|
|
836
|
+
data?: Buffer,
|
|
837
|
+
): Promise<void> {
|
|
838
|
+
return new Promise((resolve, reject) => {
|
|
839
|
+
// bmRequestType = Direction | Type | Recipient
|
|
840
|
+
// Direction: 0x00 = Host-to-Device (OUT), 0x80 = Device-to-Host (IN)
|
|
841
|
+
// Type: 0x00 = Standard, 0x20 = Class, 0x40 = Vendor
|
|
842
|
+
// Recipient: 0x00 = Device, 0x01 = Interface, 0x02 = Endpoint, 0x03 = Other
|
|
843
|
+
const bmRequestType =
|
|
844
|
+
0x00 | // Direction: Host-to-Device (OUT)
|
|
845
|
+
(params.requestType === "standard"
|
|
846
|
+
? 0x00
|
|
847
|
+
: params.requestType === "class"
|
|
848
|
+
? 0x20
|
|
849
|
+
: 0x40) |
|
|
850
|
+
(params.recipient === "device"
|
|
851
|
+
? 0x00
|
|
852
|
+
: params.recipient === "interface"
|
|
853
|
+
? 0x01
|
|
854
|
+
: params.recipient === "endpoint"
|
|
855
|
+
? 0x02
|
|
856
|
+
: 0x03);
|
|
857
|
+
|
|
858
|
+
device.controlTransfer(
|
|
859
|
+
bmRequestType,
|
|
860
|
+
params.request,
|
|
861
|
+
params.value,
|
|
862
|
+
params.index,
|
|
863
|
+
data || Buffer.alloc(0),
|
|
864
|
+
(err) => {
|
|
865
|
+
if (err) {
|
|
866
|
+
reject(err);
|
|
867
|
+
} else {
|
|
868
|
+
resolve();
|
|
869
|
+
}
|
|
870
|
+
},
|
|
871
|
+
);
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
/**
|
|
876
|
+
* List available USB serial devices
|
|
877
|
+
*/
|
|
878
|
+
export async function listUSBPorts(): Promise<
|
|
879
|
+
Array<{
|
|
880
|
+
path: string;
|
|
881
|
+
manufacturer?: string;
|
|
882
|
+
serialNumber?: string;
|
|
883
|
+
vendorId?: string;
|
|
884
|
+
productId?: string;
|
|
885
|
+
}>
|
|
886
|
+
> {
|
|
887
|
+
try {
|
|
888
|
+
const usb = await import("usb");
|
|
889
|
+
const devices = usb.getDeviceList();
|
|
890
|
+
|
|
891
|
+
const serialDevices = devices.filter((device) => {
|
|
892
|
+
const vid = device.deviceDescriptor.idVendor;
|
|
893
|
+
// Filter for known USB-Serial chips
|
|
894
|
+
return (
|
|
895
|
+
vid === 0x303a || // Espressif
|
|
896
|
+
vid === 0x0403 || // FTDI
|
|
897
|
+
vid === 0x1a86 || // CH340/CH343
|
|
898
|
+
vid === 0x10c4 || // CP210x
|
|
899
|
+
vid === 0x067b
|
|
900
|
+
); // PL2303
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
return serialDevices.map((device) => {
|
|
904
|
+
const vid = device.deviceDescriptor.idVendor;
|
|
905
|
+
const pid = device.deviceDescriptor.idProduct;
|
|
906
|
+
|
|
907
|
+
return {
|
|
908
|
+
path: `USB:${vid.toString(16)}:${pid.toString(16)}`,
|
|
909
|
+
manufacturer: undefined,
|
|
910
|
+
serialNumber: undefined,
|
|
911
|
+
vendorId: vid.toString(16).padStart(4, "0"),
|
|
912
|
+
productId: pid.toString(16).padStart(4, "0"),
|
|
913
|
+
};
|
|
914
|
+
});
|
|
915
|
+
} catch (err: any) {
|
|
916
|
+
if (
|
|
917
|
+
err.code === "ERR_MODULE_NOT_FOUND" ||
|
|
918
|
+
err.code === "MODULE_NOT_FOUND"
|
|
919
|
+
) {
|
|
920
|
+
throw new Error("usb package not installed. Run: npm install usb");
|
|
921
|
+
}
|
|
922
|
+
throw err;
|
|
923
|
+
}
|
|
924
|
+
}
|