mirror of
https://github.com/alexandrebobkov/ESP-Nodes.git
synced 2025-08-08 12:11:00 +00:00
1360 lines
56 KiB
JavaScript
1360 lines
56 KiB
JavaScript
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<number>} 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<number | Uint8Array>} 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;
|
|
}
|
|
}
|
|
}
|