esptool-js

Javascript implementation of esptool

This repository contains a Javascript implementation of esptool, a serial flasher utility for Espressif chips. esptool-js is based on Web Serial API and works in Google Chrome and Microsoft Edge version 89 or later browsers and Google Chrome on Android version 61 or later via the web-serial-polyfill compatibility layer.

NOTE: Unlike the Python-based esptool, esptool-js doesn't implement generation of binary images out of ELF files, and doesn't include companion tools similar to espefuse.py and espsecure.py.

📚 API Documentation - Complete API reference with detailed class and method documentation.

Installation

CDN

Use https://unpkg.com/esptool-js/bundle.js for a single-file browser load. Dependencies such as pako are inlined, so no import map is required.

Unbundled https://unpkg.com/esptool-js/lib/index.js is for Node and bundlers. A browser loading lib/ as native ESM must map bare specifiers, for example:

<script type="importmap">
{
"imports": {
"pako": "https://unpkg.com/pako@2.1.0/dist/pako.esm.mjs",
"atob-lite": "https://unpkg.com/atob-lite@2.0.0/atob-browser.js"
}
}
</script>

NPM

npm install --save esptool-js

Yarn

yarn add --save esptool-js

Nightly builds for ESPTOOL-JS

How to Use

Basic Setup

First, import the necessary classes and types:

import {
ESPLoader,
Transport,
LoaderOptions,
FlashOptions,
FlashModeValues,
FlashFreqValues,
FlashSizeValues,
IEspLoaderTerminal,
} from "esptool-js";

1. Request Serial Port Access

Request access to a serial port using the Web Serial API:

// Request port access (user will be prompted to select a device)
const port = await navigator.serial.requestPort();

// Optionally, filter by USB vendor/product IDs
const portFilters = [
{ usbVendorId: 0x10c4, usbProductId: 0xea60 } // Example: Silicon Labs CP210x
];
const port = await navigator.serial.requestPort({ filters: portFilters });

2. Create Transport and ESPLoader

Create a Transport instance and configure the ESPLoader:

// Create transport instance
const transport = new Transport(port, true);

// Optional: Create a terminal interface for logging
const terminal: IEspLoaderTerminal = {
clean() {
console.clear();
},
writeLine(data: string) {
console.log(data);
},
write(data: string) {
console.log(data);
},
};

// Configure loader options
const loaderOptions: LoaderOptions = {
transport: transport,
baudrate: 115200, // Communication baud rate
terminal: terminal, // Optional terminal for logging
debugLogging: false, // Optional debug logging
};

// Create ESPLoader instance
const esploader = new ESPLoader(loaderOptions);

3. Connect to Device

Connect to the ESP device and detect the chip:

try {
// Connect and detect chip (this will reset the device)
const chipName = await esploader.main();
console.log(`Connected to: ${chipName}`);
} catch (error) {
console.error("Failed to connect:", error);
}

4. Flash Firmware

Flash firmware images to the device:

// Read your firmware file (e.g., from a File input or fetch)
const firmwareData = new Uint8Array(/* your firmware binary data */);
const firmwareAddress = 0x1000; // Starting address in flash

// Configure flash options
const flashOptions: FlashOptions = {
fileArray: [
{ data: firmwareData, address: firmwareAddress }
],
flashMode: "dio" as FlashModeValues, // Flash mode: "qio", "qout", "dio", "dout"
flashFreq: "40m" as FlashFreqValues, // Flash frequency: "80m", "40m", "26m", "20m", etc.
flashSize: "4MB" as FlashSizeValues, // Flash size: "256KB", "512KB", "1MB", "2MB", "4MB", etc.
eraseAll: false, // Set to true to erase entire flash before writing
compress: true, // Compress data during transfer
reportProgress: (fileIndex, written, total) => {
const percent = (written / total) * 100;
console.log(`Progress: ${percent.toFixed(1)}%`);
},
// Optional: MD5 hash calculation for verification
calculateMD5Hash: (image: Uint8Array) => {
// Implement your MD5 calculation here
return "your-md5-hash";
},
};

