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/flash.js
ADDED
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* CH347 SPI Flash Programmer
|
|
4
|
+
*
|
|
5
|
+
* Supports common SPI flash chips (W25Qxx, GD25Qxx, MX25Lxx, etc.)
|
|
6
|
+
*/
|
|
7
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
8
|
+
if (k2 === undefined) k2 = k;
|
|
9
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
10
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
11
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
12
|
+
}
|
|
13
|
+
Object.defineProperty(o, k2, desc);
|
|
14
|
+
}) : (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
o[k2] = m[k];
|
|
17
|
+
}));
|
|
18
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
19
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
20
|
+
}) : function(o, v) {
|
|
21
|
+
o["default"] = v;
|
|
22
|
+
});
|
|
23
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
24
|
+
var ownKeys = function(o) {
|
|
25
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
26
|
+
var ar = [];
|
|
27
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
28
|
+
return ar;
|
|
29
|
+
};
|
|
30
|
+
return ownKeys(o);
|
|
31
|
+
};
|
|
32
|
+
return function (mod) {
|
|
33
|
+
if (mod && mod.__esModule) return mod;
|
|
34
|
+
var result = {};
|
|
35
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
36
|
+
__setModuleDefault(result, mod);
|
|
37
|
+
return result;
|
|
38
|
+
};
|
|
39
|
+
})();
|
|
40
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
|
+
exports.CH347Flash = void 0;
|
|
42
|
+
const fs = __importStar(require("fs/promises"));
|
|
43
|
+
const constants_1 = require("./constants");
|
|
44
|
+
const types_1 = require("./types");
|
|
45
|
+
const DEFAULT_TIMEOUT_MS = 30000; // 30 seconds for chip erase
|
|
46
|
+
const POLL_INTERVAL_MS = 1; // 1ms polling
|
|
47
|
+
class CH347Flash {
|
|
48
|
+
spi;
|
|
49
|
+
flashInfo = null;
|
|
50
|
+
constructor(spi) {
|
|
51
|
+
this.spi = spi;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Read JEDEC ID from flash chip
|
|
55
|
+
*/
|
|
56
|
+
async readJedecId() {
|
|
57
|
+
const cmd = Buffer.from([constants_1.FLASH_CMD_READ_JEDEC_ID, 0, 0, 0]);
|
|
58
|
+
const response = await this.spi.transfer(cmd);
|
|
59
|
+
const manufacturerId = response[1];
|
|
60
|
+
const memoryType = response[2];
|
|
61
|
+
const capacity = response[3];
|
|
62
|
+
const jedecId = (manufacturerId << 16) | (memoryType << 8) | capacity;
|
|
63
|
+
// Look up in database
|
|
64
|
+
const dbEntry = types_1.FlashDatabase[jedecId];
|
|
65
|
+
let size = 0;
|
|
66
|
+
let name;
|
|
67
|
+
if (dbEntry) {
|
|
68
|
+
size = dbEntry.size;
|
|
69
|
+
name = dbEntry.name;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
// Calculate size from capacity byte (2^capacity bytes)
|
|
73
|
+
size = capacity > 0 ? 1 << capacity : 0;
|
|
74
|
+
}
|
|
75
|
+
const manufacturerName = types_1.FlashManufacturers[manufacturerId];
|
|
76
|
+
if (!name && manufacturerName) {
|
|
77
|
+
name = `${manufacturerName} (0x${jedecId.toString(16)})`;
|
|
78
|
+
}
|
|
79
|
+
this.flashInfo = {
|
|
80
|
+
manufacturerId,
|
|
81
|
+
memoryType,
|
|
82
|
+
capacity,
|
|
83
|
+
jedecId,
|
|
84
|
+
size,
|
|
85
|
+
name,
|
|
86
|
+
};
|
|
87
|
+
// Try to read SFDP parameters
|
|
88
|
+
await this.readSFDPBasicParams();
|
|
89
|
+
return this.flashInfo;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Read SFDP (Serial Flash Discoverable Parameters) data
|
|
93
|
+
* @param address 24-bit address in SFDP space
|
|
94
|
+
* @param length Number of bytes to read
|
|
95
|
+
*/
|
|
96
|
+
async readSFDP(address, length) {
|
|
97
|
+
// Command: 0x5A + 24-bit address + 1 dummy byte + read data
|
|
98
|
+
const cmd = Buffer.alloc(5 + length);
|
|
99
|
+
cmd[0] = constants_1.FLASH_CMD_READ_SFDP;
|
|
100
|
+
cmd[1] = (address >> 16) & 0xff;
|
|
101
|
+
cmd[2] = (address >> 8) & 0xff;
|
|
102
|
+
cmd[3] = address & 0xff;
|
|
103
|
+
cmd[4] = 0x00; // dummy byte
|
|
104
|
+
const response = await this.spi.transfer(cmd);
|
|
105
|
+
return response.subarray(5);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Estimate erase timeout based on block size
|
|
109
|
+
*/
|
|
110
|
+
estimateEraseTimeout(size) {
|
|
111
|
+
if (size <= 4096)
|
|
112
|
+
return 3000; // 4KB: 3s
|
|
113
|
+
if (size <= 32768)
|
|
114
|
+
return 5000; // 32KB: 5s
|
|
115
|
+
if (size <= 65536)
|
|
116
|
+
return 5000; // 64KB: 5s
|
|
117
|
+
return 10000; // Larger: 10s
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Read and parse SFDP Basic Flash Parameter Table
|
|
121
|
+
* Populates flashInfo with discovered erase types and page size
|
|
122
|
+
*/
|
|
123
|
+
async readSFDPBasicParams() {
|
|
124
|
+
if (!this.flashInfo)
|
|
125
|
+
return;
|
|
126
|
+
try {
|
|
127
|
+
// Read SFDP header (8 bytes at address 0x00)
|
|
128
|
+
const header = await this.readSFDP(0, 8);
|
|
129
|
+
// Validate signature "SFDP" (little-endian: 0x50444653)
|
|
130
|
+
const signature = header.readUInt32LE(0);
|
|
131
|
+
if (signature !== 0x50444653) {
|
|
132
|
+
return; // SFDP not supported
|
|
133
|
+
}
|
|
134
|
+
// Read first parameter header (8 bytes at address 0x08)
|
|
135
|
+
const paramHeader = await this.readSFDP(8, 8);
|
|
136
|
+
const tableLength = paramHeader[3] * 4; // length in DWORDs -> bytes
|
|
137
|
+
const tableAddress = paramHeader[4] | (paramHeader[5] << 8) | (paramHeader[6] << 16);
|
|
138
|
+
// Read Basic Flash Parameter Table (BFPT)
|
|
139
|
+
const bfpt = await this.readSFDP(tableAddress, Math.min(tableLength, 64));
|
|
140
|
+
// Parse erase types from DWORDs 8-9 (bytes 28-35)
|
|
141
|
+
// Each erase type is 2 bytes: [size_code, command]
|
|
142
|
+
// size_code: 2^N bytes, command: erase opcode
|
|
143
|
+
const eraseTypes = [];
|
|
144
|
+
for (let i = 0; i < 4; i++) {
|
|
145
|
+
const offset = 28 + i * 2;
|
|
146
|
+
if (offset + 1 >= bfpt.length)
|
|
147
|
+
break;
|
|
148
|
+
const sizeCode = bfpt[offset]; // 2^N bytes
|
|
149
|
+
const command = bfpt[offset + 1];
|
|
150
|
+
if (sizeCode > 0 && command > 0) {
|
|
151
|
+
const size = 1 << sizeCode;
|
|
152
|
+
eraseTypes.push({
|
|
153
|
+
command,
|
|
154
|
+
size,
|
|
155
|
+
timeoutMs: this.estimateEraseTimeout(size),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Parse page size from DWORD 11 (byte 40, bits 7-4)
|
|
160
|
+
let pageSize = constants_1.FLASH_PAGE_SIZE; // default 256
|
|
161
|
+
if (bfpt.length > 40) {
|
|
162
|
+
const pageSizeCode = (bfpt[40] >> 4) & 0x0f;
|
|
163
|
+
if (pageSizeCode > 0) {
|
|
164
|
+
pageSize = 1 << pageSizeCode;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// Update flash info with SFDP data
|
|
168
|
+
this.flashInfo.sfdpSupported = true;
|
|
169
|
+
this.flashInfo.pageSize = pageSize;
|
|
170
|
+
this.flashInfo.eraseTypes = eraseTypes.sort((a, b) => b.size - a.size);
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
// SFDP read failed - chip may not support it, use defaults
|
|
174
|
+
this.flashInfo.sfdpSupported = false;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Read status register
|
|
179
|
+
*/
|
|
180
|
+
async readStatus() {
|
|
181
|
+
const cmd = Buffer.from([constants_1.FLASH_CMD_READ_STATUS, 0]);
|
|
182
|
+
const response = await this.spi.transfer(cmd);
|
|
183
|
+
return response[1];
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Wait for flash to be ready (WIP bit cleared)
|
|
187
|
+
*/
|
|
188
|
+
async waitReady(timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
189
|
+
const startTime = Date.now();
|
|
190
|
+
while (true) {
|
|
191
|
+
const status = await this.readStatus();
|
|
192
|
+
if ((status & constants_1.FLASH_STATUS_WIP) === 0) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (Date.now() - startTime > timeoutMs) {
|
|
196
|
+
throw new Error('Flash operation timeout');
|
|
197
|
+
}
|
|
198
|
+
await this.delay(POLL_INTERVAL_MS);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Enable write operations
|
|
203
|
+
*/
|
|
204
|
+
async writeEnable() {
|
|
205
|
+
await this.spi.command(Buffer.from([constants_1.FLASH_CMD_WRITE_ENABLE]));
|
|
206
|
+
// Verify write enable latch is set
|
|
207
|
+
const status = await this.readStatus();
|
|
208
|
+
if ((status & constants_1.FLASH_STATUS_WEL) === 0) {
|
|
209
|
+
throw new Error('Failed to enable write');
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Disable write operations
|
|
214
|
+
*/
|
|
215
|
+
async writeDisable() {
|
|
216
|
+
await this.spi.command(Buffer.from([constants_1.FLASH_CMD_WRITE_DISABLE]));
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Read data from flash
|
|
220
|
+
*/
|
|
221
|
+
async read(address, length, onProgress) {
|
|
222
|
+
const result = Buffer.alloc(length);
|
|
223
|
+
const chunkSize = 4096; // Read in 4KB chunks for progress reporting
|
|
224
|
+
let offset = 0;
|
|
225
|
+
while (offset < length) {
|
|
226
|
+
const readLen = Math.min(chunkSize, length - offset);
|
|
227
|
+
const addr = address + offset;
|
|
228
|
+
// Build read command
|
|
229
|
+
const cmd = Buffer.alloc(4 + readLen);
|
|
230
|
+
cmd[0] = constants_1.FLASH_CMD_READ_DATA;
|
|
231
|
+
cmd[1] = (addr >> 16) & 0xff;
|
|
232
|
+
cmd[2] = (addr >> 8) & 0xff;
|
|
233
|
+
cmd[3] = addr & 0xff;
|
|
234
|
+
const response = await this.spi.transfer(cmd);
|
|
235
|
+
response.copy(result, offset, 4, 4 + readLen);
|
|
236
|
+
offset += readLen;
|
|
237
|
+
if (onProgress) {
|
|
238
|
+
onProgress({
|
|
239
|
+
operation: 'read',
|
|
240
|
+
current: offset,
|
|
241
|
+
total: length,
|
|
242
|
+
percentage: Math.round((offset / length) * 100),
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return result;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Write a single page (up to 256 bytes)
|
|
250
|
+
*/
|
|
251
|
+
async writePage(address, data) {
|
|
252
|
+
if (data.length > constants_1.FLASH_PAGE_SIZE) {
|
|
253
|
+
throw new Error(`Data exceeds page size: ${data.length} > ${constants_1.FLASH_PAGE_SIZE}`);
|
|
254
|
+
}
|
|
255
|
+
await this.writeEnable();
|
|
256
|
+
// Build page program command
|
|
257
|
+
const cmd = Buffer.alloc(4 + data.length);
|
|
258
|
+
cmd[0] = constants_1.FLASH_CMD_PAGE_PROGRAM;
|
|
259
|
+
cmd[1] = (address >> 16) & 0xff;
|
|
260
|
+
cmd[2] = (address >> 8) & 0xff;
|
|
261
|
+
cmd[3] = address & 0xff;
|
|
262
|
+
data.copy(cmd, 4);
|
|
263
|
+
await this.spi.command(cmd);
|
|
264
|
+
await this.waitReady(1000); // Page program timeout 1 second
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Write data to flash (handles page boundaries)
|
|
268
|
+
*/
|
|
269
|
+
async write(address, data, onProgress) {
|
|
270
|
+
let offset = 0;
|
|
271
|
+
while (offset < data.length) {
|
|
272
|
+
// Calculate bytes remaining in current page
|
|
273
|
+
const pageOffset = (address + offset) % constants_1.FLASH_PAGE_SIZE;
|
|
274
|
+
const pageRemaining = constants_1.FLASH_PAGE_SIZE - pageOffset;
|
|
275
|
+
const writeLen = Math.min(pageRemaining, data.length - offset);
|
|
276
|
+
// Write page
|
|
277
|
+
const chunk = data.subarray(offset, offset + writeLen);
|
|
278
|
+
await this.writePage(address + offset, chunk);
|
|
279
|
+
offset += writeLen;
|
|
280
|
+
if (onProgress) {
|
|
281
|
+
onProgress({
|
|
282
|
+
operation: 'write',
|
|
283
|
+
current: offset,
|
|
284
|
+
total: data.length,
|
|
285
|
+
percentage: Math.round((offset / data.length) * 100),
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Erase a 4KB sector
|
|
292
|
+
*/
|
|
293
|
+
async eraseSector(address) {
|
|
294
|
+
// Align to sector boundary
|
|
295
|
+
const sectorAddress = address & ~(constants_1.FLASH_SECTOR_SIZE - 1);
|
|
296
|
+
await this.writeEnable();
|
|
297
|
+
const cmd = Buffer.from([
|
|
298
|
+
constants_1.FLASH_CMD_SECTOR_ERASE,
|
|
299
|
+
(sectorAddress >> 16) & 0xff,
|
|
300
|
+
(sectorAddress >> 8) & 0xff,
|
|
301
|
+
sectorAddress & 0xff,
|
|
302
|
+
]);
|
|
303
|
+
await this.spi.command(cmd);
|
|
304
|
+
await this.waitReady(3000); // Sector erase timeout 3 seconds
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Erase a 32KB block
|
|
308
|
+
*/
|
|
309
|
+
async eraseBlock32K(address) {
|
|
310
|
+
const blockAddress = address & ~(constants_1.FLASH_BLOCK_SIZE_32K - 1);
|
|
311
|
+
await this.writeEnable();
|
|
312
|
+
const cmd = Buffer.from([
|
|
313
|
+
constants_1.FLASH_CMD_BLOCK_ERASE_32K,
|
|
314
|
+
(blockAddress >> 16) & 0xff,
|
|
315
|
+
(blockAddress >> 8) & 0xff,
|
|
316
|
+
blockAddress & 0xff,
|
|
317
|
+
]);
|
|
318
|
+
await this.spi.command(cmd);
|
|
319
|
+
await this.waitReady(5000); // Block erase timeout 5 seconds
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Erase a 64KB block
|
|
323
|
+
*/
|
|
324
|
+
async eraseBlock64K(address) {
|
|
325
|
+
const blockAddress = address & ~(constants_1.FLASH_BLOCK_SIZE_64K - 1);
|
|
326
|
+
await this.writeEnable();
|
|
327
|
+
const cmd = Buffer.from([
|
|
328
|
+
constants_1.FLASH_CMD_BLOCK_ERASE_64K,
|
|
329
|
+
(blockAddress >> 16) & 0xff,
|
|
330
|
+
(blockAddress >> 8) & 0xff,
|
|
331
|
+
blockAddress & 0xff,
|
|
332
|
+
]);
|
|
333
|
+
await this.spi.command(cmd);
|
|
334
|
+
await this.waitReady(5000); // Block erase timeout 5 seconds
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Erase entire chip
|
|
338
|
+
*/
|
|
339
|
+
async eraseChip(onProgress) {
|
|
340
|
+
await this.writeEnable();
|
|
341
|
+
await this.spi.command(Buffer.from([constants_1.FLASH_CMD_CHIP_ERASE]));
|
|
342
|
+
// Poll for completion with progress
|
|
343
|
+
const startTime = Date.now();
|
|
344
|
+
const timeout = DEFAULT_TIMEOUT_MS;
|
|
345
|
+
while (true) {
|
|
346
|
+
const status = await this.readStatus();
|
|
347
|
+
if ((status & constants_1.FLASH_STATUS_WIP) === 0) {
|
|
348
|
+
if (onProgress) {
|
|
349
|
+
onProgress({
|
|
350
|
+
operation: 'erase',
|
|
351
|
+
current: 100,
|
|
352
|
+
total: 100,
|
|
353
|
+
percentage: 100,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
const elapsed = Date.now() - startTime;
|
|
359
|
+
if (elapsed > timeout) {
|
|
360
|
+
throw new Error('Chip erase timeout');
|
|
361
|
+
}
|
|
362
|
+
if (onProgress) {
|
|
363
|
+
// Estimate progress based on typical chip erase time
|
|
364
|
+
const estimatedTime = 30000; // 30 seconds typical
|
|
365
|
+
const progress = Math.min(99, Math.round((elapsed / estimatedTime) * 100));
|
|
366
|
+
onProgress({
|
|
367
|
+
operation: 'erase',
|
|
368
|
+
current: progress,
|
|
369
|
+
total: 100,
|
|
370
|
+
percentage: progress,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
await this.delay(100);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Erase using a specific erase type (from SFDP discovery)
|
|
378
|
+
*/
|
|
379
|
+
async eraseWithType(address, eraseType) {
|
|
380
|
+
// Align to erase boundary
|
|
381
|
+
const alignedAddress = address & ~(eraseType.size - 1);
|
|
382
|
+
await this.writeEnable();
|
|
383
|
+
const cmd = Buffer.from([
|
|
384
|
+
eraseType.command,
|
|
385
|
+
(alignedAddress >> 16) & 0xff,
|
|
386
|
+
(alignedAddress >> 8) & 0xff,
|
|
387
|
+
alignedAddress & 0xff,
|
|
388
|
+
]);
|
|
389
|
+
await this.spi.command(cmd);
|
|
390
|
+
await this.waitReady(eraseType.timeoutMs);
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Get default erase types (fallback when SFDP not available)
|
|
394
|
+
*/
|
|
395
|
+
getDefaultEraseTypes() {
|
|
396
|
+
return [
|
|
397
|
+
{ command: constants_1.FLASH_CMD_BLOCK_ERASE_64K, size: constants_1.FLASH_BLOCK_SIZE_64K, timeoutMs: 5000 },
|
|
398
|
+
{ command: constants_1.FLASH_CMD_BLOCK_ERASE_32K, size: constants_1.FLASH_BLOCK_SIZE_32K, timeoutMs: 5000 },
|
|
399
|
+
{ command: constants_1.FLASH_CMD_SECTOR_ERASE, size: constants_1.FLASH_SECTOR_SIZE, timeoutMs: 3000 },
|
|
400
|
+
];
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Erase range (uses optimal erase commands from SFDP or defaults)
|
|
404
|
+
*/
|
|
405
|
+
async eraseRange(address, length, onProgress) {
|
|
406
|
+
const endAddress = address + length;
|
|
407
|
+
let currentAddress = address;
|
|
408
|
+
let erased = 0;
|
|
409
|
+
// Use SFDP-discovered erase types or fall back to defaults
|
|
410
|
+
const eraseTypes = this.flashInfo?.eraseTypes ?? this.getDefaultEraseTypes();
|
|
411
|
+
while (currentAddress < endAddress) {
|
|
412
|
+
const remaining = endAddress - currentAddress;
|
|
413
|
+
// Find the largest erase type that fits
|
|
414
|
+
let usedEraseType = null;
|
|
415
|
+
for (const eraseType of eraseTypes) {
|
|
416
|
+
if ((currentAddress % eraseType.size === 0) &&
|
|
417
|
+
remaining >= eraseType.size) {
|
|
418
|
+
usedEraseType = eraseType;
|
|
419
|
+
break; // eraseTypes are sorted by size descending
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
if (usedEraseType) {
|
|
423
|
+
await this.eraseWithType(currentAddress, usedEraseType);
|
|
424
|
+
currentAddress += usedEraseType.size;
|
|
425
|
+
erased += usedEraseType.size;
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
// Fallback to smallest erase type available
|
|
429
|
+
const smallestErase = eraseTypes[eraseTypes.length - 1];
|
|
430
|
+
await this.eraseWithType(currentAddress, smallestErase);
|
|
431
|
+
currentAddress += smallestErase.size;
|
|
432
|
+
erased += smallestErase.size;
|
|
433
|
+
}
|
|
434
|
+
if (onProgress) {
|
|
435
|
+
onProgress({
|
|
436
|
+
operation: 'erase',
|
|
437
|
+
current: Math.min(erased, length),
|
|
438
|
+
total: length,
|
|
439
|
+
percentage: Math.round((Math.min(erased, length) / length) * 100),
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Verify data matches flash contents
|
|
446
|
+
*/
|
|
447
|
+
async verify(address, data, onProgress) {
|
|
448
|
+
const readData = await this.read(address, data.length, (progress) => {
|
|
449
|
+
if (onProgress) {
|
|
450
|
+
onProgress({
|
|
451
|
+
...progress,
|
|
452
|
+
operation: 'verify',
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
return data.equals(readData);
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Check if a sector needs to be erased to write new data
|
|
460
|
+
* Flash can only program 1→0, so we need to erase if any bit needs to go 0→1
|
|
461
|
+
*/
|
|
462
|
+
sectorNeedsErase(original, newData) {
|
|
463
|
+
for (let i = 0; i < newData.length; i++) {
|
|
464
|
+
// If (original & new) !== new, some bits need to go from 0 to 1, requiring erase
|
|
465
|
+
if ((original[i] & newData[i]) !== newData[i]) {
|
|
466
|
+
return true;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Check if a sector has any changes
|
|
473
|
+
*/
|
|
474
|
+
sectorHasChanges(original, newData) {
|
|
475
|
+
return !original.equals(newData);
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Program flash (smart erase + write + verify)
|
|
479
|
+
* Only erases and writes sectors that have actual changes
|
|
480
|
+
*/
|
|
481
|
+
async program(address, data, options = {}) {
|
|
482
|
+
const { erase = true, verify = true, onProgress } = options;
|
|
483
|
+
// Read original content to compare
|
|
484
|
+
const original = await this.read(address, data.length);
|
|
485
|
+
// Analyze which sectors need erasing and which need writing
|
|
486
|
+
const sectorsToErase = [];
|
|
487
|
+
const sectorsToWrite = [];
|
|
488
|
+
const startSector = Math.floor(address / constants_1.FLASH_SECTOR_SIZE);
|
|
489
|
+
const endAddress = address + data.length;
|
|
490
|
+
const endSector = Math.ceil(endAddress / constants_1.FLASH_SECTOR_SIZE);
|
|
491
|
+
for (let sector = startSector; sector < endSector; sector++) {
|
|
492
|
+
const sectorStart = sector * constants_1.FLASH_SECTOR_SIZE;
|
|
493
|
+
const sectorEnd = sectorStart + constants_1.FLASH_SECTOR_SIZE;
|
|
494
|
+
// Calculate overlap between this sector and our data range
|
|
495
|
+
const overlapStart = Math.max(sectorStart, address);
|
|
496
|
+
const overlapEnd = Math.min(sectorEnd, endAddress);
|
|
497
|
+
// Get the corresponding slices
|
|
498
|
+
const dataOffset = overlapStart - address;
|
|
499
|
+
const dataSlice = data.subarray(dataOffset, dataOffset + (overlapEnd - overlapStart));
|
|
500
|
+
const originalSlice = original.subarray(dataOffset, dataOffset + (overlapEnd - overlapStart));
|
|
501
|
+
if (this.sectorHasChanges(originalSlice, dataSlice)) {
|
|
502
|
+
sectorsToWrite.push(sector);
|
|
503
|
+
if (erase && this.sectorNeedsErase(originalSlice, dataSlice)) {
|
|
504
|
+
sectorsToErase.push(sector);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
// Calculate progress steps
|
|
509
|
+
const totalWork = sectorsToErase.length + sectorsToWrite.length + (verify ? 1 : 0);
|
|
510
|
+
let completedWork = 0;
|
|
511
|
+
const reportProgress = (operation) => {
|
|
512
|
+
if (onProgress) {
|
|
513
|
+
onProgress({
|
|
514
|
+
operation,
|
|
515
|
+
current: completedWork,
|
|
516
|
+
total: totalWork,
|
|
517
|
+
percentage: totalWork > 0 ? Math.round((completedWork / totalWork) * 100) : 100,
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
// Erase only the sectors that need it
|
|
522
|
+
for (const sector of sectorsToErase) {
|
|
523
|
+
await this.eraseSector(sector * constants_1.FLASH_SECTOR_SIZE);
|
|
524
|
+
completedWork++;
|
|
525
|
+
reportProgress('erase');
|
|
526
|
+
}
|
|
527
|
+
// Write only the sectors that have changes
|
|
528
|
+
for (const sector of sectorsToWrite) {
|
|
529
|
+
const sectorStart = sector * constants_1.FLASH_SECTOR_SIZE;
|
|
530
|
+
const sectorEnd = sectorStart + constants_1.FLASH_SECTOR_SIZE;
|
|
531
|
+
const overlapStart = Math.max(sectorStart, address);
|
|
532
|
+
const overlapEnd = Math.min(sectorEnd, endAddress);
|
|
533
|
+
const dataOffset = overlapStart - address;
|
|
534
|
+
const dataSlice = data.subarray(dataOffset, dataOffset + (overlapEnd - overlapStart));
|
|
535
|
+
await this.write(overlapStart, dataSlice);
|
|
536
|
+
completedWork++;
|
|
537
|
+
reportProgress('write');
|
|
538
|
+
}
|
|
539
|
+
// Verify
|
|
540
|
+
if (verify) {
|
|
541
|
+
const verified = await this.verify(address, data);
|
|
542
|
+
completedWork++;
|
|
543
|
+
reportProgress('verify');
|
|
544
|
+
if (!verified) {
|
|
545
|
+
throw new Error('Verification failed');
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return true;
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Program flash from a binary file
|
|
552
|
+
* @param filePath Path to the binary file to write
|
|
553
|
+
* @param address Starting address in flash (default: 0, requires file size to match flash size)
|
|
554
|
+
* @param options Programming options (erase, verify, progress callback)
|
|
555
|
+
*/
|
|
556
|
+
async programFile(filePath, address, options = {}) {
|
|
557
|
+
const data = await fs.readFile(filePath);
|
|
558
|
+
// If no address specified, default to 0 but require file size to match flash size
|
|
559
|
+
if (address === undefined) {
|
|
560
|
+
if (!this.flashInfo?.size) {
|
|
561
|
+
throw new Error('Flash size unknown. Call readJedecId() first or specify an address.');
|
|
562
|
+
}
|
|
563
|
+
if (data.length !== this.flashInfo.size) {
|
|
564
|
+
throw new Error(`File size (${data.length} bytes) does not match flash size (${this.flashInfo.size} bytes). ` +
|
|
565
|
+
`Specify an address explicitly to write partial data.`);
|
|
566
|
+
}
|
|
567
|
+
address = 0;
|
|
568
|
+
}
|
|
569
|
+
return this.program(address, data, options);
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Read flash contents and save to a binary file
|
|
573
|
+
* @param filePath Path to save the binary file
|
|
574
|
+
* @param address Starting address in flash (default: 0)
|
|
575
|
+
* @param length Number of bytes to read (if not specified, reads entire flash based on JEDEC ID)
|
|
576
|
+
* @param onProgress Progress callback
|
|
577
|
+
*/
|
|
578
|
+
async readToFile(filePath, address = 0, length, onProgress) {
|
|
579
|
+
const readLength = length ?? this.flashInfo?.size;
|
|
580
|
+
if (!readLength) {
|
|
581
|
+
throw new Error('Length not specified and flash size unknown. Call readJedecId() first or specify length.');
|
|
582
|
+
}
|
|
583
|
+
const data = await this.read(address, readLength, onProgress);
|
|
584
|
+
await fs.writeFile(filePath, data);
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Get flash info (call readJedecId first)
|
|
588
|
+
*/
|
|
589
|
+
getFlashInfo() {
|
|
590
|
+
return this.flashInfo;
|
|
591
|
+
}
|
|
592
|
+
delay(ms) {
|
|
593
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
exports.CH347Flash = CH347Flash;
|
|
597
|
+
//# sourceMappingURL=flash.js.map
|
package/dist/gpio.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CH347 GPIO Control
|
|
3
|
+
*
|
|
4
|
+
* Protocol from ch347-gpio.c:
|
|
5
|
+
* - GPIO buffer is 11 bytes: [0xCC, length(8), 0x00, pin0, pin1, pin2, pin3, pin4, pin5, pin6, pin7]
|
|
6
|
+
* - Each pin byte has control bits:
|
|
7
|
+
* - Bit 7 (0x80): In response, indicates output direction
|
|
8
|
+
* - Bit 6 (0x40): Enable pin change (out) / Current value (in)
|
|
9
|
+
* - Bit 5-4 (0x30): Set direction to output
|
|
10
|
+
* - Bit 3 (0x08): Set pin value high
|
|
11
|
+
*/
|
|
12
|
+
import { CH347USB } from './usb';
|
|
13
|
+
import { GPIOConfig, GPIOState } from './types';
|
|
14
|
+
export declare class CH347GPIO {
|
|
15
|
+
private usb;
|
|
16
|
+
private pinStates;
|
|
17
|
+
constructor(usb: CH347USB);
|
|
18
|
+
/**
|
|
19
|
+
* Build GPIO command buffer
|
|
20
|
+
*/
|
|
21
|
+
private buildCommandBuffer;
|
|
22
|
+
/**
|
|
23
|
+
* Parse GPIO response buffer
|
|
24
|
+
*/
|
|
25
|
+
private parseResponse;
|
|
26
|
+
/**
|
|
27
|
+
* Read all GPIO states
|
|
28
|
+
*/
|
|
29
|
+
readAll(): Promise<GPIOState[]>;
|
|
30
|
+
/**
|
|
31
|
+
* Read single GPIO pin state
|
|
32
|
+
*/
|
|
33
|
+
read(pin: number): Promise<GPIOState>;
|
|
34
|
+
/**
|
|
35
|
+
* Configure GPIO pin direction
|
|
36
|
+
*/
|
|
37
|
+
setDirection(pin: number, direction: 'input' | 'output'): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Set GPIO output value
|
|
40
|
+
*/
|
|
41
|
+
write(pin: number, value: boolean): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Set multiple GPIO pins at once
|
|
44
|
+
*/
|
|
45
|
+
writeMultiple(pins: {
|
|
46
|
+
pin: number;
|
|
47
|
+
value: boolean;
|
|
48
|
+
}[]): Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* Configure multiple GPIO pins
|
|
51
|
+
*/
|
|
52
|
+
configure(configs: GPIOConfig[]): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Pulse a GPIO pin (useful for buttons)
|
|
55
|
+
*/
|
|
56
|
+
pulse(pin: number, durationMs?: number, activeHigh?: boolean): Promise<void>;
|
|
57
|
+
/**
|
|
58
|
+
* Toggle GPIO pin
|
|
59
|
+
*/
|
|
60
|
+
toggle(pin: number): Promise<boolean>;
|
|
61
|
+
/**
|
|
62
|
+
* Get cached pin states (call readAll() to refresh)
|
|
63
|
+
*/
|
|
64
|
+
getPinStates(): GPIOState[];
|
|
65
|
+
private delay;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=gpio.d.ts.map
|