Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
188 changes: 167 additions & 21 deletions src/esploader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,20 @@ import { FlashOptions } from "./types/flashOptions.js";
import { After, Before } from "./types/resetModes.js";
import { FlashFreqValues, FlashModeValues, FlashSizeValues } from "./types/arguments.js";
import { loadFirmwareImage } from "./image/index.js";
import { ROM_LIST } from "./targets/index.js";
import { parseSecurityFlags, SecurityInfo } from "./types/securityInfo.js";

/**
* Flash read callback function type
* @param {Uint8Array} packet - Packet data
* @param {number} progress - Progress number
* @param {number} totalSize - Total size number
* Callback function type for handling packets received during flash memory read operations.
* @callback FlashReadCallback
* @param {Uint8Array} packet - The data packet received from the flash memory.
* @param {number} progress - The current progress of the read operation in bytes.
* @param {number} totalSize - The total size of the data to be read in bytes.
*/
export type FlashReadCallback = ((packet: Uint8Array, progress: number, totalSize: number) => void) | null;

export { SecurityInfo, SECURITY_INFO_FLAG_MAP, ParsedSecurityFlags } from "./types/securityInfo.js";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This re-export seems to be redundant, should be removed from here and incorporated in the index barrel file

// esploader.ts — delete line 26 entirely

// index.ts
export { ESPLoader, FlashReadCallback } from "./esploader.js";
export { SecurityInfo, SECURITY_INFO_FLAG_MAP, ParsedSecurityFlags } from "./types/securityInfo.js";
// SecurityInfo and ParsedSecurityFlags are pure types, so export type { ... } would be more precise to have


/**
* Return the chip ROM based on the given magic number
* @param {number} magic - magic hex number to select ROM.
Expand Down Expand Up @@ -106,6 +111,8 @@ export class ESPLoader {
ESP_FLASH_DEFL_END = 0x12;
ESP_SPI_FLASH_MD5 = 0x13;

ESP_GET_SECURITY_INFO = 0x14;

// Only Stub supported commands
ESP_ERASE_FLASH = 0xd0;
ESP_ERASE_REGION = 0xd1;
Expand Down Expand Up @@ -164,6 +171,8 @@ export class ESPLoader {
chip!: ROM;
IS_STUB: boolean;
FLASH_WRITE_SIZE: number;
secureDownloadMode = false;
private securityInfoCache: SecurityInfo | null = null;

public transport: Transport;
private baudrate: number;
Expand Down Expand Up @@ -637,30 +646,160 @@ export class ESPLoader {
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);
this.info("Detecting chip type... ");
await this.identifyChip(mode, attempts);
}
}

/**
* Identify the connected chip using GET_SECURITY_INFO chip-id, then magic-register fallback.
* @param {Before} mode Reset mode used if a reconnect is required
* @param {number} attempts Connection attempts used if a reconnect is required
*/
private async identifyChip(mode: Before, attempts: number) {

@RushikeshPatange RushikeshPatange Aug 26, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Few thoughts on identifyChip method breakdown for better modularity, readability and reusability

  1. identifyChip() is doing too many things. It currently handles chip-ID detection, magic-value fallback, the S2-in-SDM case, reconnect/retry, and updating loader state. It may be cleaner to split these into small helpers such as:

    • romFromChipId(chipId)
    • readSecureDownloadMode()
    • identifyChipByMagic()
    • applyDetectedChip(chip)
  2. connect() and identifyChip() are somewhat coupled. connect() calls identifyChip(), which can call connect() again for retry. This is currently safe because of detecting=false, but moving the retry logic to the caller would make the flow easier to understand.

  3. There is some duplicated SDM/security-info logic. A common readSecureDownloadMode() helper could avoid duplication and also provide a single place to handle security-info cache invalidation after reconnect.

  4. The S2-SDM path creates another ESP32S2ROM instance. Since CHIP_DEFS.esp32s2 already exists in the target registry, it would be better to reuse that instance instead of creating a new one. This keeps all detection paths consistent.

  5. The biggest concern for me is using error messages for control flow. For example, checking error.message === "unsupported command error" or startsWith("Unexpected chip ID value"). A small change to the error message could break the detection flow. It would be more robust to introduce typed errors such as UnsupportedCommandError and UnexpectedChipIdError and check them using instanceof, similar to Python.

  6. The catch block currently treats any failure as "GET_SECURITY_INFO not supported". An actual timeout or connection failure could therefore incorrectly fall back to magic-value detection. Typed errors would make it possible to fall back only for the expected unsupported-command case and propagate unexpected errors.

  7. Returning the detected ROM instead of modifying loader state inside identifyChip() could make the flow clearer. Something like identifyChip(): Promise<ROM> would make it easier to see what the method actually produces and could also make the logic easier to reuse later.

Overall, I think typed errors (5) would be the most important change to consider in this MR because it affects correctness. The remaining points are mostly structural improvements and can reasonably be deferred if we want to keep this MR focused on parity.

let chip: ROM | null = null;
const errMsg = "Failed to autodetect chip type.";

try {
const chipId = await this.getChipId();
for (const cls of ROM_LIST) {
// ESP8266/ESP32: command unsupported; ESP32-S2: no chip-id in the payload
if (cls.USES_MAGIC_VALUE) {
continue;
}
if (chipId === cls.IMAGE_CHIP_ID) {
chip = cls;
const securityInfo = await this.getSecurityInfo();
this.secureDownloadMode = securityInfo.parsedFlags.SECURE_DOWNLOAD_ENABLE;
break;
}
}
if (chip === null) {
throw new ESPError(
`Unexpected CHIP magic value 0x${chipMagicValue.toString(16)}. Failed to autodetect chip type.`,
);
} else {
this.chip = chip;
throw new ESPError(`Unexpected chip ID value ${chipId}. Failed to autodetect chip type.`);
}
} catch (error) {
if (error instanceof ESPError && error.message.startsWith("Unexpected chip ID value")) {
throw error;
}
this.debug("GET_SECURITY_INFO not supported, falling back to magic value");
}

if (chip === null) {
try {
chip = await this.chipFromMagicValue();
} catch (error) {
if (error instanceof ESPError && error.message === "unsupported command error") {
// ESP32-S2 supports GET_SECURITY_INFO but not magic-register reads in SDM
const { ESP32S2ROM } = await import("./targets/esp32s2.js");
chip = new ESP32S2ROM();
const securityInfo = await this.getSecurityInfo();
this.secureDownloadMode = securityInfo.parsedFlags.SECURE_DOWNLOAD_ENABLE;
} else if (error instanceof ESPError && error.message.startsWith("Unexpected CHIP magic value")) {
throw error;
} else {
this.info(" Autodetection failed, trying again...");
await this.transport.disconnect();
await this.connect(mode, attempts, false);
this.info("Detecting chip type... ");
chip = await this.chipFromMagicValue();
}
}
}

if (chip === null) {
throw new ESPError(errMsg);
}
this.chip = chip;
if (chip.SPI_ADDR_REG_MSB !== undefined) {
this.SPI_ADDR_REG_MSB = chip.SPI_ADDR_REG_MSB;
}
}