// Flash the firmware
await esploader.writeFlash(flashOptions);

// Reset the device after flashing (optional)
await esploader.after("hard_reset");

Flashing an Intel HEX file

A merged .bin is one contiguous image. The bytes between partitions are 0xFF, so writeFlash erases every sector from the start of that image through its end. NVS and factory partitions inside that span are erased with it.

A sparse Intel HEX file leaves those gaps out. idf.py merge-bin --format hex and esptool merge-bin --format hex produce that kind of file. parseIntelHex splits it into one { data, address } entry per contiguous region. The addresses are the offsets stored in the HEX records (extended linear and extended segment address records included). Pass the segments straight to writeFlash.

Keep eraseAll: false so only the sectors covered by those regions are erased. Keep compress: true so each region is programmed at its real length. Sectors with no HEX records stay on the device.

parseIntelHex throws if two regions share a 4 KiB flash sector, because the erase for each region is rounded to that sector. A HEX file built by encoding a padded merged.bin still contains the gap bytes, and flashing it erases the same partitions as the binary.

import { parseIntelHex } from "esptool-js";

const segments = parseIntelHex(await file.text());
await esploader.writeFlash({
fileArray: segments,
eraseAll: false,
compress: true,
flashMode: "dio" as FlashModeValues,
flashFreq: "40m" as FlashFreqValues,
flashSize: "4MB" as FlashSizeValues,
});

5. Read Flash Memory

Read data from flash memory:

const startAddress = 0x1000;
const size = 4096; // Read 4KB

const data = await esploader.readFlash(
startAddress,
size,
(packet, progress, totalSize) => {
// Optional progress callback
console.log(`Read: ${progress}/${totalSize} bytes`);
}
);

console.log("Read data:", data);

6. Erase Flash

Erase flash memory:

// Erase entire flash
await esploader.eraseFlash();

// Or erase a specific region (if supported)
// await esploader.eraseRegion(startAddress, size);

7. Reset Strategies (OPTIONAL and advanced)

Note: This is an advanced feature. For most use cases, the default reset strategies provided by esptool-js will work correctly. You only need to implement custom reset strategies if:

  • You're using custom hardware that requires specific DTR/RTS sequences
  • The default reset methods don't work with your particular board or adapter
  • You need fine-grained control over the reset timing and sequence
  • You're working with non-standard ESP development boards

By default, ESPLoader uses built-in reset strategies that work with most ESP32/ESP8266 boards. However, if you need to implement your own reset sequence, you can provide custom reset constructors.

Understanding Reset Commands

Reset strategies use a sequence of commands to control the DTR (Data Terminal Ready) and RTS (Request To Send) signals on the serial port:

  • D: setDTR - Set DTR signal (0=False, 1=True)
  • R: setRTS - Set RTS signal (0=False, 1=True)
  • W: Wait - Delay in milliseconds (positive integer)

Commands are separated by | (pipe). For example, "D0|R1|W100|D1|R0|W50|D0" represents:

  1. Set DTR to false
  2. Set RTS to true
  3. Wait 100ms
  4. Set DTR to true
  5. Set RTS to false
  6. Wait 50ms
  7. Set DTR to false

Implementing Custom Reset Strategies

import {
ClassicReset,
HardReset,
UsbJtagSerialReset,
CustomReset,
ResetConstructors,
validateCustomResetStringSequence,
} from "esptool-js";

// Option 1: Use built-in reset strategies with custom parameters
const resetConstructors: ResetConstructors = {
classicReset: (transport, resetDelay) => new ClassicReset(transport, resetDelay),
hardReset: (transport, usingUsbOtg) => new HardReset(transport, usingUsbOtg),
usbJTAGSerialReset: (transport) => new UsbJtagSerialReset(transport),
};

// Option 2: Implement a completely custom reset sequence
const customResetSequence = "D0|R1|W100|D1|R0|W50|D0";

// Validate the sequence before using it
if (validateCustomResetStringSequence(customResetSequence)) {
resetConstructors.customReset = (transport, sequenceString) =>
new CustomReset(transport, customResetSequence);
} else {
console.error("Invalid reset sequence");
}

