2025-02-23 10:19:53 +01:00
|
|
|
import * as zip from "./zip.min.js";
|
|
|
|
|
|
|
|
|
|
// Constants adapted from dfu/dfu_transport_serial.py
|
|
|
|
|
const DFU_TOUCH_BAUD = 1200;
|
|
|
|
|
const SERIAL_PORT_OPEN_WAIT_TIME = 0.1;
|
|
|
|
|
const TOUCH_RESET_WAIT_TIME = 1.5;
|
|
|
|
|
|
|
|
|
|
const DEFAULT_SERIAL_PORT_TIMEOUT = 1.0; // Timeout time on serial port read
|
|
|
|
|
const FLASH_PAGE_SIZE = 4096;
|
|
|
|
|
const FLASH_PAGE_ERASE_TIME = 0.0897; // nRF52840 max erase time
|
|
|
|
|
const FLASH_WORD_WRITE_TIME = 0.000100; // nRF52840 max write time
|
|
|
|
|
const FLASH_PAGE_WRITE_TIME = (FLASH_PAGE_SIZE / 4) * FLASH_WORD_WRITE_TIME;
|
|
|
|
|
const DFU_PACKET_MAX_SIZE = 512;
|
|
|
|
|
|
|
|
|
|
const DATA_INTEGRITY_CHECK_PRESENT = 1;
|
|
|
|
|
const RELIABLE_PACKET = 1;
|
|
|
|
|
const HCI_PACKET_TYPE = 14;
|
|
|
|
|
|
|
|
|
|
const DFU_INIT_PACKET = 1;
|
|
|
|
|
const DFU_START_PACKET = 3;
|
|
|
|
|
const DFU_DATA_PACKET = 4;
|
|
|
|
|
const DFU_STOP_DATA_PACKET = 5;
|
|
|
|
|
const DFU_ERASE_PAGE = 6; // Added for explicit page erase
|
|
|
|
|
|
|
|
|
|
const DFU_UPDATE_MODE_APP = 4;
|
|
|
|
|
|
|
|
|
|
// --- Utility Functions (adapted from dfu/util.py) ---
|
|
|
|
|
|
|
|
|
|
function int32ToBytes(value) {
|
2025-02-23 10:27:17 +01:00
|
|
|
const buffer = new ArrayBuffer(4);
|
|
|
|
|
const view = new DataView(buffer);
|
|
|
|
|
view.setUint32(0, value, true); // Little-endian
|
|
|
|
|
return new Uint8Array(buffer);
|
2025-02-23 10:19:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function int16ToBytes(value) {
|
2025-02-23 10:27:17 +01:00
|
|
|
const buffer = new ArrayBuffer(2);
|
|
|
|
|
const view = new DataView(buffer);
|
|
|
|
|
view.setUint16(0, value, true); // Little-endian
|
|
|
|
|
return new Uint8Array(buffer);
|
2025-02-23 10:19:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function slipPartsToFourBytes(seq, dip, rp, pktType, pktLen) {
|
2025-02-23 10:27:17 +01:00
|
|
|
const ints = new Uint8Array(4);
|
|
|
|
|
ints[0] = seq | (((seq + 1) % 8) << 3) | (dip << 6) | (rp << 7);
|
|
|
|
|
ints[1] = pktType | ((pktLen & 0x000F) << 4);
|
|
|
|
|
ints[2] = (pktLen & 0x0FF0) >> 4;
|
|
|
|
|
ints[3] = (~(ints[0] + ints[1] + ints[2]) + 1) & 0xFF;
|
|
|
|
|
return ints;
|
2025-02-23 10:19:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function slipEncodeEscChars(data) {
|
2025-02-23 10:27:17 +01:00
|
|
|
const result = [];
|
|
|
|
|
for (const byte of data) {
|
|
|
|
|
if (byte === 0xC0) {
|
|
|
|
|
result.push(0xDB, 0xDC);
|
|
|
|
|
} else if (byte === 0xDB) {
|
|
|
|
|
result.push(0xDB, 0xDD);
|
|
|
|
|
} else {
|
|
|
|
|
result.push(byte);
|
2025-02-23 10:19:53 +01:00
|
|
|
}
|
2025-02-23 10:27:17 +01:00
|
|
|
}
|
|
|
|
|
return new Uint8Array(result);
|
2025-02-23 10:19:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- CRC16 Calculation (adapted from dfu/crc16.py) ---
|
|
|
|
|
|
|
|
|
|
function calcCrc16(data, crc = 0xFFFF) {
|
2025-02-23 10:27:17 +01:00
|
|
|
if (!(data instanceof Uint8Array)) {
|
|
|
|
|
throw new Error("calcCrc16 requires Uint8Array input");
|
|
|
|
|
}
|
|
|
|
|
for (let i = 0; i < data.length; i++) {
|
|
|
|
|
crc = ((crc >> 8) & 0x00FF) | ((crc << 8) & 0xFF00);
|
|
|
|
|
crc ^= data[i];
|
|
|
|
|
crc ^= (crc & 0x00FF) >> 4;
|
|
|
|
|
crc ^= (crc << 8) << 4;
|
|
|
|
|
crc ^= ((crc & 0x00FF) << 4) << 1;
|
|
|
|
|
}
|
|
|
|
|
return crc & 0xFFFF;
|
2025-02-23 10:19:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function sleep(milliseconds) {
|
2026-03-26 13:14:06 +01:00
|
|
|
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
2025-02-23 10:19:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- HciPacket Class (adapted from dfu/dfu_transport_serial.py) ---
|
|
|
|
|
|
|
|
|
|
class HciPacket {
|
2025-02-23 10:27:17 +01:00
|
|
|
static sequenceNumber = 0;
|
|
|
|
|
|
|
|
|
|
constructor(data) {
|
|
|
|
|
HciPacket.sequenceNumber = (HciPacket.sequenceNumber + 1) % 8;
|
|
|
|
|
let tempData = [];
|
|
|
|
|
|
|
|
|
|
const slipBytes = slipPartsToFourBytes(
|
|
|
|
|
HciPacket.sequenceNumber,
|
|
|
|
|
DATA_INTEGRITY_CHECK_PRESENT,
|
|
|
|
|
RELIABLE_PACKET,
|
|
|
|
|
HCI_PACKET_TYPE,
|
|
|
|
|
data.length
|
|
|
|
|
);
|
|
|
|
|
tempData = tempData.concat(Array.from(slipBytes));
|
|
|
|
|
|
|
|
|
|
tempData = tempData.concat(Array.from(data));
|
|
|
|
|
|
|
|
|
|
// Add CRC
|
|
|
|
|
const crc = calcCrc16(new Uint8Array(tempData));
|
|
|
|
|
tempData.push(crc & 0xFF);
|
|
|
|
|
tempData.push((crc & 0xFF00) >> 8);
|
|
|
|
|
|
|
|
|
|
const encoded = slipEncodeEscChars(new Uint8Array(tempData));
|
|
|
|
|
this.data = new Uint8Array([0xC0, ...encoded, 0xC0]);
|
|
|
|
|
}
|
2025-02-23 10:19:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// --- Main DFU Class ---
|
|
|
|
|
|
|
|
|
|
export class Dfu {
|
2025-02-23 10:27:17 +01:00
|
|
|
/**
|
|
|
|
|
* @param {SerialPort} port - The Web Serial API port object.
|
|
|
|
|
* @param {boolean} [eraseBeforeUpdate=false] - Whether to erase the entire flash before updating.
|
|
|
|
|
*/
|
|
|
|
|
constructor(port, eraseBeforeUpdate = false) {
|
|
|
|
|
this.port = port;
|
|
|
|
|
this.transferInProgress = false;
|
|
|
|
|
this.lastAck = -1;
|
|
|
|
|
this.eraseBeforeUpdate = eraseBeforeUpdate; // Store the erase flag
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-10 23:15:50 +01:00
|
|
|
getReader() {
|
|
|
|
|
const reader = this.port.readable.getReader();
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
read() {
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
const timeoutHandle = setTimeout(() => {
|
|
|
|
|
reader.releaseLock();
|
|
|
|
|
reject(new Error("Read timeout"));
|
|
|
|
|
}, DEFAULT_SERIAL_PORT_TIMEOUT * 1000 * 5)
|
|
|
|
|
|
|
|
|
|
reader.read().then(result => {
|
|
|
|
|
clearTimeout(timeoutHandle);
|
|
|
|
|
resolve(result);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
releaseLock() {
|
|
|
|
|
return reader.releaseLock();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-23 10:27:17 +01:00
|
|
|
async sendPacket(pkt) {
|
|
|
|
|
if (!this.port || !this.port.writable) {
|
|
|
|
|
throw new Error("Serial port not open or not writable.");
|
2025-02-23 10:19:53 +01:00
|
|
|
}
|
|
|
|
|
|
2025-02-23 10:27:17 +01:00
|
|
|
const writer = this.port.writable.getWriter();
|
|
|
|
|
try {
|
|
|
|
|
await writer.write(pkt.data);
|
2025-02-25 10:54:04 +01:00
|
|
|
console.debug("Sent packet:", pkt.data.length);
|
2025-02-23 10:27:17 +01:00
|
|
|
} finally {
|
|
|
|
|
writer.releaseLock();
|
|
|
|
|
}
|
2025-02-23 10:19:53 +01:00
|
|
|
|
2025-02-23 10:27:17 +01:00
|
|
|
await this.getAck(); // Wait for ACK after sending
|
|
|
|
|
}
|
2025-02-23 10:19:53 +01:00
|
|
|
|
2025-02-23 10:27:17 +01:00
|
|
|
async getAck() {
|
|
|
|
|
if (!this.port || !this.port.readable) {
|
|
|
|
|
throw new Error("Serial port not open or not readable.");
|
2025-02-23 10:19:53 +01:00
|
|
|
}
|
|
|
|
|
|
2026-01-10 23:15:50 +01:00
|
|
|
const reader = this.getReader();
|
2025-02-23 10:27:17 +01:00
|
|
|
let buffer = [];
|
|
|
|
|
let c0Count = 0;
|
2025-02-23 10:19:53 +01:00
|
|
|
|
2025-02-23 10:27:17 +01:00
|
|
|
try {
|
|
|
|
|
const startTime = Date.now();
|
|
|
|
|
while (c0Count < 2) {
|
|
|
|
|
const { value, done } = await reader.read();
|
|
|
|
|
if (done) {
|
|
|
|
|
throw new Error("Stream closed before receiving full ACK.");
|
2025-02-23 10:19:53 +01:00
|
|
|
}
|
|
|
|
|
|
2025-02-23 10:27:17 +01:00
|
|
|
if (value) {
|
|
|
|
|
for (const byte of value) {
|
|
|
|
|
buffer.push(byte);
|
|
|
|
|
if (byte === 0xC0) {
|
|
|
|
|
c0Count++;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-02-23 10:19:53 +01:00
|
|
|
}
|
2025-02-23 10:27:17 +01:00
|
|
|
}
|
2026-01-10 23:15:50 +01:00
|
|
|
}
|
|
|
|
|
catch(e) {
|
|
|
|
|
HciPacket.sequenceNumber = 0;
|
2026-03-26 13:14:06 +01:00
|
|
|
throw e;
|
2026-01-10 23:15:50 +01:00
|
|
|
}
|
|
|
|
|
finally {
|
2025-02-23 10:27:17 +01:00
|
|
|
reader.releaseLock();
|
2025-02-23 10:19:53 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-26 13:14:06 +01:00
|
|
|
// Extract the SLIP frame between the two 0xC0 delimiters, ignoring any
|
|
|
|
|
// stale bytes that arrived before the opening delimiter.
|
|
|
|
|
const firstC0 = buffer.indexOf(0xC0);
|
|
|
|
|
const secondC0 = buffer.indexOf(0xC0, firstC0 + 1);
|
|
|
|
|
if (firstC0 === -1 || secondC0 === -1) {
|
|
|
|
|
throw new Error("Received incomplete ACK.");
|
|
|
|
|
}
|
|
|
|
|
const decodedData = this.decodeSlip(buffer.slice(firstC0 + 1, secondC0));
|
|
|
|
|
|
|
|
|
|
if (decodedData.length < 2) {
|
2025-02-23 10:27:17 +01:00
|
|
|
throw new Error("Received incomplete ACK.");
|
2025-02-23 10:19:53 +01:00
|
|
|
}
|
2025-02-23 10:27:17 +01:00
|
|
|
const ack = (decodedData[0] >> 3) & 0x07;
|
2025-02-23 10:19:53 +01:00
|
|
|
|
2025-02-23 10:27:17 +01:00
|
|
|
// Check for valid ACK sequence
|
|
|
|
|
if (this.lastAck !== -1 && ack !== (this.lastAck + 1) % 8) {
|
|
|
|
|
HciPacket.sequenceNumber = 0; // Reset on bad ack
|
|
|
|
|
throw new Error(`Invalid ACK sequence. Expected ${(this.lastAck + 1) % 8}, got ${ack}`);
|
2025-02-23 10:19:53 +01:00
|
|
|
}
|
2025-02-23 10:27:17 +01:00
|
|
|
this.lastAck = ack;
|
2025-02-23 10:19:53 +01:00
|
|
|
|
2025-02-23 10:27:17 +01:00
|
|
|
return ack;
|
|
|
|
|
}
|
2025-02-23 10:19:53 +01:00
|
|
|
|
2025-02-23 10:27:17 +01:00
|
|
|
decodeSlip(data) {
|
|
|
|
|
const result = [];
|
|
|
|
|
let i = 0;
|
|
|
|
|
while (i < data.length) {
|
|
|
|
|
if (data[i] === 0xDB) {
|
|
|
|
|
i++;
|
|
|
|
|
if (i >= data.length) {
|
|
|
|
|
throw new Error("Invalid SLIP escape sequence: incomplete.");
|
|
|
|
|
}
|
|
|
|
|
if (data[i] === 0xDC) {
|
|
|
|
|
result.push(0xC0);
|
|
|
|
|
} else if (data[i] === 0xDD) {
|
|
|
|
|
result.push(0xDB);
|
|
|
|
|
} else {
|
|
|
|
|
throw new Error(`Invalid SLIP escape sequence: DB followed by ${data[i].toString(16)}`);
|
|
|
|
|
}
|
|
|
|
|
} else if (data[i] === 0xC0) {
|
|
|
|
|
// Ignore 0xC0 (start/end of packet)
|
|
|
|
|
}
|
2026-01-10 23:15:50 +01:00
|
|
|
else {
|
2025-02-23 10:27:17 +01:00
|
|
|
result.push(data[i]);
|
|
|
|
|
}
|
|
|
|
|
i++;
|
2025-02-23 10:19:53 +01:00
|
|
|
}
|
2025-02-23 10:27:17 +01:00
|
|
|
return new Uint8Array(result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async sendInitPacket(initPacket) {
|
|
|
|
|
const frame = new Uint8Array([
|
|
|
|
|
...int32ToBytes(DFU_INIT_PACKET),
|
|
|
|
|
...initPacket,
|
|
|
|
|
...int16ToBytes(0x0000), // Padding
|
|
|
|
|
]);
|
|
|
|
|
const packet = new HciPacket(frame);
|
|
|
|
|
await this.sendPacket(packet);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// THANKS Liam!!!
|
|
|
|
|
static async forceDfuMode(port) {
|
|
|
|
|
// open port
|
|
|
|
|
await port.open({
|
|
|
|
|
baudRate: DFU_TOUCH_BAUD,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// wait SERIAL_PORT_OPEN_WAIT_TIME before closing port
|
|
|
|
|
await sleep(SERIAL_PORT_OPEN_WAIT_TIME * 1000);
|
|
|
|
|
|
|
|
|
|
// close port
|
|
|
|
|
await port.close();
|
|
|
|
|
|
|
|
|
|
// wait TOUCH_RESET_WAIT_TIME for device to enter into DFU mode
|
|
|
|
|
await sleep(TOUCH_RESET_WAIT_TIME * 1000);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async sendStartDfu(mode, softdeviceSize = 0, bootloaderSize = 0, appSize = 0) {
|
|
|
|
|
const frame = new Uint8Array([
|
|
|
|
|
...int32ToBytes(DFU_START_PACKET),
|
|
|
|
|
...int32ToBytes(mode),
|
|
|
|
|
...int32ToBytes(softdeviceSize),
|
|
|
|
|
...int32ToBytes(bootloaderSize),
|
|
|
|
|
...int32ToBytes(appSize),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const packet = new HciPacket(frame);
|
|
|
|
|
await this.sendPacket(packet);
|
|
|
|
|
|
|
|
|
|
// Calculate and apply erase wait time.
|
|
|
|
|
const totalSize = softdeviceSize + bootloaderSize + appSize;
|
|
|
|
|
const eraseWaitTime = Math.max(0.5, ((totalSize / FLASH_PAGE_SIZE) + 1) * FLASH_PAGE_ERASE_TIME);
|
|
|
|
|
await sleep(eraseWaitTime * 1000);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async sendErasePage(pageAddress) {
|
|
|
|
|
const frame = new Uint8Array([
|
|
|
|
|
...int32ToBytes(DFU_ERASE_PAGE),
|
|
|
|
|
...int32ToBytes(pageAddress),
|
|
|
|
|
]);
|
|
|
|
|
const packet = new HciPacket(frame);
|
|
|
|
|
await this.sendPacket(packet);
|
|
|
|
|
await sleep(FLASH_PAGE_ERASE_TIME * 1000); // Wait for page erase
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async eraseFlash(appSize) {
|
|
|
|
|
console.log("Erasing flash...");
|
|
|
|
|
const numPages = Math.ceil(appSize / FLASH_PAGE_SIZE);
|
|
|
|
|
|
|
|
|
|
// Assuming application starts at address 0x00000000
|
|
|
|
|
let startAddress = 0x00000000;
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < numPages; i++) {
|
|
|
|
|
const pageAddress = startAddress + (i * FLASH_PAGE_SIZE);
|
|
|
|
|
console.log(`Erasing page ${i} at address 0x${pageAddress.toString(16)}`);
|
|
|
|
|
await this.sendErasePage(pageAddress);
|
2025-02-23 10:19:53 +01:00
|
|
|
}
|
2025-02-23 10:27:17 +01:00
|
|
|
console.log("Flash erase complete.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async sendFirmware(firmware, progressCallback) {
|
|
|
|
|
const frames = [];
|
|
|
|
|
let totalBytes = firmware.length;
|
|
|
|
|
|
|
|
|
|
// Chunk firmware into DFU packets
|
|
|
|
|
for (let i = 0; i < firmware.length; i += DFU_PACKET_MAX_SIZE) {
|
|
|
|
|
const chunk = firmware.subarray(i, i + DFU_PACKET_MAX_SIZE);
|
|
|
|
|
const frame = new Uint8Array([
|
|
|
|
|
...int32ToBytes(DFU_DATA_PACKET),
|
|
|
|
|
...chunk,
|
|
|
|
|
]);
|
|
|
|
|
const dataPacket = new HciPacket(frame);
|
|
|
|
|
frames.push(dataPacket);
|
2025-02-23 10:19:53 +01:00
|
|
|
}
|
|
|
|
|
|
2025-02-23 10:27:17 +01:00
|
|
|
let bytesSent = 0;
|
2026-03-26 13:14:06 +01:00
|
|
|
// Brief stabilization pause before starting data transfer (mirrors Python's implicit
|
|
|
|
|
// pause at count=0 — it sleeps FLASH_PAGE_WRITE_TIME after the very first packet).
|
|
|
|
|
await sleep(FLASH_PAGE_WRITE_TIME * 1000);
|
|
|
|
|
|
2025-02-23 10:27:17 +01:00
|
|
|
// Send firmware packets
|
|
|
|
|
for (const [index, pkt] of frames.entries()) {
|
|
|
|
|
await this.sendPacket(pkt);
|
|
|
|
|
bytesSent += pkt.data.length - 6; // Correctly calculate sent bytes, excluding SLIP overhead
|
2025-02-23 10:19:53 +01:00
|
|
|
|
2025-02-23 10:27:17 +01:00
|
|
|
if (progressCallback) {
|
|
|
|
|
const progress = Math.min(100, Math.round((bytesSent / totalBytes) * 100)); // Ensure progress doesn't exceed 100
|
|
|
|
|
progressCallback(progress);
|
|
|
|
|
}
|
2025-02-23 10:19:53 +01:00
|
|
|
|
2025-02-23 10:27:17 +01:00
|
|
|
// Wait after every 8 frames (one flash page)
|
|
|
|
|
if ((index + 1) % 8 === 0) {
|
|
|
|
|
await sleep(FLASH_PAGE_WRITE_TIME * 1000);
|
|
|
|
|
}
|
2025-02-23 10:19:53 +01:00
|
|
|
}
|
|
|
|
|
|
2025-02-23 10:27:17 +01:00
|
|
|
// Wait for the last page to be written
|
|
|
|
|
await sleep(FLASH_PAGE_WRITE_TIME * 1000);
|
2025-02-23 10:19:53 +01:00
|
|
|
|
2025-02-23 10:27:17 +01:00
|
|
|
// Send stop packet
|
|
|
|
|
const stopPacket = new HciPacket(int32ToBytes(DFU_STOP_DATA_PACKET));
|
|
|
|
|
await this.sendPacket(stopPacket);
|
|
|
|
|
}
|
2025-02-23 10:19:53 +01:00
|
|
|
|
2025-02-23 10:27:17 +01:00
|
|
|
async dfuUpdate(zipFile, progressCallback) {
|
|
|
|
|
if (this.transferInProgress) {
|
|
|
|
|
throw new Error("DFU update already in progress.");
|
2025-02-23 10:19:53 +01:00
|
|
|
}
|
2025-02-23 10:27:17 +01:00
|
|
|
this.transferInProgress = true;
|
|
|
|
|
this.lastAck = -1; // Reset last ACK
|
2026-03-26 13:14:06 +01:00
|
|
|
HciPacket.sequenceNumber = 0; // Reset HCI sequence number
|
2025-02-23 10:27:17 +01:00
|
|
|
const decoder = new TextDecoder();
|
|
|
|
|
try {
|
|
|
|
|
await this.port.open({ baudRate: 115200 }); // Open with correct baudrate
|
|
|
|
|
|
|
|
|
|
const reader = new zip.ZipReader(new zip.BlobReader(zipFile));
|
|
|
|
|
const entries = await reader.getEntries();
|
|
|
|
|
|
|
|
|
|
let manifest = null;
|
|
|
|
|
let firmwareFiles = {};
|
|
|
|
|
|
|
|
|
|
for (const entry of entries) {
|
|
|
|
|
const filename = decoder.decode(entry.rawFilename);
|
|
|
|
|
console.debug('Found zip filename: ', filename);
|
|
|
|
|
if (filename === 'manifest.json') {
|
|
|
|
|
const text = await entry.getData(new zip.TextWriter());
|
|
|
|
|
manifest = JSON.parse(text);
|
|
|
|
|
} else if (filename.endsWith('.bin') || filename.endsWith('.dat')) {
|
|
|
|
|
firmwareFiles[filename] = await entry.getData(new zip.Uint8ArrayWriter());
|
2025-02-23 10:19:53 +01:00
|
|
|
}
|
2025-02-23 10:27:17 +01:00
|
|
|
}
|
2025-02-23 10:19:53 +01:00
|
|
|
|
2025-02-23 10:27:17 +01:00
|
|
|
await reader.close();
|
2025-02-23 10:19:53 +01:00
|
|
|
|
2025-02-23 10:27:17 +01:00
|
|
|
if (!manifest) {
|
|
|
|
|
throw new Error("manifest.json not found in the ZIP file.");
|
|
|
|
|
}
|
|
|
|
|
if (!firmwareFiles[manifest.manifest.application.bin_file] ||
|
|
|
|
|
!firmwareFiles[manifest.manifest.application.dat_file])
|
|
|
|
|
{
|
|
|
|
|
throw new Error("Application .bin or .dat file not found.");
|
|
|
|
|
}
|
2025-02-23 10:19:53 +01:00
|
|
|
|
2025-02-23 10:27:17 +01:00
|
|
|
const appBin = firmwareFiles[manifest.manifest.application.bin_file];
|
|
|
|
|
const initPacket = firmwareFiles[manifest.manifest.application.dat_file];
|
|
|
|
|
const appSize = appBin.length;
|
2025-02-23 10:19:53 +01:00
|
|
|
|
2025-02-23 10:27:17 +01:00
|
|
|
// Erase flash if requested
|
|
|
|
|
if (this.eraseBeforeUpdate) {
|
|
|
|
|
await this.eraseFlash(appSize);
|
|
|
|
|
}
|
2025-02-23 10:19:53 +01:00
|
|
|
|
2025-02-23 10:27:17 +01:00
|
|
|
// Start DFU
|
|
|
|
|
await this.sendStartDfu(DFU_UPDATE_MODE_APP, 0, 0, appSize);
|
2025-02-23 10:19:53 +01:00
|
|
|
|
2025-02-23 10:27:17 +01:00
|
|
|
// Send Init Packet
|
|
|
|
|
await this.sendInitPacket(initPacket);
|
2025-02-23 10:19:53 +01:00
|
|
|
|
2025-02-23 10:27:17 +01:00
|
|
|
// Send Firmware
|
|
|
|
|
await this.sendFirmware(appBin, progressCallback);
|
2025-02-23 10:19:53 +01:00
|
|
|
|
2025-02-23 10:27:17 +01:00
|
|
|
console.log("DFU update complete.");
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("DFU Update failed:", error);
|
|
|
|
|
throw error; // Re-throw the error for handling by the caller
|
|
|
|
|
} finally {
|
|
|
|
|
this.transferInProgress = false;
|
|
|
|
|
if (this.port && this.port.readable) {
|
|
|
|
|
try {
|
|
|
|
|
const reader = this.port.readable.getReader();
|
|
|
|
|
await reader.cancel();
|
|
|
|
|
reader.releaseLock();
|
2025-02-23 10:19:53 +01:00
|
|
|
|
|
|
|
|
} catch (error) {
|
2025-02-23 10:27:17 +01:00
|
|
|
// Ignore errors when trying to cancel the reader
|
|
|
|
|
console.debug(`Error: closing reader: ${error}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (this.port && this.port.writable) {
|
|
|
|
|
try {
|
|
|
|
|
const writer = this.port.writable.getWriter();
|
|
|
|
|
await writer.close();
|
|
|
|
|
writer.releaseLock();
|
|
|
|
|
} catch(error) {
|
|
|
|
|
// Ignore errors when trying to close the writer
|
|
|
|
|
console.debug(`Error: closing writer: ${error}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (this.port) {
|
|
|
|
|
try {
|
|
|
|
|
await this.port.close();
|
|
|
|
|
}
|
|
|
|
|
catch (error) {
|
|
|
|
|
// Ignore errors when trying to close the port
|
|
|
|
|
console.debug(`Error: closing port: ${error}`);
|
2025-02-23 10:19:53 +01:00
|
|
|
}
|
2025-02-23 10:27:17 +01:00
|
|
|
}
|
2025-02-23 10:19:53 +01:00
|
|
|
}
|
2025-02-23 10:27:17 +01:00
|
|
|
}
|
2025-02-23 10:19:53 +01:00
|
|
|
}
|