private async chipFromMagicValue(): Promise<ROM> {
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 (chip === null) {
throw new ESPError(
`Unexpected CHIP magic value 0x${chipMagicValue.toString(16)}. Failed to autodetect chip type.`,
);
}
return chip;
}

/**
* Read GET_SECURITY_INFO (0x14): flags, flash crypt count, key purposes, chip id, API version.
* Tries the 20-byte layout first (ESP32-S3 and later), then 12 bytes (ESP32-S2).
* @param {boolean} cache Return a previously parsed result when available
* @returns {Promise<SecurityInfo>} Parsed security information
*/
async getSecurityInfo(cache = true): Promise<SecurityInfo> {
if (cache && this.securityInfoCache !== null) {
return this.securityInfoCache;
}

let res: Uint8Array;
let esp32s2 = false;
try {
res = (await this.checkCommand(
"get security info",
this.ESP_GET_SECURITY_INFO,
new Uint8Array(0),
0,
20,
)) as Uint8Array;
} catch {
res = (await this.checkCommand(
"get security info",
this.ESP_GET_SECURITY_INFO,
new Uint8Array(0),
0,
12,
)) as Uint8Array;
esp32s2 = true;
}

const flags = this._byteArrayToInt(res[0], res[1], res[2], res[3]) >>> 0;
const securityInfo: SecurityInfo = {
flags,
flashCryptCnt: res[4],
keyPurposes: Array.from(res.slice(5, 12)),
chipId: esp32s2 ? null : this._byteArrayToInt(res[12], res[13], res[14], res[15]) >>> 0,
apiVersion: esp32s2 ? null : this._byteArrayToInt(res[16], res[17], res[18], res[19]) >>> 0,
parsedFlags: parseSecurityFlags(flags),
};

this.securityInfoCache = securityInfo;
return securityInfo;
}