// Apply custom reset strategies to loader options
const loaderOptions: LoaderOptions = {
transport: transport,
baudrate: 115200,
resetConstructors: resetConstructors,
};

const esploader = new ESPLoader(loaderOptions);

// Use specific reset mode when connecting
await esploader.main("default_reset"); // Uses classicReset
// await esploader.main("hard_reset"); // Uses hardReset
// await esploader.main("no_reset"); // Skips reset

Example: Custom Reset for Special Hardware

If your board requires a specific reset sequence:

// Custom sequence for a board that needs longer reset pulse
const customSequence = "D0|R0|W50|D1|R1|W200|D0|R0|W100";

if (validateCustomResetStringSequence(customSequence)) {
const resetConstructors: ResetConstructors = {
customReset: (transport, _) => new CustomReset(transport, customSequence),
};

const esploader = new ESPLoader({
transport,
baudrate: 115200,
resetConstructors,
});

await esploader.main("default_reset");
}

8. Complete Example

Here's a complete example that demonstrates the full workflow:

import {
ESPLoader,
Transport,
LoaderOptions,
FlashOptions,
FlashModeValues,
FlashFreqValues,
FlashSizeValues,
} from "esptool-js";

async function flashFirmware() {
try {
// 1. Request serial port
const port = await navigator.serial.requestPort();

// 2. Create transport and loader
const transport = new Transport(port, true);
const esploader = new ESPLoader({
transport,
baudrate: 115200,
terminal: {
clean: () => console.clear(),
writeLine: (data) => console.log(data),
write: (data) => console.log(data),
},
});

// 3. Connect to device
const chipName = await esploader.main();
console.log(`Connected to: ${chipName}`);

// 4. Load firmware (example: from a file input)
const fileInput = document.getElementById("firmwareFile") as HTMLInputElement;
const file = fileInput.files[0];
const firmwareData = new Uint8Array(await file.arrayBuffer());

// 5. Flash firmware
const flashOptions: FlashOptions = {
fileArray: [{ data: firmwareData, address: 0x1000 }],
flashMode: "dio" as FlashModeValues,
flashFreq: "40m" as FlashFreqValues,
flashSize: "4MB" as FlashSizeValues,
eraseAll: false,
compress: true,
reportProgress: (fileIndex, written, total) => {
console.log(`Progress: ${((written / total) * 100).toFixed(1)}%`);
},
};

await esploader.writeFlash(flashOptions);
console.log("Firmware flashed successfully!");

// 6. Reset device
await esploader.after("hard_reset");

// 7. Disconnect
await transport.disconnect();
} catch (error) {
console.error("Error:", error);
}
}

Terminal Interface

You can use any JavaScript-compatible terminal by implementing the IEspLoaderTerminal interface:

import { IEspLoaderTerminal } from "esptool-js";

const terminal: IEspLoaderTerminal = {
clean() {
// Clear terminal output
},
writeLine(data: string) {
// Write a line with newline
},
write(data: string) {
// Write data without newline
},
};

Check the example project for a complete working implementation.

Live demo

Visit https://espressif.github.io/esptool-js/ to see this tool in action.

Testing it locally

npm install
npm run build
cd examples/typescript
npm install
npm run dev # Run local sever with example code

Then open http://localhost:1234 in a Chrome browser. The npm run build step builds the lib used in the example examples/typescript/index.html. Update this reference as described in the Installation section.

Test from Pull Request artifact

If you are testing the main branch or any Pull Request (PR) artifact you can follow these steps:

  1. Get the esptool-js-<version>.tgz where <version> is the current version and download it.
  2. Add the following line to your project's package.json dependencies
"dependencies": {
"esptool-js": "file:../path/to/esptool-js-<version>.tgz"
}
  1. Use the package like import "esptool-js/lib/index.js" when added in package.json as shown before.

License

The code in this repository is Copyright (c) 2023 Espressif Systems (Shanghai) Co. Ltd. It is licensed under Apache 2.0 license, as described in LICENSE file.