UNPKG

esp-controller

Version:

Typescript package for connecting and flashing images to your ESP device.

1,660 lines (1,637 loc) 57.2 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { AppPartitionSubType: () => AppPartitionSubType, BinFilePartition: () => BinFilePartition, DataPartitionSubType: () => DataPartitionSubType, ESPImage: () => ESPImage, NVSPartition: () => NVSPartition, PartitionTable: () => PartitionTable, PartitionType: () => PartitionType, SerialController: () => SerialController }); module.exports = __toCommonJS(index_exports); // src/utils/common.ts function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function toHex(bytes) { return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join(""); } function slipEncode(buffer) { const encoded = [192 /* END */]; for (const byte of buffer) { if (byte === 192 /* END */) { encoded.push(219 /* ESC */, 220 /* ESC_END */); } else if (byte === 219 /* ESC */) { encoded.push(219 /* ESC */, 221 /* ESC_ESC */); } else { encoded.push(byte); } } encoded.push(192 /* END */); return new Uint8Array(encoded); } function base64ToUint8Array(base64) { const binaryString = atob(base64); const len = binaryString.length; const bytes = new Uint8Array(len); for (let i = 0; i < len; i++) { bytes[i] = binaryString.charCodeAt(i); } return bytes; } // src/esp/stream-transformers.ts var LineBreakTransformer = class { buffer = ""; transform(chunk, controller) { this.buffer += chunk; const lines = this.buffer?.split("\r\n"); this.buffer = lines?.pop(); lines?.forEach((line) => controller.enqueue(line)); } }; var SlipStreamTransformer = class { // Buffer to accumulate bytes for the current frame. /** * Constructs a new SlipStreamTransformer. * @param mode Specifies whether the transformer should operate in "encoding" or "decoding" mode. */ constructor(mode) { this.mode = mode; if (this.mode === "encoding") { this.decoding = false; } } decoding = false; // Flag to indicate if the initial END byte for a packet has been received in decoding mode. escape = false; // Flag to indicate if the current byte is an escape character in decoding mode. frame = []; transform(chunk, controller) { if (this.mode === "decoding") { for (const byte of chunk) { if (this.decoding) { if (this.escape) { if (byte === 220 /* ESC_END */) { this.frame.push(192 /* END */); } else if (byte === 221 /* ESC_ESC */) { this.frame.push(219 /* ESC */); } else { this.frame.push(byte); } this.escape = false; } else if (byte === 219 /* ESC */) { this.escape = true; } else if (byte === 192 /* END */) { if (this.frame.length > 0) { controller.enqueue(new Uint8Array(this.frame)); } this.frame = []; } else { this.frame.push(byte); } } else if (byte === 192 /* END */) { this.decoding = true; this.frame = []; this.escape = false; } } } else { for (const byte of chunk) { if (byte === 192 /* END */) { this.frame.push(219 /* ESC */, 220 /* ESC_END */); } else if (byte === 219 /* ESC */) { this.frame.push(219 /* ESC */, 221 /* ESC_ESC */); } else { this.frame.push(byte); } } } } flush(controller) { if (this.mode === "encoding") { if (this.frame.length > 0) { const finalPacket = new Uint8Array([ 192 /* END */, ...this.frame, 192 /* END */ ]); controller.enqueue(finalPacket); this.frame = []; } } } }; function createLineBreakTransformer() { return new TransformStream(new LineBreakTransformer()); } var SlipStreamEncoder = class extends TransformStream { /** * Constructs a new SlipStreamEncoder. * This sets up the underlying SlipStreamTransformer in "encoding" mode. */ constructor() { super(new SlipStreamTransformer("encoding")); } }; var SlipStreamDecoder = class extends TransformStream { /** * Constructs a new SlipStreamDecoder. * This sets up the underlying SlipStreamTransformer in "decoding" mode. */ constructor() { super(new SlipStreamTransformer("decoding")); } }; // src/esp/command.ts var EspCommand = /* @__PURE__ */ ((EspCommand2) => { EspCommand2[EspCommand2["FLASH_BEGIN"] = 2] = "FLASH_BEGIN"; EspCommand2[EspCommand2["FLASH_DATA"] = 3] = "FLASH_DATA"; EspCommand2[EspCommand2["FLASH_END"] = 4] = "FLASH_END"; EspCommand2[EspCommand2["MEM_BEGIN"] = 5] = "MEM_BEGIN"; EspCommand2[EspCommand2["MEM_END"] = 6] = "MEM_END"; EspCommand2[EspCommand2["MEM_DATA"] = 7] = "MEM_DATA"; EspCommand2[EspCommand2["SYNC"] = 8] = "SYNC"; EspCommand2[EspCommand2["WRITE_REG"] = 9] = "WRITE_REG"; EspCommand2[EspCommand2["READ_REG"] = 10] = "READ_REG"; EspCommand2[EspCommand2["SPI_SET_PARAMS"] = 11] = "SPI_SET_PARAMS"; EspCommand2[EspCommand2["SPI_ATTACH"] = 13] = "SPI_ATTACH"; EspCommand2[EspCommand2["CHANGE_BAUDRATE"] = 15] = "CHANGE_BAUDRATE"; EspCommand2[EspCommand2["FLASH_DEFL_BEGIN"] = 16] = "FLASH_DEFL_BEGIN"; EspCommand2[EspCommand2["FLASH_DEFL_DATA"] = 17] = "FLASH_DEFL_DATA"; EspCommand2[EspCommand2["FLASH_DEFL_END"] = 18] = "FLASH_DEFL_END"; EspCommand2[EspCommand2["SPI_FLASH_MD5"] = 19] = "SPI_FLASH_MD5"; EspCommand2[EspCommand2["ERASE_FLASH"] = 208] = "ERASE_FLASH"; EspCommand2[EspCommand2["ERASE_REGION"] = 209] = "ERASE_REGION"; EspCommand2[EspCommand2["READ_FLASH"] = 210] = "READ_FLASH"; EspCommand2[EspCommand2["RUN_USER_CODE"] = 211] = "RUN_USER_CODE"; return EspCommand2; })(EspCommand || {}); var EspCommandPacket = class { packetHeader = new Uint8Array(8); packetData = new Uint8Array(0); set direction(direction) { new DataView(this.packetHeader.buffer, 0, 1).setUint8(0, direction); } get direction() { return new DataView(this.packetHeader.buffer, 0, 1).getUint8(0); } set command(command) { new DataView(this.packetHeader.buffer, 1, 1).setUint8(0, command); } get command() { return new DataView(this.packetHeader.buffer, 1, 1).getUint8(0); } set size(size) { new DataView(this.packetHeader.buffer, 2, 2).setUint16(0, size, true); } get size() { return new DataView(this.packetHeader.buffer, 2, 2).getUint16(0, true); } set checksum(checksum) { new DataView(this.packetHeader.buffer, 4, 4).setUint32(0, checksum, true); } get checksum() { return new DataView(this.packetHeader.buffer, 4, 4).getUint32(0, true); } set value(value) { new DataView(this.packetHeader.buffer, 4, 4).setUint32(0, value, true); } get value() { return new DataView(this.packetHeader.buffer, 4, 4).getUint32(0, true); } get status() { return new DataView(this.packetData.buffer, 0, 1).getUint8(0); } get error() { return new DataView(this.packetData.buffer, 1, 1).getUint8(0); } generateChecksum(data) { let cs = 239; for (const byte of data) { cs ^= byte; } return cs; } set data(packetData) { this.size = packetData.length; this.packetData = packetData; } get data() { return this.packetData; } parseResponse(responsePacket) { const responseDataView = new DataView(responsePacket.buffer); this.direction = responseDataView.getUint8(0); this.command = responseDataView.getUint8(1); this.size = responseDataView.getUint16(2, true); this.value = responseDataView.getUint32(4, true); this.packetData = responsePacket.slice(8); if (this.status === 1) { console.log(this.getErrorMessage(this.error)); } } getErrorMessage(error) { switch (error) { case 5: return "Status Error: Received message is invalid. (parameters or length field is invalid)"; case 6: return "Failed to act on received message"; case 7: return "Invalid CRC in message"; case 8: return "flash write error - after writing a block of data to flash, the ROM loader reads the value back and the 8-bit CRC is compared to the data read from flash. If they don't match, this error is returned."; case 9: return "flash read error - SPI read failed"; case 10: return "flash read length error - SPI read request length is too long"; case 11: return "Deflate error (compressed uploads only)"; default: return "No error status for response"; } } getPacketData() { const header = new Uint8Array(8); const view = new DataView(header.buffer); view.setUint8(0, this.direction); view.setUint8(1, this.command); view.setUint16(2, this.data.length, true); view.setUint32(4, this.checksum, true); return new Uint8Array([...header, ...this.data]); } getSlipStreamEncodedPacketData() { return slipEncode(this.getPacketData()); } }; // src/esp/command.sync.ts var EspCommandSync = class extends EspCommandPacket { constructor() { super(); this.direction = 0 /* REQUEST */; this.command = 8 /* SYNC */; this.data = new Uint8Array([ 7, 7, 18, 32, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85 ]); this.checksum = 0; } }; // src/esp/command.spi-attach.ts var EspCommandSpiAttach = class extends EspCommandPacket { spiAttachData = new ArrayBuffer(8); view1 = new DataView(this.spiAttachData, 0, 4); view2 = new DataView(this.spiAttachData, 4, 4); constructor() { super(); this.direction = 0 /* REQUEST */; this.command = 13 /* SPI_ATTACH */; this.view1.setUint32(0, 0, true); this.view2.setUint32(0, 0, true); this.data = new Uint8Array(this.spiAttachData); } }; // src/esp/command.spi-set-params.ts var EspCommandSpiSetParams = class extends EspCommandPacket { paramsData = new ArrayBuffer(24); id = new DataView(this.paramsData, 0, 4); totalSize = new DataView(this.paramsData, 4, 4); blockSize = new DataView(this.paramsData, 8, 4); sectorSize = new DataView(this.paramsData, 12, 4); pageSize = new DataView(this.paramsData, 16, 4); statusMask = new DataView(this.paramsData, 20, 4); constructor() { super(); this.direction = 0 /* REQUEST */; this.command = 11 /* SPI_SET_PARAMS */; this.id.setUint32(0, 0, true); this.totalSize.setUint32(0, 4 * 1024 * 1024, true); this.blockSize.setUint32(0, 65536, true); this.sectorSize.setUint32(0, 4096, true); this.pageSize.setUint32(0, 256, true); this.statusMask.setUint32(0, 4294967295, true); this.data = new Uint8Array(this.paramsData); } }; // src/esp/command.flash-begin.ts var EspCommandFlashBegin = class extends EspCommandPacket { flashBeginData = new ArrayBuffer(16); eraseSizeView = new DataView(this.flashBeginData, 0, 4); numDataPacketsView = new DataView(this.flashBeginData, 4, 4); dataSizeView = new DataView(this.flashBeginData, 8, 4); offsetView = new DataView(this.flashBeginData, 12, 4); constructor(image, offset, packetSize, numPackets) { super(); this.direction = 0 /* REQUEST */; this.command = 2 /* FLASH_BEGIN */; this.eraseSizeView.setUint32(0, image.length, true); this.numDataPacketsView.setUint32(0, numPackets, true); this.dataSizeView.setUint32(0, packetSize, true); this.offsetView.setUint32(0, offset, true); this.data = new Uint8Array(this.flashBeginData); } }; // src/esp/command.flash-data.ts var EspCommandFlashData = class extends EspCommandPacket { constructor(image, sequenceNumber, blockSize) { super(); this.direction = 0 /* REQUEST */; this.command = 3 /* FLASH_DATA */; const flashDownloadData = new Uint8Array(16 + blockSize); const blockSizeView = new DataView(flashDownloadData.buffer, 0, 4); const sequenceView = new DataView(flashDownloadData.buffer, 4, 4); const paddingView = new DataView(flashDownloadData.buffer, 8, 8); blockSizeView.setUint32(0, blockSize, true); sequenceView.setUint32(0, sequenceNumber, true); paddingView.setUint32(0, 0, true); paddingView.setUint32(4, 0, true); const block = image.slice( sequenceNumber * blockSize, sequenceNumber * blockSize + blockSize ); const blockData = new Uint8Array(blockSize); blockData.fill(255); blockData.set(block, 0); flashDownloadData.set(blockData, 16); this.data = flashDownloadData; this.checksum = this.generateChecksum(blockData); } }; // src/esp/command.read-reg.ts var EspCommandReadReg = class extends EspCommandPacket { readRegData = new ArrayBuffer(4); constructor(address) { super(); this.command = 10 /* READ_REG */; this.direction = 0 /* REQUEST */; new DataView(this.readRegData).setUint32(0, address, true); this.data = new Uint8Array(this.readRegData); } }; // src/esp/command.mem-begin.ts var EspCommandMemBegin = class extends EspCommandPacket { constructor(totalSize, numPackets, packetSize, offset) { super(); this.totalSize = totalSize; this.numPackets = numPackets; this.packetSize = packetSize; this.offset = offset; this.direction = 0 /* REQUEST */; this.command = 5 /* MEM_BEGIN */; this.checksum = 0; const dataPayload = new Uint8Array(16); const view = new DataView(dataPayload.buffer); view.setUint32(0, this.totalSize, true); view.setUint32(4, this.numPackets, true); view.setUint32(8, this.packetSize, true); view.setUint32(12, this.offset, true); this.data = dataPayload; } }; // src/esp/command.mem-data.ts var EspCommandMemData = class extends EspCommandPacket { constructor(binary, sequence, packetSize) { super(); this.binary = binary; this.sequence = sequence; this.packetSize = packetSize; const chunk = binary.slice( sequence * packetSize, (sequence + 1) * packetSize ); const header = new Uint8Array(16); const view = new DataView(header.buffer); view.setUint32(0, chunk.length, true); view.setUint32(4, sequence, true); view.setUint32(8, 0, true); view.setUint32(12, 0, true); this.direction = 0 /* REQUEST */; this.command = 7 /* MEM_DATA */; this.data = new Uint8Array([...header, ...chunk]); this.checksum = this.generateChecksum(chunk); } }; // src/esp/command.mem-end.ts var EspCommandMemEnd = class extends EspCommandPacket { constructor(executeFlag, entryPoint) { super(); this.executeFlag = executeFlag; this.entryPoint = entryPoint; this.direction = 0 /* REQUEST */; this.command = 6 /* MEM_END */; this.checksum = 0; const dataPayload = new Uint8Array(8); const view = new DataView(dataPayload.buffer); view.setUint32(0, this.executeFlag, true); view.setUint32(4, this.entryPoint, true); this.data = dataPayload; } }; // src/esp/serial-controller.ts var DEFAULT_ESP32_SERIAL_OPTIONS = { baudRate: 115200, dataBits: 8, stopBits: 1, bufferSize: 255, parity: "none", flowControl: "none" }; var ChipFamily = /* @__PURE__ */ ((ChipFamily2) => { ChipFamily2[ChipFamily2["ESP32"] = 15736195] = "ESP32"; ChipFamily2[ChipFamily2["ESP32S2"] = 1990] = "ESP32S2"; ChipFamily2[ChipFamily2["ESP32S3"] = 9] = "ESP32S3"; ChipFamily2[ChipFamily2["ESP32C3"] = 1763790959] = "ESP32C3"; ChipFamily2[ChipFamily2["ESP32C6"] = 752910447] = "ESP32C6"; ChipFamily2[ChipFamily2["ESP32H2"] = 3389177967] = "ESP32H2"; ChipFamily2[ChipFamily2["ESP8266"] = 4293968129] = "ESP8266"; ChipFamily2[ChipFamily2["UNKNOWN"] = 4294967295] = "UNKNOWN"; return ChipFamily2; })(ChipFamily || {}); var SerialController = class extends EventTarget { connection; constructor() { super(); this.connection = this.createSerialConnection(); } createSerialConnection() { return { port: void 0, connected: false, synced: false, chip: null, readable: null, writable: null, abortStreamController: void 0, commandResponseStream: void 0 }; } async requestPort() { this.connection.port = await navigator.serial.requestPort(); this.connection.synced = false; this.connection.chip = null; } createLogStreamReader() { if (!this.connection.connected || !this.connection.readable || !this.connection.abortStreamController) return async function* logStream() { }; const streamPipeOptions = { signal: this.connection.abortStreamController.signal, preventCancel: false, preventClose: false, preventAbort: false }; const [newReadable, logReadable] = this.connection.readable.tee(); this.connection.readable = newReadable; const reader = logReadable.pipeThrough(new TextDecoderStream(), streamPipeOptions).pipeThrough(createLineBreakTransformer(), streamPipeOptions).getReader(); const connection = this.connection; return async function* logStream() { try { while (connection.connected) { const result = await reader?.read(); if (result?.done) return; yield result?.value; } } finally { reader?.releaseLock(); } }; } async openPort(options = DEFAULT_ESP32_SERIAL_OPTIONS) { if (!this.connection.port) return; await this.connection?.port.open(options); if (!this.connection.port?.readable) return; this.connection.abortStreamController = new AbortController(); const [commandTee, logTee] = this.connection.port.readable.tee(); this.connection.connected = true; this.connection.readable = logTee; this.connection.writable = this.connection.port.writable; this.connection.commandResponseStream = commandTee.pipeThrough( new SlipStreamDecoder(), { signal: this.connection.abortStreamController.signal } ); } async disconnect() { if (!this.connection.connected || !this.connection.port) { return; } this.connection.abortStreamController?.abort(); try { await this.connection.port.close(); } catch (error) { console.error("Failed to close the serial port:", error); } const port = this.connection.port; this.connection = this.createSerialConnection(); this.connection.port = port; } async sendResetPulse() { if (!this.connection.port) return; this.connection.port.setSignals({ dataTerminalReady: false, requestToSend: true }); await sleep(100); this.connection.port.setSignals({ dataTerminalReady: true, requestToSend: false }); await sleep(100); } async writeToConnection(data) { if (this.connection.writable) { const writer = this.connection.writable.getWriter(); await writer.write(data); writer.releaseLock(); } } async sync() { await this.sendResetPulse(); const maxAttempts = 10; const timeoutPerAttempt = 500; const syncCommand = new EspCommandSync(); for (let i = 0; i < maxAttempts; i++) { this.dispatchEvent( new CustomEvent("sync-progress", { detail: { progress: i / maxAttempts * 100 } }) ); console.log(`Sync attempt ${i + 1} of ${maxAttempts}`); await this.writeToConnection( syncCommand.getSlipStreamEncodedPacketData() ); let responseReader; try { if (!this.connection.commandResponseStream) { throw new Error(`No command response stream available.`); } responseReader = this.connection.commandResponseStream.getReader(); const timeoutPromise = sleep(timeoutPerAttempt).then(() => { throw new Error(`Timeout after ${timeoutPerAttempt}ms`); }); while (true) { const { value, done } = await Promise.race([ responseReader.read(), timeoutPromise ]); if (done) { throw new Error("Stream closed unexpectedly while syncing."); } if (value) { try { const responsePacket = new EspCommandPacket(); responsePacket.parseResponse(value); if (responsePacket.command === 8 /* SYNC */) { console.log("SYNCED successfully.", responsePacket); this.connection.synced = true; this.dispatchEvent( new CustomEvent("sync-progress", { detail: { progress: 100 } }) ); return true; } } catch { } } } } catch (e) { console.log(`Sync attempt ${i + 1} failed.`, e); } finally { if (responseReader) { responseReader.releaseLock(); } } await sleep(100); } console.log("Failed to sync with the device."); this.connection.synced = false; return false; } async detectChip() { if (!this.connection.synced) { throw new Error("Device must be synced to detect chip type."); } const CHIP_DETECT_MAGIC_REG_ADDR = 1073745920; const readRegCmd = new EspCommandReadReg(CHIP_DETECT_MAGIC_REG_ADDR); await this.writeToConnection(readRegCmd.getSlipStreamEncodedPacketData()); const response = await this.readResponse(10 /* READ_REG */); const magicValue = response.value; const numericChipValues = Object.values(ChipFamily).filter( (v) => typeof v === "number" ); const chip = numericChipValues.find((c) => c === magicValue) || 4294967295 /* UNKNOWN */; this.connection.chip = chip; console.log( `Detected chip: ${ChipFamily[chip]} (Magic value: ${toHex(new Uint8Array(new Uint32Array([magicValue]).buffer))})` ); if (chip === 4294967295 /* UNKNOWN */) { throw new Error("Could not detect a supported chip family."); } return chip; } async loadToRam(binary, offset, execute = false, entryPoint = 0) { console.log( `Loading binary to RAM at offset ${toHex(new Uint8Array(new Uint32Array([offset]).buffer))}` ); const packetSize = 1460; const numPackets = Math.ceil(binary.length / packetSize); const memBeginCmd = new EspCommandMemBegin( binary.length, numPackets, packetSize, offset ); await this.writeToConnection(memBeginCmd.getSlipStreamEncodedPacketData()); await this.readResponse(5 /* MEM_BEGIN */); for (let i = 0; i < numPackets; i++) { const memDataCmd = new EspCommandMemData(binary, i, packetSize); await this.writeToConnection(memDataCmd.getSlipStreamEncodedPacketData()); await this.readResponse(7 /* MEM_DATA */, 1e3); } if (execute) { console.log(`Executing from entry point ${entryPoint}`); const memEndCmd = new EspCommandMemEnd(1, entryPoint); await this.writeToConnection(memEndCmd.getSlipStreamEncodedPacketData()); await this.readResponse(6 /* MEM_END */); } } /** * Fetches the stub for the given chip family from the local file system. * @param chip The chip family to fetch the stub for. * @returns A promise that resolves to the Stub object. */ async getStubForChip(chip) { const chipNameMap = { [15736195 /* ESP32 */]: "32", [1990 /* ESP32S2 */]: "32s2", [9 /* ESP32S3 */]: "32s3", [1763790959 /* ESP32C3 */]: "32c3", [752910447 /* ESP32C6 */]: "32c6", [3389177967 /* ESP32H2 */]: "32h2", [4293968129 /* ESP8266 */]: "8266" }; const chipName = chipNameMap[chip]; if (!chipName) { throw new Error(`No stub file mapping for chip: ${ChipFamily[chip]}`); } const stubUrl = `./stub-flasher/stub_flasher_${chipName}.json`; console.log(`Fetching stub from ${stubUrl}`); try { const response = await fetch(stubUrl); if (!response.ok) { throw new Error(`Failed to fetch stub file: ${response.statusText}`); } return await response.json(); } catch (e) { console.error(`Error loading stub for ${ChipFamily[chip]}:`, e); throw e; } } async uploadStub(stub) { const text = base64ToUint8Array(stub.text); const data = base64ToUint8Array(stub.data); await this.loadToRam(text, stub.text_start, false); await this.loadToRam(data, stub.data_start, false); console.log(`Starting stub at entry point 0x${stub.entry.toString(16)}...`); const memEndCmd = new EspCommandMemEnd(1, stub.entry); await this.writeToConnection(memEndCmd.getSlipStreamEncodedPacketData()); await this.readResponse(6 /* MEM_END */); console.log("Stub started successfully."); await this.awaitOhaiResponse(); } async awaitOhaiResponse(timeout = 2e3) { let responseReader; const ohaiPacket = new Uint8Array([79, 72, 65, 73]); try { if (!this.connection.commandResponseStream) { throw new Error("No command response stream available."); } responseReader = this.connection.commandResponseStream.getReader(); const timeoutPromise = sleep(timeout).then(() => { throw new Error( `Timeout: Did not receive "OHAI" from stub within ${timeout}ms.` ); }); console.log("Waiting for 'OHAI' packet from stub..."); while (true) { const { value, done } = await Promise.race([ responseReader.read(), timeoutPromise ]); if (done) { throw new Error( "Stream closed unexpectedly while waiting for 'OHAI'." ); } if (value && value.length === ohaiPacket.length) { if (value.every((byte, index) => byte === ohaiPacket[index])) { console.log("'OHAI' packet received, stub confirmed."); return; } } } } finally { if (responseReader) { responseReader.releaseLock(); } } } async readResponse(expectedCommand, timeout = 2e3) { let responseReader; try { if (!this.connection.commandResponseStream) { throw new Error(`No command response stream available.`); } responseReader = this.connection.commandResponseStream.getReader(); const timeoutPromise = sleep(timeout).then(() => { throw new Error( `Timeout: No response received for command ${EspCommand[expectedCommand]} within ${timeout}ms.` ); }); while (true) { const { value, done } = await Promise.race([ responseReader.read(), timeoutPromise ]); if (done) { throw new Error( "Stream closed unexpectedly while awaiting response." ); } if (value) { try { const responsePacket = new EspCommandPacket(); responsePacket.parseResponse(value); if (responsePacket.direction === 1 /* RESPONSE */ && responsePacket.command === expectedCommand) { if (responsePacket.error > 0) { throw new Error( `Device returned error for ${EspCommand[expectedCommand]}: ${responsePacket.getErrorMessage(responsePacket.error)}` ); } return responsePacket; } } catch { } } } } finally { if (responseReader) { responseReader.releaseLock(); } } } async flashPartition(partition) { console.log( `Flashing partition: ${partition.filename}, offset: ${toHex( new Uint8Array(new Uint32Array([partition.offset]).buffer) )}` ); const packetSize = 4096; const numPackets = Math.ceil(partition.binary.length / packetSize); const flashBeginCmd = new EspCommandFlashBegin( partition.binary, partition.offset, packetSize, numPackets ); await this.writeToConnection( flashBeginCmd.getSlipStreamEncodedPacketData() ); await this.readResponse(2 /* FLASH_BEGIN */); console.log("FLASH_BEGIN successful."); for (let i = 0; i < numPackets; i++) { const flashDataCmd = new EspCommandFlashData( partition.binary, i, packetSize ); await this.writeToConnection( flashDataCmd.getSlipStreamEncodedPacketData() ); this.dispatchEvent( new CustomEvent("flash-progress", { detail: { progress: (i + 1) / numPackets * 100, partition } }) ); console.log( `[${partition.filename}] Writing block ${i + 1}/${numPackets}` ); await this.readResponse(3 /* FLASH_DATA */, 5e3); } console.log(`Flash data for ${partition.filename} sent successfully.`); } /** * Main method to flash a complete image. * @param image The ESPImage to flash. */ async flashImage(image) { if (!this.connection.connected) { throw new Error("Device is not connected."); } if (!this.connection.synced) { const synced = await this.sync(); if (!synced) { throw new Error( "ESP32 Needs to Sync before flashing. Hold the `boot` button on the device during sync attempts." ); } } if (!this.connection.chip) { await this.detectChip(); } const stub = await this.getStubForChip(this.connection.chip); await this.uploadStub(stub); const attachCmd = new EspCommandSpiAttach(); await this.writeToConnection(attachCmd.getSlipStreamEncodedPacketData()); await this.readResponse(13 /* SPI_ATTACH */); console.log("SPI_ATTACH successful."); const setParamsCmd = new EspCommandSpiSetParams(); await this.writeToConnection(setParamsCmd.getSlipStreamEncodedPacketData()); await this.readResponse(11 /* SPI_SET_PARAMS */); console.log("SPI_SET_PARAMS successful."); const totalSize = image.partitions.reduce( (acc, part) => acc + part.binary.length, 0 ); let flashedSize = 0; for (const partition of image.partitions) { const originalDispatchEvent = this.dispatchEvent; this.dispatchEvent = (event) => { if (event.type === "flash-progress" && "detail" in event) { const partitionFlashed = partition.binary.length * event.detail.progress / 100; originalDispatchEvent.call( this, new CustomEvent("flash-image-progress", { detail: { progress: (flashedSize + partitionFlashed) / totalSize * 100, partition } }) ); } return originalDispatchEvent.call(this, event); }; await this.flashPartition(partition); flashedSize += partition.binary.length; this.dispatchEvent = originalDispatchEvent; } this.dispatchEvent( new CustomEvent("flash-image-progress", { detail: { progress: 100 } }) ); console.log("Flashing complete. Resetting device..."); await this.sendResetPulse(); console.log("Device has been reset."); } }; // src/image/bin-file-partition.ts var BinFilePartition = class { constructor(offset, filename) { this.offset = offset; this.filename = filename; } binary = new Uint8Array(0); async load() { try { const response = await fetch(this.filename); if (!response.ok) { console.error( `Failed to fetch ${this.filename}: ${response.statusText}` ); return false; } this.binary = new Uint8Array(await response.arrayBuffer()); return true; } catch (e) { console.error(`Error loading file ${this.filename}:`, e); return false; } } }; // src/image/image.ts var ESPImage = class { partitions = []; addBootloader(fileName) { this.partitions.push(new BinFilePartition(4096, fileName)); } addPartitionTable(fileName) { this.partitions.push(new BinFilePartition(32768, fileName)); } addApp(fileName) { this.partitions.push(new BinFilePartition(65536, fileName)); } addPartition(partition) { this.partitions.push(partition); } }; // src/utils/crc32.ts var crc32Table = [ 0, 1996959894, 3993919788, 2567524794, 124634137, 1886057615, 3915621685, 2657392035, 249268274, 2044508324, 3772115230, 2547177864, 162941995, 2125561021, 3887607047, 2428444049, 498536548, 1789927666, 4089016648, 2227061214, 450548861, 1843258603, 4107580753, 2211677639, 325883990, 1684777152, 4251122042, 2321926636, 335633487, 1661365465, 4195302755, 2366115317, 997073096, 1281953886, 3579855332, 2724688242, 1006888145, 1258607687, 3524101629, 2768942443, 901097722, 1119000684, 3686517206, 2898065728, 853044451, 1172266101, 3705015759, 2882616665, 651767980, 1373503546, 3369554304, 3218104598, 565507253, 1454621731, 3485111705, 3099436303, 671266974, 1594198024, 3322730930, 2970347812, 795835527, 1483230225, 3244367275, 3060149565, 1994146192, 31158534, 2563907772, 4023717930, 1907459465, 112637215, 2680153253, 3904427059, 2013776290, 251722036, 2517215374, 3775830040, 2137656763, 141376813, 2439277719, 3865271297, 1802195444, 476864866, 2238001368, 4066508878, 1812370925, 453092731, 2181625025, 4111451223, 1706088902, 314042704, 2344532202, 4240017532, 1658658271, 366619977, 2362670323, 4224994405, 1303535960, 984961486, 2747007092, 3569037538, 1256170817, 1037604311, 2765210733, 3554079995, 1131014506, 879679996, 2909243462, 3663771856, 1141124467, 855842277, 2852801631, 3708648649, 1342533948, 654459306, 3188396048, 3373015174, 1466479909, 544179635, 3110523913, 3462522015, 1591671054, 702138776, 2966460450, 3352799412, 1504918807, 783551873, 3082640443, 3233442989, 3988292384, 2596254646, 62317068, 1957810842, 3939845945, 2647816111, 81470997, 1943803523, 3814918930, 2489596804, 225274430, 2053790376, 3826175755, 2466906013, 167816743, 2097651377, 4027552580, 2265490386, 503444072, 1762050814, 4150417245, 2154129355, 426522225, 1852507879, 4275313526, 2312317920, 282753626, 1742555852, 4189708143, 2394877945, 397917763, 1622183637, 3604390888, 2714866558, 953729732, 1340076626, 3518719985, 2797360999, 1068828381, 1219638859, 3624741850, 2936675148, 906185462, 1090812512, 3747672003, 2825379669, 829329135, 1181335161, 3412177804, 3160834842, 628085408, 1382605366, 3423369109, 3138078467, 570562233, 1426400815, 3317316542, 2998733608, 733239954, 1555261956, 3268935591, 3050360625, 752459403, 1541320221, 2607071920, 3965973030, 1969922972, 40735498, 2617837225, 3943577151, 1913087877, 83908371, 2512341634, 3803740692, 2075208622, 213261112, 2463272603, 3855990285, 2094854071, 198958881, 2262029012, 4057260610, 1759359992, 534414190, 2176718541, 4139329115, 1873836001, 414664567, 2282248934, 4279200368, 1711684554, 285281116, 2405801727, 4167216745, 1634467795, 376229701, 2685067896, 3608007406, 1308918612, 956543938, 2808555105, 3495958263, 1231636301, 1047427035, 2932959818, 3654703836, 1088359270, 936918e3, 2847714899, 3736837829, 1202900863, 817233897, 3183342108, 3401237130, 1404277552, 615818150, 3134207493, 3453421203, 1423857449, 601450431, 3009837614, 3294710456, 1567103746, 711928724, 3020668471, 3272380065, 1510334235, 755167117 ]; function crc32(buf, seed = 4294967295) { let C = seed ^ -1; const L = buf.length - 7; let i = 0; for (i = 0; i < L; ) { C = C >>> 8 ^ crc32Table[(C ^ buf[i++]) & 255]; C = C >>> 8 ^ crc32Table[(C ^ buf[i++]) & 255]; C = C >>> 8 ^ crc32Table[(C ^ buf[i++]) & 255]; C = C >>> 8 ^ crc32Table[(C ^ buf[i++]) & 255]; C = C >>> 8 ^ crc32Table[(C ^ buf[i++]) & 255]; C = C >>> 8 ^ crc32Table[(C ^ buf[i++]) & 255]; C = C >>> 8 ^ crc32Table[(C ^ buf[i++]) & 255]; C = C >>> 8 ^ crc32Table[(C ^ buf[i++]) & 255]; } while (i < L + 7) C = C >>> 8 ^ crc32Table[(C ^ buf[i++]) & 255]; C = C ^ -1; const buffer = new ArrayBuffer(4); const view = new DataView(buffer); view.setUint32(0, C, true); return new Uint8Array(buffer); } // src/nvs/nvs-settings.ts var NVSSettings = class { static BLOCK_SIZE = 32; static PAGE_SIZE = 4096; static PAGE_MAX_ENTRIES = 126; static PAGE_ACTIVE = 4294967294; static PAGE_FULL = 4294967292; static NVS_VERSION = 254; // version 2 static DEFAULT_NAMESPACE = "storage"; }; // src/nvs/nvs-entry.ts var NVS_BLOCK_SIZE = NVSSettings.BLOCK_SIZE; var NvsEntry = class { namespaceIndex; type; key; data; chunkIndex; headerNamespace; headerType; headerSpan; headerChunkIndex; headerCRC32; headerKey; headerData; headerDataSize; headerDataCRC32; headerBuffer; dataBuffer; entriesNeeded = 0; constructor(entry) { this.namespaceIndex = entry.namespaceIndex; this.type = entry.type; this.data = entry.data; this.chunkIndex = 255; if (entry.key.length > 15) { throw Error( `NVS max key length is 15, received '${entry.key}' of length ${entry.key.length}` ); } this.key = entry.key + "\0"; this.headerBuffer = new Uint8Array(NVS_BLOCK_SIZE); this.headerNamespace = new Uint8Array(this.headerBuffer.buffer, 0, 1); this.headerType = new Uint8Array(this.headerBuffer.buffer, 1, 1); this.headerSpan = new Uint8Array(this.headerBuffer.buffer, 2, 1); this.headerChunkIndex = new Uint8Array(this.headerBuffer.buffer, 3, 1); this.headerCRC32 = new Uint8Array(this.headerBuffer.buffer, 4, 4); this.headerKey = new Uint8Array(this.headerBuffer.buffer, 8, 16); this.headerData = new Uint8Array(this.headerBuffer.buffer, 24, 8); this.headerDataSize = new Uint8Array(this.headerBuffer.buffer, 24, 4); this.headerDataCRC32 = new Uint8Array(this.headerBuffer.buffer, 28, 4); this.dataBuffer = new Uint8Array(0); this.setEntryData(); this.setEntryHeader(); this.setEntryHeaderCRC(); } setEntryHeader() { const encoder = new TextEncoder(); this.headerNamespace.set([this.namespaceIndex]); this.headerType.set([this.type]); this.headerSpan.set([this.entriesNeeded]); this.headerChunkIndex.set([this.chunkIndex]); this.headerKey.set(encoder.encode(this.key)); } setEntryData() { if (this.type === 33 /* STR */) { this.setStringEntry(); } else if (typeof this.data === "number") { this.setPrimitiveEntry(); } else { throw new Error("Unsupported data type for NVS entry."); } } // In src/nvs/nvs-entry.ts setStringEntry() { if (typeof this.data === "string") { this.headerData.fill(255); const valueWithTerminator = this.data + "\0"; const encoder = new TextEncoder(); const data = encoder.encode(valueWithTerminator); if (data.length > 4e3) { throw new Error("String values are limited to 4000 bytes."); } this.entriesNeeded = 1 + Math.ceil(data.length / NVSSettings.BLOCK_SIZE); this.dataBuffer = new Uint8Array( (this.entriesNeeded - 1) * NVSSettings.BLOCK_SIZE ).fill(255); this.dataBuffer.set(data); const dataSizeBuffer = new ArrayBuffer(2); const dataSizeView = new DataView(dataSizeBuffer); dataSizeView.setUint16(0, data.length, true); this.headerData.set(new Uint8Array(dataSizeBuffer), 0); this.headerDataCRC32.set(crc32(data)); } } setPrimitiveEntry() { if (typeof this.data === "number") { this.entriesNeeded = 1; this.headerData.fill(255); const dataView = new DataView( this.headerData.buffer, this.headerData.byteOffset, 8 ); switch (this.type) { case 1 /* U8 */: dataView.setUint8(0, this.data); break; case 17 /* I8 */: dataView.setInt8(0, this.data); break; case 2 /* U16 */: dataView.setUint16(0, this.data, true); break; case 18 /* I16 */: dataView.setInt16(0, this.data, true); break; case 4 /* U32 */: dataView.setUint32(0, this.data, true); break; case 20 /* I32 */: dataView.setInt32(0, this.data, true); break; case 8 /* U64 */: dataView.setBigUint64(0, BigInt(this.data), true); break; case 24 /* I64 */: dataView.setBigInt64(0, BigInt(this.data), true); break; default: throw new Error(`Unsupported primitive type: ${this.type}`); } } } setEntryHeaderCRC() { const crcData = new Uint8Array(28); crcData.set(this.headerBuffer.slice(0, 4), 0); crcData.set(this.headerBuffer.slice(8, 32), 4); this.headerCRC32.set(crc32(crcData)); } }; // src/nvs/state-bitmap.ts var EntryStateBitmap = { /** * Updates the state for a given entry in the bitmap. * @param currentBitmap The current state bitmap as a BigInt. * @param entryIndex The index of the entry to update (0-125). * @param newState The new state for the entry. * @returns The updated bitmap as a BigInt. */ setState(currentBitmap, entryIndex, newState) { if (entryIndex < 0 || entryIndex >= 126) { throw new Error("Entry index is out of bounds."); } const bitPosition = BigInt(entryIndex * 2); const clearMask = ~(0b11n << bitPosition); const clearedBitmap = currentBitmap & clearMask; const newValue = BigInt(newState) << bitPosition; return clearedBitmap | newValue; } }; // src/nvs/nvs-page.ts var NVSPage = class { constructor(pageNumber, version) { this.pageNumber = pageNumber; this.version = version; this.pageBuffer = new Uint8Array(NVSSettings.PAGE_SIZE).fill(255); this.pageHeader = new Uint8Array(this.pageBuffer.buffer, 0, 32); this.headerPageState = new Uint8Array(this.pageHeader.buffer, 0, 4); this.headerPageNumber = new Uint8Array(this.pageHeader.buffer, 4, 4); this.headerVersion = new Uint8Array(this.pageHeader.buffer, 8, 1); this.headerCRC32 = new Uint8Array(this.pageHeader.buffer, 28, 4); this.setPageHeader(); } entryNumber = 0; pageBuffer; pageHeader; stateBitmap = BigInt( "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" ); entries = []; itemHashMap = /* @__PURE__ */ new Map(); headerPageState; headerPageNumber; headerVersion; headerCRC32; isStateLocked = false; setPageHeader() { this.setPageState("ACTIVE"); const pageNumView = new DataView( this.headerPageNumber.buffer, this.headerPageNumber.byteOffset, 4 ); pageNumView.setUint32(0, this.pageNumber, true); this.headerVersion.set([this.version]); this.updateHeaderCrc(); } updateHeaderCrc() { const crcData = this.pageHeader.slice(4, 28); this.headerCRC32.set(crc32(crcData)); } _calculateItemHash(namespaceIndex, key, chunkIndex) { const hashData = `${namespaceIndex}:${key}:${chunkIndex}`; const fullCrc = crc32(new TextEncoder().encode(hashData)); return new DataView(fullCrc.buffer).getUint32(0, true) & 16777215; } getNVSEncoding(value) { if (typeof value === "string") { return 33 /* STR */; } const isNegative = value < 0; const absValue = Math.abs(value); if (isNegative) { if (absValue <= 128) return 17 /* I8 */; if (absValue <= 32768) return 18 /* I16 */; if (absValue <= 2147483648) return 20 /* I32 */; return 24 /* I64 */; } else { if (absValue <= 255) return 1 /* U8 */; if (absValue <= 65535) return 2 /* U16 */; if (absValue <= 4294967295) return 4 /* U32 */; return 8 /* U64 */; } } writeEntry(key, data, namespaceIndex) { if (this.isStateLocked) { throw new Error("Page is full and locked. Cannot write new entries."); } const entryKv = { namespaceIndex, key, data, type: this.getNVSEncoding(data) }; const entry = new NvsEntry(entryKv); if (entry.entriesNeeded + this.entryNumber > NVSSettings.PAGE_MAX_ENTRIES) { this.setPageState("FULL"); throw new Error("Entry doesn't fit on the page"); } const hash = this._calculateItemHash(namespaceIndex, key, entry.chunkIndex); this.itemHashMap.set(hash, this.entryNumber); this.entries.push(entry); for (let i = 0; i < entry.entriesNeeded; i++) { this.stateBitmap = EntryStateBitmap.setState( this.stateBitmap, this.entryNumber + i, 2 /* Written */ ); } this.entryNumber += entry.entriesNeeded; return entry; } findEntry(key, namespaceIndex, chunkIndex = 255) { const hash = this._calculateItemHash(namespaceIndex, key, chunkIndex); const potentialIndex = this.itemHashMap.get(hash); if (potentialIndex !== void 0) { const entry = this.entries[potentialIndex]; if (entry && entry.key === key && entry.namespaceIndex === namespaceIndex && entry.chunkIndex === chunkIndex) { return entry; } } return this.entries.find( (e) => e.key === key && e.namespaceIndex === namespaceIndex && e.chunkIndex === chunkIndex ); } setPageState(state) { if (state === "FULL") { this.headerPageState.set( new Uint8Array(new Uint32Array([NVSSettings.PAGE_FULL]).buffer) ); this.isStateLocked = true; } else if (state === "ACTIVE") { this.headerPageState.set( new Uint8Array(new Uint32Array([NVSSettings.PAGE_ACTIVE]).buffer) ); } else { throw Error("Invalid page state requested"); } this.updateHeaderCrc(); } getData() { const sbm = new Uint8Array(NVSSettings.BLOCK_SIZE).fill(255); new DataView(sbm.buffer).setBigUint64(0, this.stateBitmap, true); this.pageBuffer.set(this.pageHeader, 0); this.pageBuffer.set(sbm, NVSSettings.BLOCK_SIZE); let currentEntrySlot = 0; for (const entry of this.entries) { const headerOffset = (2 + currentEntrySlot) * NVSSettings.BLOCK_SIZE; this.pageBuffer.set(entry.headerBuffer, headerOffset); if (entry.dataBuffer.length > 0) { const dataOffset = (2 + currentEntrySlot + 1) * NVSSettings.BLOCK_SIZE; this.pageBuffer.set(entry.dataBuffer, dataOffset); } currentEntrySlot += entry.entriesNeeded; } return this.pageBuffer; } }; // src/nvs/nvs-partition.ts var NVSPartition = class { constructor(offset, filename, size = 12288) { this.offset = offset; this.filename = filename; this.size = size; this.namespaces.push("RESERVED_NS_0"); this.newPage(); } namespaces = []; pages = []; newPage() { const lastPage = this.getLastPage(); if (lastPage) { lastPage.setPageState("FULL"); } const index = this.pages.length; const nvsPage = new NVSPage(index, NVSSettings.NVS_VERSION); this.pages.push(nvsPage); return nvsPage; } getLastPage() { if (this.pages.length === 0) { return null; } return this.pages[this.pages.length - 1]; } getNameSpaceIndex(namespace) { const existingIndex = this.namespaces.indexOf(namespace); if (existingIndex !== -1) { return existingIndex; } if (this.namespaces.length >= 254) { throw new Error("Maximum number of namespaces (254) reached."); } const newIndex = this.namespaces.length; this.namespaces.push(namespace); try { this.write(namespace, newIndex, 0); } catch (e) { console.log("Page is full, creating new", e); this.newPage(); this.write(namespace, newIndex, 0); } return newIndex; } get binary() { const buffer = new Uint8Array(this.size).fill(255); let offset = 0; for (const page of this.pages) { const pageBuffer = page.getData(); buffer.set(pageBuffer, offset); offset += pageBuffer.length; } return buffer; } writeEntry(namespace, key, data) { const namespaceIndex = this.getNameSpaceIndex(namespace); this.write(key, data, namespaceIndex); } // Private write helper to avoid recursive loop in namespace creation write(key, data, namespaceIndex) { try { const page = this.getLastPage();