import { ESPError } from "./types/error.js"; import { deflate, Inflate } from "pako"; import { Transport } from "./webserial.js"; import { ClassicReset, CustomReset, HardReset, UsbJtagSerialReset } from "./reset.js"; import { getStubJsonByChipName } from "./stubFlasher.js"; import { padTo } from "./util.js"; /** * Return the chip ROM based on the given magic number * @param {number} magic - magic hex number to select ROM. * @returns {ROM} The chip ROM class related to given magic hex number. */ async function magic2Chip(magic) { switch (magic) { case 0x00f01d83: { const { ESP32ROM } = await import("./targets/esp32.js"); return new ESP32ROM(); } case 0xc21e06f: case 0x6f51306f: case 0x7c41a06f: { const { ESP32C2ROM } = await import("./targets/esp32c2.js"); return new ESP32C2ROM(); } case 0x6921506f: case 0x1b31506f: case 0x4881606f: case 0x4361606f: { const { ESP32C3ROM } = await import("./targets/esp32c3.js"); return new ESP32C3ROM(); } case 0x2ce0806f: { const { ESP32C6ROM } = await import("./targets/esp32c6.js"); return new ESP32C6ROM(); } case 0x2421606f: case 0x33f0206f: case 0x4f81606f: { const { ESP32C61ROM } = await import("./targets/esp32c61.js"); return new ESP32C61ROM(); } case 0x1101406f: case 0x63e1406f: case 0x5fd1406f: { const { ESP32C5ROM } = await import("./targets/esp32c5.js"); return new ESP32C5ROM(); } case 0xd7b73e80: case 0x97e30068: { const { ESP32H2ROM } = await import("./targets/esp32h2.js"); return new ESP32H2ROM(); } case 0x09: { const { ESP32S3ROM } = await import("./targets/esp32s3.js"); return new ESP32S3ROM(); } case 0x000007c6: { const { ESP32S2ROM } = await import("./targets/esp32s2.js"); return new ESP32S2ROM(); } case 0xfff0c101: { const { ESP8266ROM } = await import("./targets/esp8266.js"); return new ESP8266ROM(); } case 0x0: case 0x0addbad0: case 0x7039ad9: { const { ESP32P4ROM } = await import("./targets/esp32p4.js"); return new ESP32P4ROM(); } default: return null; } } export class ESPLoader { /** * Create a new ESPLoader to perform serial communication * such as read/write flash memory and registers using a LoaderOptions object. * @param {LoaderOptions} options - LoaderOptions object argument for ESPLoader. * ``` * const myLoader = new ESPLoader({ transport: Transport, baudrate: number, terminal?: IEspLoaderTerminal }); * ``` */ constructor(options) { var _a, _b, _c, _d, _e, _f, _g, _h; this.ESP_RAM_BLOCK = 0x1800; this.ESP_FLASH_BEGIN = 0x02; this.ESP_FLASH_DATA = 0x03; this.ESP_FLASH_END = 0x04; this.ESP_MEM_BEGIN = 0x05; this.ESP_MEM_END = 0x06; this.ESP_MEM_DATA = 0x07; this.ESP_WRITE_REG = 0x09; this.ESP_READ_REG = 0x0a; this.ESP_SPI_ATTACH = 0x0d; this.ESP_CHANGE_BAUDRATE = 0x0f; this.ESP_FLASH_DEFL_BEGIN = 0x10; this.ESP_FLASH_DEFL_DATA = 0x11; this.ESP_FLASH_DEFL_END = 0x12; this.ESP_SPI_FLASH_MD5 = 0x13; // Only Stub supported commands this.ESP_ERASE_FLASH = 0xd0; this.ESP_ERASE_REGION = 0xd1; this.ESP_READ_FLASH = 0xd2; this.ESP_RUN_USER_CODE = 0xd3; this.ESP_IMAGE_MAGIC = 0xe9; this.ESP_CHECKSUM_MAGIC = 0xef; // Response code(s) sent by ROM this.ROM_INVALID_RECV_MSG = 0x05; // response if an invalid message is received this.DEFAULT_TIMEOUT = 3000; this.ERASE_REGION_TIMEOUT_PER_MB = 30000; this.ERASE_WRITE_TIMEOUT_PER_MB = 40000; this.MD5_TIMEOUT_PER_MB = 8000; this.CHIP_ERASE_TIMEOUT = 120000; this.FLASH_READ_TIMEOUT = 100000; this.MAX_TIMEOUT = this.CHIP_ERASE_TIMEOUT * 2; this.CHIP_DETECT_MAGIC_REG_ADDR = 0x40001000; this.DETECTED_FLASH_SIZES = { 0x12: "256KB", 0x13: "512KB", 0x14: "1MB", 0x15: "2MB", 0x16: "4MB", 0x17: "8MB", 0x18: "16MB", }; this.DETECTED_FLASH_SIZES_NUM = { 0x12: 256, 0x13: 512, 0x14: 1024, 0x15: 2048, 0x16: 4096, 0x17: 8192, 0x18: 16384, }; this.USB_JTAG_SERIAL_PID = 0x1001; this.romBaudrate = 115200; this.debugLogging = false; this.syncStubDetected = false; /** * Get flash size bytes from flash size string. * @param {string} flashSize Flash Size string * @returns {number} Flash size bytes */ this.flashSizeBytes = function (flashSize) { let flashSizeB = -1; if (flashSize.indexOf("KB") !== -1) { flashSizeB = parseInt(flashSize.slice(0, flashSize.indexOf("KB"))) * 1024; } else if (flashSize.indexOf("MB") !== -1) { flashSizeB = parseInt(flashSize.slice(0, flashSize.indexOf("MB"))) * 1024 * 1024; } return flashSizeB; }; this.IS_STUB = false; this.FLASH_WRITE_SIZE = 0x4000; this.transport = options.transport; this.baudrate = options.baudrate; this.resetConstructors = { classicReset: (transport, resetDelay) => new ClassicReset(transport, resetDelay), customReset: (transport, sequenceString) => new CustomReset(transport, sequenceString), hardReset: (transport, usingUsbOtg) => new HardReset(transport, usingUsbOtg), usbJTAGSerialReset: (transport) => new UsbJtagSerialReset(transport), }; if (options.serialOptions) { this.serialOptions = options.serialOptions; } if (options.romBaudrate) { this.romBaudrate = options.romBaudrate; } if (options.terminal) { this.terminal = options.terminal; this.terminal.clean(); } if (typeof options.debugLogging !== "undefined") { this.debugLogging = options.debugLogging; } if (options.port) { this.transport = new Transport(options.port); } if (typeof options.enableTracing !== "undefined") { this.transport.tracing = options.enableTracing; } if ((_a = options.resetConstructors) === null || _a === void 0 ? void 0 : _a.classicReset) { this.resetConstructors.classicReset = (_b = options.resetConstructors) === null || _b === void 0 ? void 0 : _b.classicReset; } if ((_c = options.resetConstructors) === null || _c === void 0 ? void 0 : _c.customReset) { this.resetConstructors.customReset = (_d = options.resetConstructors) === null || _d === void 0 ? void 0 : _d.customReset; } if ((_e = options.resetConstructors) === null || _e === void 0 ? void 0 : _e.hardReset) { this.resetConstructors.hardReset = (_f = options.resetConstructors) === null || _f === void 0 ? void 0 : _f.hardReset; } if ((_g = options.resetConstructors) === null || _g === void 0 ? void 0 : _g.usbJTAGSerialReset) { this.resetConstructors.usbJTAGSerialReset = (_h = options.resetConstructors) === null || _h === void 0 ? void 0 : _h.usbJTAGSerialReset; } this.info("esptool.js"); this.info("Serial port " + this.transport.getInfo()); } _sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } /** * Write to ESP Loader constructor's terminal with or without new line. * @param {string} str - String to write. * @param {boolean} withNewline - Add new line at the end ? */ write(str, withNewline = true) { if (this.terminal) { if (withNewline) { this.terminal.writeLine(str); } else { this.terminal.write(str); } } else { // eslint-disable-next-line no-console console.log(str); } } /** * Write error message to ESP Loader constructor's terminal with or without new line. * @param {string} str - String to write. * @param {boolean} withNewline - Add new line at the end ? */ error(str, withNewline = true) { this.write(`Error: ${str}`, withNewline); } /** * Write information message to ESP Loader constructor's terminal with or without new line. * @param {string} str - String to write. * @param {boolean} withNewline - Add new line at the end ? */ info(str, withNewline = true) { this.write(str, withNewline); } /** * Write debug message to ESP Loader constructor's terminal with or without new line. * @param {string} str - String to write. * @param {boolean} withNewline - Add new line at the end ? */ debug(str, withNewline = true) { if (this.debugLogging) { this.write(`Debug: ${str}`, withNewline); } } /** * Convert short integer to byte array * @param {number} i - Number to convert. * @returns {Uint8Array} Byte array. */ _shortToBytearray(i) { return new Uint8Array([i & 0xff, (i >> 8) & 0xff]); } /** * Convert an integer to byte array * @param {number} i - Number to convert. * @returns {ROM} The chip ROM class related to given magic hex number. */ _intToByteArray(i) { return new Uint8Array([i & 0xff, (i >> 8) & 0xff, (i >> 16) & 0xff, (i >> 24) & 0xff]); } /** * Convert a byte array to short integer. * @param {number} i - Number to convert. * @param {number} j - Number to convert. * @returns {number} Return a short integer number. */ _byteArrayToShort(i, j) { return i | (j >> 8); } /** * Convert a byte array to integer. * @param {number} i - Number to convert. * @param {number} j - Number to convert. * @param {number} k - Number to convert. * @param {number} l - Number to convert. * @returns {number} Return a integer number. */ _byteArrayToInt(i, j, k, l) { return i | (j << 8) | (k << 16) | (l << 24); } /** * Append a buffer array after another buffer array * @param {ArrayBuffer} buffer1 - First array buffer. * @param {ArrayBuffer} buffer2 - magic hex number to select ROM. * @returns {ArrayBufferLike} Return an array buffer. */ _appendBuffer(buffer1, buffer2) { const tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength); tmp.set(new Uint8Array(buffer1), 0); tmp.set(new Uint8Array(buffer2), buffer1.byteLength); return tmp.buffer; } /** * Append a buffer array after another buffer array * @param {Uint8Array} arr1 - First array buffer. * @param {Uint8Array} arr2 - magic hex number to select ROM. * @returns {Uint8Array} Return a 8 bit unsigned array. */ _appendArray(arr1, arr2) { const c = new Uint8Array(arr1.length + arr2.length); c.set(arr1, 0); c.set(arr2, arr1.length); return c; } /** * Convert a unsigned 8 bit integer array to byte string. * @param {Uint8Array} u8Array - magic hex number to select ROM. * @returns {string} Return the equivalent string. */ ui8ToBstr(u8Array) { let bStr = ""; for (let i = 0; i < u8Array.length; i++) { bStr += String.fromCharCode(u8Array[i]); } return bStr; } /** * Convert a byte string to unsigned 8 bit integer array. * @param {string} bStr - binary string input * @returns {Uint8Array} Return a 8 bit unsigned integer array. */ bstrToUi8(bStr) { const u8Array = new Uint8Array(bStr.length); for (let i = 0; i < bStr.length; i++) { u8Array[i] = bStr.charCodeAt(i); } return u8Array; } /** * Flush the serial input by raw read with 200 ms timeout. */ async flushInput() { try { await this.transport.flushInput(); } catch (e) { this.error(e.message); } } /** * Use the device serial port read function with given timeout to create a valid packet. * @param {number} op Operation number * @param {number} timeout timeout number in milliseconds * @returns {[number, Uint8Array]} valid response packet. */ async readPacket(op = null, timeout = this.DEFAULT_TIMEOUT) { // Check up-to next 100 packets for valid response packet for (let i = 0; i < 100; i++) { const { value: p } = await this.transport.read(timeout).next(); if (!p || p.length < 8) { continue; } const resp = p[0]; if (resp !== 1) { continue; } const opRet = p[1]; const val = this._byteArrayToInt(p[4], p[5], p[6], p[7]); const data = p.slice(8); if (resp == 1) { if (op == null || opRet == op) { return [val, data]; } else if (data[0] != 0 && data[1] == this.ROM_INVALID_RECV_MSG) { await this.flushInput(); throw new ESPError("unsupported command error"); } } } throw new ESPError("invalid response"); } /** * Write a serial command to the chip * @param {number} op - Operation number * @param {Uint8Array} data - Unsigned 8 bit array * @param {number} chk - channel number * @param {boolean} waitResponse - wait for response ? * @param {number} timeout - timeout number in milliseconds * @returns {Promise<[number, Uint8Array]>} Return a number and a 8 bit unsigned integer array. */ async command(op = null, data = new Uint8Array(0), chk = 0, waitResponse = true, timeout = this.DEFAULT_TIMEOUT) { if (op != null) { if (this.transport.tracing) { this.transport.trace(`command op:0x${op.toString(16).padStart(2, "0")} data len=${data.length} wait_response=${waitResponse ? 1 : 0} timeout=${(timeout / 1000).toFixed(3)} data=${this.transport.hexConvert(data)}`); } const pkt = new Uint8Array(8 + data.length); pkt[0] = 0x00; pkt[1] = op; pkt[2] = this._shortToBytearray(data.length)[0]; pkt[3] = this._shortToBytearray(data.length)[1]; pkt[4] = this._intToByteArray(chk)[0]; pkt[5] = this._intToByteArray(chk)[1]; pkt[6] = this._intToByteArray(chk)[2]; pkt[7] = this._intToByteArray(chk)[3]; let i; for (i = 0; i < data.length; i++) { pkt[8 + i] = data[i]; } await this.transport.write(pkt); } if (!waitResponse) { return [0, new Uint8Array(0)]; } return this.readPacket(op, timeout); } /** * Read a register from chip. * @param {number} addr - Register address number * @param {number} timeout - Timeout in milliseconds (Default: 3000ms) * @returns {number} - Command number value */ async readReg(addr, timeout = this.DEFAULT_TIMEOUT) { const pkt = this._intToByteArray(addr); const val = await this.command(this.ESP_READ_REG, pkt, undefined, undefined, timeout); return val[0]; } /** * Write a number value to register address in chip. * @param {number} addr - Register address number * @param {number} value - Number value to write in register * @param {number} mask - Hex number for mask * @param {number} delayUs Delay number * @param {number} delayAfterUs Delay after previous delay */ async writeReg(addr, value, mask = 0xffffffff, delayUs = 0, delayAfterUs = 0) { let pkt = this._appendArray(this._intToByteArray(addr), this._intToByteArray(value)); pkt = this._appendArray(pkt, this._intToByteArray(mask)); pkt = this._appendArray(pkt, this._intToByteArray(delayUs)); if (delayAfterUs > 0) { pkt = this._appendArray(pkt, this._intToByteArray(this.chip.UART_DATE_REG_ADDR)); pkt = this._appendArray(pkt, this._intToByteArray(0)); pkt = this._appendArray(pkt, this._intToByteArray(0)); pkt = this._appendArray(pkt, this._intToByteArray(delayAfterUs)); } await this.checkCommand("write target memory", this.ESP_WRITE_REG, pkt); } /** * Sync chip by sending sync command. * @returns {[number, Uint8Array]} Command result */ async sync() { this.debug("Sync"); const cmd = new Uint8Array(36); let i; cmd[0] = 0x07; cmd[1] = 0x07; cmd[2] = 0x12; cmd[3] = 0x20; for (i = 0; i < 32; i++) { cmd[4 + i] = 0x55; } try { let resp = await this.command(0x08, cmd, undefined, undefined, 100); // ROM bootloaders send some non-zero "val" response. The flasher stub sends 0. // If we receive 0 then it probably indicates that the chip wasn't or couldn't be // reset properly and esptool is talking to the flasher stub. this.syncStubDetected = resp[0] === 0; for (let i = 0; i < 7; i++) { resp = await this.command(); this.syncStubDetected = this.syncStubDetected && resp[0] === 0; } return resp; } catch (e) { this.debug("Sync err " + e); throw e; } } /** * Attempt to connect to the chip by sending a reset sequence and later a sync command. * @param {string} mode - Reset mode to use * @param {ResetStrategy} resetStrategy - Reset strategy class to use for connect * @returns {string} - Returns 'success' or 'error' message. */ async _connectAttempt(mode = "default_reset", resetStrategy) { this.debug("_connect_attempt " + mode); if (resetStrategy) { await resetStrategy.reset(); } const waitingBytes = this.transport.inWaiting(); const readBytes = await this.transport.newRead(waitingBytes > 0 ? waitingBytes : 1, this.DEFAULT_TIMEOUT); const binaryString = Array.from(readBytes, (byte) => String.fromCharCode(byte)).join(""); const regex = /boot:(0x[0-9a-fA-F]+)(.*waiting for download)?/; const match = binaryString.match(regex); let bootLogDetected = false, bootMode = "", downloadMode = false; if (match) { bootLogDetected = true; bootMode = match[1]; downloadMode = !!match[2]; } let lastError = ""; for (let i = 0; i < 5; i++) { try { this.debug(`Sync connect attempt ${i}`); const resp = await this.sync(); this.debug(resp[0].toString()); return "success"; } catch (error) { this.debug(`Error at sync ${error}`); if (error instanceof Error) { lastError = error.message; } else if (typeof error === "string") { lastError = error; } else { lastError = JSON.stringify(error); } } } if (bootLogDetected) { lastError = `Wrong boot mode detected (${bootMode}). This chip needs to be in download mode.`; if (downloadMode) { lastError = `Download mode successfully detected, but getting no sync reply: The serial TX path seems to be down.`; } } return lastError; } /** * Constructs a sequence of reset strategies based on the OS, * used ESP chip, external settings, and environment variables. * Returns a tuple of one or more reset strategies to be tried sequentially. * @param {string} mode - Reset mode to use * @returns {ResetStrategy[]} - Array of reset strategies */ constructResetSequence(mode) { if (mode !== "no_reset") { if (mode === "usb_reset" || this.transport.getPid() === this.USB_JTAG_SERIAL_PID) { // Custom reset sequence, which is required when the device // is connecting via its USB-JTAG-Serial peripheral if (this.resetConstructors.usbJTAGSerialReset) { this.debug("using USB JTAG Serial Reset"); return [this.resetConstructors.usbJTAGSerialReset(this.transport)]; } } else { const DEFAULT_RESET_DELAY = 50; const EXTRA_DELAY = DEFAULT_RESET_DELAY + 500; if (this.resetConstructors.classicReset) { this.debug("using Classic Serial Reset"); return [ this.resetConstructors.classicReset(this.transport, DEFAULT_RESET_DELAY), this.resetConstructors.classicReset(this.transport, EXTRA_DELAY), ]; } } } return []; } /** * Perform a connection to chip. * @param {string} mode - Reset mode to use. Example: 'default_reset' | 'no_reset' * @param {number} attempts - Number of connection attempts * @param {boolean} detecting - Detect the connected chip */ async connect(mode = "default_reset", attempts = 7, detecting = true) { let resp; this.info("Connecting...", false); await this.transport.connect(this.romBaudrate, this.serialOptions); const resetSequences = this.constructResetSequence(mode); for (let i = 0; i < attempts; i++) { const resetSequence = resetSequences.length > 0 ? resetSequences[i % resetSequences.length] : null; resp = await this._connectAttempt(mode, resetSequence); if (resp === "success") { break; } } if (resp !== "success") { throw new ESPError("Failed to connect with the device"); } this.debug("Connect attempt successful."); this.info("\n\r", false); if (detecting) { const chipMagicValue = (await this.readReg(this.CHIP_DETECT_MAGIC_REG_ADDR)) >>> 0; this.debug("Chip Magic " + chipMagicValue.toString(16)); const chip = await magic2Chip(chipMagicValue); if (this.chip === null) { throw new ESPError(`Unexpected CHIP magic value ${chipMagicValue}. Failed to autodetect chip type.`); } else { this.chip = chip; } } } /** * Connect and detect the existing chip. * @param {string} mode Reset mode to use for connection. */ async detectChip(mode = "default_reset") { await this.connect(mode); this.info("Detecting chip type... ", false); if (this.chip != null) { this.info(this.chip.CHIP_NAME); } else { this.info("unknown!"); } } /** * Execute the command and check the command response. * @param {string} opDescription Command operation description. * @param {number} op Command operation number * @param {Uint8Array} data Command value * @param {number} chk Checksum to use * @param {number} timeout TImeout number in milliseconds (ms) * @returns {number} Command result */ async checkCommand(opDescription = "", op = null, data = new Uint8Array(0), chk = 0, timeout = this.DEFAULT_TIMEOUT) { this.debug("check_command " + opDescription); const resp = await this.command(op, data, chk, undefined, timeout); if (resp[1].length > 4) { return resp[1]; } else { return resp[0]; } } /** * Start downloading an application image to RAM * @param {number} size Image size number * @param {number} blocks Number of data blocks * @param {number} blocksize Size of each data block * @param {number} offset Image offset number */ async memBegin(size, blocks, blocksize, offset) { /* XXX: Add check to ensure that STUB is not getting overwritten */ if (this.IS_STUB) { const loadStart = offset; const loadEnd = offset + size; const stub = await getStubJsonByChipName(this.chip.CHIP_NAME); if (stub) { const areasToCheck = [ [stub.bss_start || stub.data_start, stub.data_start + stub.decodedData.length], [stub.text_start, stub.text_start + stub.decodedText.length], ]; for (const [stubStart, stubEnd] of areasToCheck) { if (loadStart < stubEnd && loadEnd > stubStart) { throw new ESPError(`Software loader is resident at 0x${stubStart.toString(16).padStart(8, "0")}-0x${stubEnd .toString(16) .padStart(8, "0")}. Can't load binary at overlapping address range 0x${loadStart.toString(16).padStart(8, "0")}-0x${loadEnd .toString(16) .padStart(8, "0")}. Either change binary loading address, or use the no-stub option to disable the software loader.`); } } } } this.debug("mem_begin " + size + " " + blocks + " " + blocksize + " " + offset.toString(16)); let pkt = this._appendArray(this._intToByteArray(size), this._intToByteArray(blocks)); pkt = this._appendArray(pkt, this._intToByteArray(blocksize)); pkt = this._appendArray(pkt, this._intToByteArray(offset)); await this.checkCommand("enter RAM download mode", this.ESP_MEM_BEGIN, pkt); } /** * Get the checksum for given unsigned 8-bit array * @param {Uint8Array} data Unsigned 8-bit integer array * @param {number} state Initial checksum * @returns {number} - Array checksum */ checksum(data, state = this.ESP_CHECKSUM_MAGIC) { for (let i = 0; i < data.length; i++) { state ^= data[i]; } return state; } /** * Send a block of image to RAM * @param {Uint8Array} buffer Unsigned 8-bit array * @param {number} seq Sequence number */ async memBlock(buffer, seq) { let pkt = this._appendArray(this._intToByteArray(buffer.length), this._intToByteArray(seq)); pkt = this._appendArray(pkt, this._intToByteArray(0)); pkt = this._appendArray(pkt, this._intToByteArray(0)); pkt = this._appendArray(pkt, buffer); const checksum = this.checksum(buffer); await this.checkCommand("write to target RAM", this.ESP_MEM_DATA, pkt, checksum); } /** * Leave RAM download mode and run application * @param {number} entrypoint - Entrypoint number */ async memFinish(entrypoint) { const isEntry = entrypoint === 0 ? 1 : 0; const pkt = this._appendArray(this._intToByteArray(isEntry), this._intToByteArray(entrypoint)); await this.checkCommand("leave RAM download mode", this.ESP_MEM_END, pkt, undefined, 200); // XXX: handle non-stub with diff timeout } /** * Configure SPI flash pins * @param {number} hspiArg - Argument for SPI attachment */ async flashSpiAttach(hspiArg) { const pkt = this._intToByteArray(hspiArg); await this.checkCommand("configure SPI flash pins", this.ESP_SPI_ATTACH, pkt); } /** * Scale timeouts which are size-specific. * @param {number} secondsPerMb Seconds per megabytes as number * @param {number} sizeBytes Size bytes number * @returns {number} - Scaled timeout for specified size. */ timeoutPerMb(secondsPerMb, sizeBytes) { const result = secondsPerMb * (sizeBytes / 1000000); if (result < 3000) { return 3000; } else { return result; } } /** * Start downloading to Flash (performs an erase) * @param {number} size Size to erase * @param {number} offset Offset to erase * @returns {number} Number of blocks (of size self.FLASH_WRITE_SIZE) to write. */ async flashBegin(size, offset) { const numBlocks = Math.floor((size + this.FLASH_WRITE_SIZE - 1) / this.FLASH_WRITE_SIZE); const eraseSize = this.chip.getEraseSize(offset, size); const d = new Date(); const t1 = d.getTime(); let timeout = 3000; if (this.IS_STUB == false) { timeout = this.timeoutPerMb(this.ERASE_REGION_TIMEOUT_PER_MB, size); } this.debug("flash begin " + eraseSize + " " + numBlocks + " " + this.FLASH_WRITE_SIZE + " " + offset + " " + size); let pkt = this._appendArray(this._intToByteArray(eraseSize), this._intToByteArray(numBlocks)); pkt = this._appendArray(pkt, this._intToByteArray(this.FLASH_WRITE_SIZE)); pkt = this._appendArray(pkt, this._intToByteArray(offset)); if (this.IS_STUB == false) { pkt = this._appendArray(pkt, this._intToByteArray(0)); // XXX: Support encrypted } await this.checkCommand("enter Flash download mode", this.ESP_FLASH_BEGIN, pkt, undefined, timeout); const t2 = d.getTime(); if (size != 0 && this.IS_STUB == false) { this.info("Took " + (t2 - t1) / 1000 + "." + ((t2 - t1) % 1000) + "s to erase flash block"); } return numBlocks; } /** * Start downloading compressed data to Flash (performs an erase) * @param {number} size Write size * @param {number} compsize Compressed size * @param {number} offset Offset for write * @returns {number} Returns number of blocks (size self.FLASH_WRITE_SIZE) to write. */ async flashDeflBegin(size, compsize, offset) { const numBlocks = Math.floor((compsize + this.FLASH_WRITE_SIZE - 1) / this.FLASH_WRITE_SIZE); const eraseBlocks = Math.floor((size + this.FLASH_WRITE_SIZE - 1) / this.FLASH_WRITE_SIZE); const d = new Date(); const t1 = d.getTime(); let writeSize, timeout; if (this.IS_STUB) { writeSize = size; timeout = this.DEFAULT_TIMEOUT; } else { writeSize = eraseBlocks * this.FLASH_WRITE_SIZE; timeout = this.timeoutPerMb(this.ERASE_REGION_TIMEOUT_PER_MB, writeSize); } this.info("Compressed " + size + " bytes to " + compsize + "..."); let pkt = this._appendArray(this._intToByteArray(writeSize), this._intToByteArray(numBlocks)); pkt = this._appendArray(pkt, this._intToByteArray(this.FLASH_WRITE_SIZE)); pkt = this._appendArray(pkt, this._intToByteArray(offset)); if ((this.chip.CHIP_NAME === "ESP32-S2" || this.chip.CHIP_NAME === "ESP32-S3" || this.chip.CHIP_NAME === "ESP32-C3" || this.chip.CHIP_NAME === "ESP32-C2") && this.IS_STUB === false) { pkt = this._appendArray(pkt, this._intToByteArray(0)); } await this.checkCommand("enter compressed flash mode", this.ESP_FLASH_DEFL_BEGIN, pkt, undefined, timeout); const t2 = d.getTime(); if (size != 0 && this.IS_STUB === false) { this.info("Took " + (t2 - t1) / 1000 + "." + ((t2 - t1) % 1000) + "s to erase flash block"); } return numBlocks; } /** * Write block to flash, retry if fail * @param {Uint8Array} data Unsigned 8-bit array data. * @param {number} seq Sequence number * @param {number} timeout Timeout in milliseconds (ms) */ async flashBlock(data, seq, timeout) { let pkt = this._appendArray(this._intToByteArray(data.length), this._intToByteArray(seq)); pkt = this._appendArray(pkt, this._intToByteArray(0)); pkt = this._appendArray(pkt, this._intToByteArray(0)); pkt = this._appendArray(pkt, data); const checksum = this.checksum(data); await this.checkCommand("write to target Flash after seq " + seq, this.ESP_FLASH_DATA, pkt, checksum, timeout); } /** * Write block to flash, send compressed, retry if fail * @param {Uint8Array} data Unsigned int 8-bit array data to write * @param {number} seq Sequence number * @param {number} timeout Timeout in milliseconds (ms) */ async flashDeflBlock(data, seq, timeout) { let pkt = this._appendArray(this._intToByteArray(data.length), this._intToByteArray(seq)); pkt = this._appendArray(pkt, this._intToByteArray(0)); pkt = this._appendArray(pkt, this._intToByteArray(0)); pkt = this._appendArray(pkt, data); const checksum = this.checksum(data); this.debug("flash_defl_block " + data[0].toString(16) + " " + data[1].toString(16)); await this.checkCommand("write compressed data to flash after seq " + seq, this.ESP_FLASH_DEFL_DATA, pkt, checksum, timeout); } /** * Leave flash mode and run/reboot * @param {boolean} reboot Reboot after leaving flash mode ? */ async flashFinish(reboot = false) { const val = reboot ? 0 : 1; const pkt = this._intToByteArray(val); await this.checkCommand("leave Flash mode", this.ESP_FLASH_END, pkt); } /** * Leave compressed flash mode and run/reboot * @param {boolean} reboot Reboot after leaving flash mode ? */ async flashDeflFinish(reboot = false) { const val = reboot ? 0 : 1; const pkt = this._intToByteArray(val); await this.checkCommand("leave compressed flash mode", this.ESP_FLASH_DEFL_END, pkt); } /** * Run an arbitrary SPI flash command. * * This function uses the "USR_COMMAND" functionality in the ESP * SPI hardware, rather than the precanned commands supported by * hardware. So the value of spiflashCommand is an actual command * byte, sent over the wire. * * After writing command byte, writes 'data' to MOSI and then * reads back 'readBits' of reply on MISO. Result is a number. * @param {number} spiflashCommand Command to execute in SPI * @param {Uint8Array} data Data to send * @param {number} readBits Number of bits to read * @returns {number} Register SPI_W0_REG value */ async runSpiflashCommand(spiflashCommand, data, readBits) { // SPI_USR register flags const SPI_USR_COMMAND = 1 << 31; const SPI_USR_MISO = 1 << 28; const SPI_USR_MOSI = 1 << 27; // SPI registers, base address differs ESP32* vs 8266 const base = this.chip.SPI_REG_BASE; const SPI_CMD_REG = base + 0x00; const SPI_USR_REG = base + this.chip.SPI_USR_OFFS; const SPI_USR1_REG = base + this.chip.SPI_USR1_OFFS; const SPI_USR2_REG = base + this.chip.SPI_USR2_OFFS; const SPI_W0_REG = base + this.chip.SPI_W0_OFFS; let setDataLengths; if (this.chip.SPI_MOSI_DLEN_OFFS != null) { setDataLengths = async (mosiBits, misoBits) => { const SPI_MOSI_DLEN_REG = base + this.chip.SPI_MOSI_DLEN_OFFS; const SPI_MISO_DLEN_REG = base + this.chip.SPI_MISO_DLEN_OFFS; if (mosiBits > 0) { await this.writeReg(SPI_MOSI_DLEN_REG, mosiBits - 1); } if (misoBits > 0) { await this.writeReg(SPI_MISO_DLEN_REG, misoBits - 1); } }; } else { setDataLengths = async (mosiBits, misoBits) => { const SPI_DATA_LEN_REG = SPI_USR1_REG; const SPI_MOSI_BITLEN_S = 17; const SPI_MISO_BITLEN_S = 8; const mosiMask = mosiBits === 0 ? 0 : mosiBits - 1; const misoMask = misoBits === 0 ? 0 : misoBits - 1; const val = (misoMask << SPI_MISO_BITLEN_S) | (mosiMask << SPI_MOSI_BITLEN_S); await this.writeReg(SPI_DATA_LEN_REG, val); }; } const SPI_CMD_USR = 1 << 18; const SPI_USR2_COMMAND_LEN_SHIFT = 28; if (readBits > 32) { throw new ESPError("Reading more than 32 bits back from a SPI flash operation is unsupported"); } if (data.length > 64) { throw new ESPError("Writing more than 64 bytes of data with one SPI command is unsupported"); } const dataBits = data.length * 8; const oldSpiUsr = await this.readReg(SPI_USR_REG); const oldSpiUsr2 = await this.readReg(SPI_USR2_REG); let flags = SPI_USR_COMMAND; let i; if (readBits > 0) { flags |= SPI_USR_MISO; } if (dataBits > 0) { flags |= SPI_USR_MOSI; } await setDataLengths(dataBits, readBits); await this.writeReg(SPI_USR_REG, flags); let val = (7 << SPI_USR2_COMMAND_LEN_SHIFT) | spiflashCommand; await this.writeReg(SPI_USR2_REG, val); if (dataBits == 0) { await this.writeReg(SPI_W0_REG, 0); } else { if (data.length % 4 != 0) { const padding = new Uint8Array(data.length % 4); data = this._appendArray(data, padding); } let nextReg = SPI_W0_REG; for (i = 0; i < data.length - 4; i += 4) { val = this._byteArrayToInt(data[i], data[i + 1], data[i + 2], data[i + 3]); await this.writeReg(nextReg, val); nextReg += 4; } } await this.writeReg(SPI_CMD_REG, SPI_CMD_USR); for (i = 0; i < 10; i++) { val = (await this.readReg(SPI_CMD_REG)) & SPI_CMD_USR; if (val == 0) { break; } } if (i === 10) { throw new ESPError("SPI command did not complete in time"); } const stat = await this.readReg(SPI_W0_REG); await this.writeReg(SPI_USR_REG, oldSpiUsr); await this.writeReg(SPI_USR2_REG, oldSpiUsr2); return stat; } /** * Read flash id by executing the SPIFLASH_RDID flash command. * @returns {Promise} Register SPI_W0_REG value */ async readFlashId() { const SPIFLASH_RDID = 0x9f; const pkt = new Uint8Array(0); return await this.runSpiflashCommand(SPIFLASH_RDID, pkt, 24); } /** * Execute the erase flash command * @returns {Promise} Erase flash command result */ async eraseFlash() { this.info("Erasing flash (this may take a while)..."); let d = new Date(); const t1 = d.getTime(); const ret = await this.checkCommand("erase flash", this.ESP_ERASE_FLASH, undefined, undefined, this.CHIP_ERASE_TIMEOUT); d = new Date(); const t2 = d.getTime(); this.info("Chip erase completed successfully in " + (t2 - t1) / 1000 + "s"); return ret; } /** * Convert a number or unsigned 8-bit array to hex string * @param {number | Uint8Array } buffer Data to convert to hex string. * @returns {string} A hex string */ toHex(buffer) { return Array.prototype.map.call(buffer, (x) => ("00" + x.toString(16)).slice(-2)).join(""); } /** * Calculate the MD5 Checksum command * @param {number} addr Address number * @param {number} size Package size * @returns {string} MD5 Checksum string */ async flashMd5sum(addr, size) { const timeout = this.timeoutPerMb(this.MD5_TIMEOUT_PER_MB, size); let pkt = this._appendArray(this._intToByteArray(addr), this._intToByteArray(size)); pkt = this._appendArray(pkt, this._intToByteArray(0)); pkt = this._appendArray(pkt, this._intToByteArray(0)); let res = await this.checkCommand("calculate md5sum", this.ESP_SPI_FLASH_MD5, pkt, undefined, timeout); if (res instanceof Uint8Array && res.length > 16) { res = res.slice(0, 16); } const strmd5 = this.toHex(res); return strmd5; } async readFlash(addr, size, onPacketReceived = null) { let pkt = this._appendArray(this._intToByteArray(addr), this._intToByteArray(size)); pkt = this._appendArray(pkt, this._intToByteArray(0x1000)); pkt = this._appendArray(pkt, this._intToByteArray(1024)); const res = await this.checkCommand("read flash", this.ESP_READ_FLASH, pkt); if (res != 0) { throw new ESPError("Failed to read memory: " + res); } let resp = new Uint8Array(0); while (resp.length < size) { const { value: packet } = await this.transport.read(this.FLASH_READ_TIMEOUT).next(); if (packet instanceof Uint8Array) { if (packet.length > 0) { resp = this._appendArray(resp, packet); await this.transport.write(this._intToByteArray(resp.length)); if (onPacketReceived) { onPacketReceived(packet, resp.length, size); } } } else { throw new ESPError("Failed to read memory: " + packet); } } return resp; } /** * Upload the flasher ROM bootloader (flasher stub) to the chip. * @returns {ROM} The Chip ROM */ async runStub() { if (this.syncStubDetected) { this.info("Stub is already running. No upload is necessary."); return this.chip; } this.info("Uploading stub..."); const stubFlasher = await getStubJsonByChipName(this.chip.CHIP_NAME); if (stubFlasher === undefined) { this.debug("Error loading Stub json"); throw new Error("Error loading Stub json"); } const stub = [stubFlasher.decodedText, stubFlasher.decodedData]; for (let i = 0; i < stub.length; i++) { if (stub[i]) { const offs = i === 0 ? stubFlasher.text_start : stubFlasher.data_start; const length = stub[i].length; const blocks = Math.floor((length + this.ESP_RAM_BLOCK - 1) / this.ESP_RAM_BLOCK); await this.memBegin(length, blocks, this.ESP_RAM_BLOCK, offs); for (let seq = 0; seq < blocks; seq++) { const fromOffs = seq * this.ESP_RAM_BLOCK; const toOffs = fromOffs + this.ESP_RAM_BLOCK; await this.memBlock(stub[i].slice(fromOffs, toOffs), seq); } } } this.info("Running stub..."); await this.memFinish(stubFlasher.entry); const { value: packetResult } = await this.transport.read(this.DEFAULT_TIMEOUT).next(); const packetStr = String.fromCharCode(...packetResult); if (packetStr !== "OHAI") { throw new ESPError(`Failed to start stub. Unexpected response ${packetStr}`); } this.info("Stub running..."); this.IS_STUB = true; return this.chip; } /** * Change the chip baudrate. */ async changeBaud() { this.info("Changing baudrate to " + this.baudrate); const secondArg = this.IS_STUB ? this.romBaudrate : 0; const pkt = this._appendArray(this._intToByteArray(this.baudrate), this._intToByteArray(secondArg)); await this.command(this.ESP_CHANGE_BAUDRATE, pkt); this.info("Changed"); await this.transport.disconnect(); await this._sleep(50); await this.transport.connect(this.baudrate, this.serialOptions); } /** * Execute the main function of ESPLoader. * @param {string} mode Reset mode to use * @returns {string} chip ROM */ async main(mode = "default_reset") { await this.detectChip(mode); const chip = await this.chip.getChipDescription(this); this.info("Chip is " + chip); this.info("Features: " + (await this.chip.getChipFeatures(this))); this.info("Crystal is " + (await this.chip.getCrystalFreq(this)) + "MHz"); this.info("MAC: " + (await this.chip.readMac(this))); await this.chip.readMac(this); if (typeof this.chip.postConnect != "undefined") { await this.chip.postConnect(this); } await this.runStub(); if (this.romBaudrate !== this.baudrate) { await this.changeBaud(); } return chip; } /** * Parse a given flash size string to a number * @param {string} flsz Flash size to request * @returns {number} Flash size number */ parseFlashSizeArg(flsz) { if (typeof this.chip.FLASH_SIZES[flsz] === "undefined") { throw new ESPError("Flash size " + flsz + " is not supported by this chip type. Supported sizes: " + this.chip.FLASH_SIZES); } return this.chip.FLASH_SIZES[flsz]; } /** * Update the image flash parameters with given arguments. * @param {string} image binary image as string * @param {number} address flash address number * @param {string} flashSize Flash size string * @param {string} flashMode Flash mode string * @param {string} flashFreq Flash frequency string * @returns {string} modified image string */ _updateImageFlashParams(image, address, flashSize, flashMode, flashFreq) { this.debug("_update_image_flash_params " + flashSize + " " + flashMode + " " + flashFreq); if (image.length < 8) { return image; } if (address != this.chip.BOOTLOADER_FLASH_OFFSET) { return image; } if (flashSize === "keep" && flashMode === "keep" && flashFreq === "keep") { this.info("Not changing the image"); return image; } const magic = parseInt(image[0]); let aFlashMode = parseInt(image[2]); const flashSizeFreq = parseInt(image[3]); if (magic !== this.ESP_IMAGE_MAGIC) { this.info("Warning: Image file at 0x" + address.toString(16) + " doesn't look like an image file, so not changing any flash settings."); return image; } /* XXX: Yet to implement actual image verification */ if (flashMode !== "keep") { const flashModes = { qio: 0, qout: 1, dio: 2, dout: 3 }; aFlashMode = flashModes[flashMode]; } let aFlashFreq = flashSizeFreq & 0x0f; if (flashFreq !== "keep") { const flashFreqs = { "40m": 0, "26m": 1, "20m": 2, "80m": 0xf }; aFlashFreq = flashFreqs[flashFreq]; } let aFlashSize = flashSizeFreq & 0xf0; if (flashSize !== "keep") { aFlashSize = this.parseFlashSizeArg(flashSize); } const flashParams = (aFlashMode << 8) | (aFlashFreq + aFlashSize); this.info("Flash params set to " + flashParams.toString(16)); if (parseInt(image[2]) !== aFlashMode << 8) { image = image.substring(0, 2) + (aFlashMode << 8).toString() + image.substring(2 + 1); } if (parseInt(image[3]) !== aFlashFreq + aFlashSize) { image = image.substring(0, 3) + (aFlashFreq + aFlashSize).toString() + image.substring(3 + 1); } return image; } /** * Write set of file images into given address based on given FlashOptions object. * @param {FlashOptions} options FlashOptions to configure how and what to write into flash. */ async writeFlash(options) { this.debug("EspLoader program"); if (options.flashSize !== "keep") { const flashEnd = this.flashSizeBytes(options.flashSize); for (let i = 0; i < options.fileArray.length; i++) { if (options.fileArray[i].data.length + options.fileArray[i].address > flashEnd) { throw new ESPError(`File ${i + 1} doesn't fit in the available flash`); } } } if (this.IS_STUB === true && options.eraseAll === true) { await this.eraseFlash(); } let image, address; for (let i = 0; i < options.fileArray.length; i++) { this.debug("Data Length " + options.fileArray[i].data.length); image = options.fileArray[i].data; this.debug("Image Length " + image.length); if (image.length === 0) { this.debug("Warning: File is empty"); continue; } image = this.ui8ToBstr(padTo(this.bstrToUi8(image), 4)); address = options.fileArray[i].address; image = this._updateImageFlashParams(image, address, options.flashSize, options.flashMode, options.flashFreq); let calcmd5 = null; if (options.calculateMD5Hash) { calcmd5 = options.calculateMD5Hash(image); this.debug("Image MD5 " + calcmd5); } const uncsize = image.length; let blocks; if (options.compress) { const uncimage = this.bstrToUi8(image); image = this.ui8ToBstr(deflate(uncimage, { level: 9 })); blocks = await this.flashDeflBegin(uncsize, image.length, address); } else { blocks = await this.flashBegin(uncsize, address); } let seq = 0; let bytesSent = 0; const totalBytes = image.length; if (options.reportProgress) options.reportProgress(i, 0, totalBytes); let d = new Date(); const t1 = d.getTime(); let timeout = 5000; // Create a decompressor to keep track of the size of uncompressed data // to be written in each chunk. const inflate = new Inflate({ chunkSize: 1 }); let totalLenUncompressed = 0; inflate.onData = function (chunk) { totalLenUncompressed += chunk.byteLength; }; while (image.length > 0) { this.debug("Write loop " + address + " " + seq + " " + blocks); this.info("Writing at 0x" + (address + totalLenUncompressed).toString(16) + "... (" + Math.floor((100 * (seq + 1)) / blocks) + "%)"); const block = this.bstrToUi8(image.slice(0, this.FLASH_WRITE_SIZE)); if (options.compress) { const lenUncompressedPrevious = totalLenUncompressed; inflate.push(block, false); const blockUncompressed = totalLenUncompressed - lenUncompressedPrevious; let blockTimeout = 3000; if (this.timeoutPerMb(this.ERASE_WRITE_TIMEOUT_PER_MB, blockUncompressed) > 3000) { blockTimeout = this.timeoutPerMb(this.ERASE_WRITE_TIMEOUT_PER_MB, blockUncompressed); } if (this.IS_STUB === false) { // ROM code writes block to flash before ACKing timeout = blockTimeout; } await this.flashDeflBlock(block, seq, timeout); if (this.IS_STUB) { // Stub ACKs when block is received, then writes to flash while receiving the block after it timeout = blockTimeout; } } else { throw new ESPError("Yet to handle Non Compressed writes"); } bytesSent += block.length; image = image.slice(this.FLASH_WRITE_SIZE, image.length); seq++; if (options.reportProgress) options.reportProgress(i, bytesSent, totalBytes); } if (this.IS_STUB) { await this.readReg(this.CHIP_DETECT_MAGIC_REG_ADDR, timeout); } d = new Date(); const t = d.getTime() - t1; if (options.compress) { this.info("Wrote " + uncsize + " bytes (" + bytesSent + " compressed) at 0x" + address.toString(16) + " in " + t / 1000 + " seconds."); } if (calcmd5) { const res = await this.flashMd5sum(address, uncsize); if (new String(res).valueOf() != new String(calcmd5).valueOf()) { this.info("File md5: " + calcmd5); this.info("Flash md5: " + res); throw new ESPError("MD5 of file does not match data in flash!"); } else { this.info("Hash of data verified."); } } } this.info("Leaving..."); if (this.IS_STUB) { await this.flashBegin(0, 0); if (options.compress) { await this.flashDeflFinish(); } else { await this.flashFinish(); } } } /** * Read SPI flash manufacturer and device id. */ async flashId() { this.debug("flash_id"); const flashid = await this.readFlashId(); this.info("Manufacturer: " + (flashid & 0xff).toString(16)); const flidLowbyte = (flashid >> 16) & 0xff; this.info("Device: " + ((flashid >> 8) & 0xff).toString(16) + flidLowbyte.toString(16)); this.info("Detected flash size: " + this.DETECTED_FLASH_SIZES[flidLowbyte]); } async getFlashSize() { this.debug("flash_id"); const flashid = await this.readFlashId(); const flidLowbyte = (flashid >> 16) & 0xff; return this.DETECTED_FLASH_SIZES_NUM[flidLowbyte]; } /** * Soft reset the device chip. Soft reset with run user code is the closest. * @param {boolean} stayInBootloader Flag to indicate if to stay in bootloader */ async softReset(stayInBootloader) { if (!this.IS_STUB) { if (stayInBootloader) { return; // ROM bootloader is already in bootloader! } // "run user code" is as close to a soft reset as we can do await this.flashBegin(0, 0); await this.flashFinish(false); } else if (this.chip.CHIP_NAME != "ESP8266") { throw new ESPError("Soft resetting is currently only supported on ESP8266"); } else { if (stayInBootloader) { // soft resetting from the stub loader // will re-load the ROM bootloader await this.flashBegin(0, 0); await this.flashFinish(true); } else { // running user code from stub loader requires some hacks // in the stub loader await this.command(this.ESP_RUN_USER_CODE, undefined, undefined, false); } } } /** * Execute this function to execute after operation reset functions. * @param {After} mode After operation mode. Default is 'hard_reset'. * @param { boolean } usingUsbOtg For 'hard_reset' to specify if using USB-OTG */ async after(mode = "hard_reset", usingUsbOtg) { switch (mode) { case "hard_reset": if (this.resetConstructors.hardReset) { this.info("Hard resetting via RTS pin..."); const hardReset = this.resetConstructors.hardReset(this.transport, usingUsbOtg); await hardReset.reset(); } break; case "soft_reset": this.info("Soft resetting..."); await this.softReset(false); break; case "no_reset_stub": this.info("Staying in flasher stub."); break; default: this.info("Staying in bootloader."); if (this.IS_STUB) { this.softReset(true); } break; } } }