/**
* Get the CHIP ID from ESP_GET_SECURITY_INFO.
* @returns {number} Chip ID number
*/
async getChipId(): Promise<number> {
const chipId = (await this.getSecurityInfo()).chipId;
if (chipId === null) {
throw new ESPError(
"Security info command does not contain chip ID. " +
"This is expected for ESP32-S2 which doesn't support chip ID in security info.",
);
}
this.debug("get_chip_id " + chipId.toString(16));
return chipId;
}

/**
* Connect and detect the existing chip.
* @param {string} mode Reset mode to use for connection.
* @param {number} attempts - Number of connection attempts
*/
async detectChip(mode: Before = "default_reset") {
await this.connect(mode);
this.info("Detecting chip type... ", false);
async detectChip(mode: Before = "default_reset", attempts = 7) {
await this.connect(mode, attempts, true);
if (this.chip != null) {
this.info(this.chip.CHIP_NAME);
} else {
this.info("unknown!");
this.info("unknown chip! detectchip has failed.");
}
}

Expand Down Expand Up @@ -1221,11 +1360,14 @@ export class ESPLoader {
}

/**
* Read flash memory from the chip.
* @param {number} addr Address number
* @param {number} size Package size
* @param {FlashReadCallback} onPacketReceived Callback function to call when packet is received
* @returns {Uint8Array} Flash read data
* Read data from flash memory of the chip.
* This function reads a specified amount of data from the flash memory starting at a given address.
* It sends a read command to the chip and processes the response packets until the requested size is read.
* @param {number} addr - The starting address in flash memory to read from.
* @param {number} size - The number of bytes to read from flash memory.
* @param {FlashReadCallback} onPacketReceived - Optional callback function to handle each received packet.
* @returns {Promise<Uint8Array>} A promise that resolves to the data read from flash memory as a Uint8Array.
* @throws {ESPError} If the read operation fails or an unexpected response is received.
*/
async readFlash(addr: number, size: number, onPacketReceived: FlashReadCallback = null) {
let pkt = this._appendArray(this._intToByteArray(addr), this._intToByteArray(size));
Expand Down Expand Up @@ -1312,6 +1454,10 @@ export class ESPLoader {
* Change the chip baudrate.
*/
async changeBaud() {
if (this.secureDownloadMode) {
this.info("Baud rate change is not supported in secure download mode. Keeping 115200 baud.");
return;
}
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));
Expand Down
4 changes: 2 additions & 2 deletions src/image/esp32.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,8 +301,8 @@
[this.hdDrv, this.wpDrv] = this.splitByte(hdConfig);

this.chipId = view.getUint8(4);
if (this.chipId !== this.ROM_LOADER.IMAGE_CHIP_ID) {
if (this.ROM_LOADER.IMAGE_CHIP_ID !== undefined && this.chipId !== this.ROM_LOADER.IMAGE_CHIP_ID) {
console.warn(

Check warning on line 305 in src/image/esp32.ts

View workflow job for this annotation

GitHub Actions / ci

Unexpected console statement
`Unexpected chip id in image. Expected ${this.ROM_LOADER.IMAGE_CHIP_ID} but value was ${this.chipId}. ` +
"Is this image for a different chip model?",
);
Expand Down Expand Up @@ -330,7 +330,7 @@
view.setUint8(2, this.joinByte(this.dDrv, this.csDrv));
view.setUint8(3, this.joinByte(this.hdDrv, this.wpDrv));

view.setUint8(4, this.ROM_LOADER.IMAGE_CHIP_ID);
view.setUint8(4, this.ROM_LOADER.IMAGE_CHIP_ID ?? 0);
view.setUint8(5, this.minRev);
view.setUint16(6, this.minRevFull, true);
view.setUint16(8, this.maxRevFull, true);
Expand Down
16 changes: 16 additions & 0 deletions src/image/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,14 @@ import {
ESP32C5FirmwareImage,
ESP32C61FirmwareImage,
ESP32C6FirmwareImage,
ESP32E22FirmwareImage,
ESP32H2FirmwareImage,
ESP32H21FirmwareImage,
ESP32H4FirmwareImage,
ESP32P4FirmwareImage,
ESP32S2FirmwareImage,
ESP32S3FirmwareImage,
ESP32S31FirmwareImage,
} from "./others";

/**
Expand Down Expand Up @@ -42,6 +46,9 @@ export async function loadFirmwareImage(rom: ROM, imageData: Uint8Array | string
case "esp32s3":
firmwareImageClass = ESP32S3FirmwareImage;
break;
case "esp32s31":
firmwareImageClass = ESP32S31FirmwareImage;
break;
case "esp32c3":
firmwareImageClass = ESP32C3FirmwareImage;
break;
Expand All @@ -57,9 +64,18 @@ export async function loadFirmwareImage(rom: ROM, imageData: Uint8Array | string
case "esp32c5":
firmwareImageClass = ESP32C5FirmwareImage;
break;
case "esp32e22":
firmwareImageClass = ESP32E22FirmwareImage;
break;
case "esp32h2":
firmwareImageClass = ESP32H2FirmwareImage;
break;
case "esp32h21":
firmwareImageClass = ESP32H21FirmwareImage;
break;
case "esp32h4":
firmwareImageClass = ESP32H4FirmwareImage;
break;
case "esp32p4":
firmwareImageClass = ESP32P4FirmwareImage;
break;
Expand Down
64 changes: 64 additions & 0 deletions src/image/others.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@ import { ESP32C3ROM } from "../targets/esp32c3";
import { ESP32C5ROM } from "../targets/esp32c5";
import { ESP32C6ROM } from "../targets/esp32c6";
import { ESP32C61ROM } from "../targets/esp32c61";
import { ESP32E22ROM } from "../targets/esp32e22";
import { ESP32H2ROM } from "../targets/esp32h2";
import { ESP32H21ROM } from "../targets/esp32h21";
import { ESP32H4ROM } from "../targets/esp32h4";
import { ESP32P4ROM } from "../targets/esp32p4";
import { ESP32S2ROM } from "../targets/esp32s2";
import { ESP32S3ROM } from "../targets/esp32s3";
import { ESP32S31ROM } from "../targets/esp32s31";
import { ESP32FirmwareImage } from "./esp32";

export class ESP32S2FirmwareImage extends ESP32FirmwareImage {
Expand Down Expand Up @@ -138,3 +142,63 @@ export class ESP32H2FirmwareImage extends ESP32C6FirmwareImage {
this.ROM_LOADER = rom as ESP32H2ROM;
}
}

export class ESP32H21FirmwareImage extends ESP32C6FirmwareImage {
ROM_LOADER: ESP32H21ROM;

constructor(
rom: ESP32H21ROM,
loadFile: Uint8Array | string | null = null,
appendDigest = true,
ramOnlyHeader = false,
) {
super(rom, loadFile, appendDigest, ramOnlyHeader);
this.ROM_LOADER = rom as ESP32H21ROM;
}
}

export class ESP32H4FirmwareImage extends ESP32FirmwareImage {
ROM_LOADER: ESP32H4ROM;

constructor(
rom: ESP32H4ROM,
loadFile: Uint8Array | string | null = null,
appendDigest = true,
ramOnlyHeader = false,
) {
super(rom, loadFile, appendDigest, ramOnlyHeader);
this.ROM_LOADER = rom as ESP32H4ROM;
}

MMU_PAGE_SIZE_CONF = [8192, 16384, 32768, 65536];
}

export class ESP32S31FirmwareImage extends ESP32C5FirmwareImage {
ROM_LOADER: ESP32S31ROM;

constructor(
rom: ESP32S31ROM,
loadFile: Uint8Array | string | null = null,
appendDigest = true,
ramOnlyHeader = false,
) {
super(rom, loadFile, appendDigest, ramOnlyHeader);
this.ROM_LOADER = rom as ESP32S31ROM;
}

MMU_PAGE_SIZE_CONF = [32768, 65536, 131072, 262144];
}

export class ESP32E22FirmwareImage extends ESP32FirmwareImage {
ROM_LOADER: ESP32E22ROM;

constructor(
rom: ESP32E22ROM,
loadFile: Uint8Array | string | null = null,
appendDigest = true,
ramOnlyHeader = false,
) {
super(rom, loadFile, appendDigest, ramOnlyHeader);
this.ROM_LOADER = rom as ESP32E22ROM;
}
}
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { ESPLoader, FlashReadCallback } from "./esploader.js";
export { ESPLoader, FlashReadCallback, SecurityInfo } from "./esploader.js";
export { SECURITY_INFO_FLAG_MAP, ParsedSecurityFlags } from "./types/securityInfo.js";
export {
ClassicReset,
CustomReset,
Expand Down
Loading
Loading