Termux-native .bin flasher for ESP32/ESP8266 boards — no root, no esptool.py subprocess, no pyserial.
Project description
Termux-ESP-Flasher
A Termux-native .bin flasher for ESP32 boards — no root, no
esptool.py subprocess, no pyserial.
Covers two device families, auto-detected from the USB VID:PID:
- Native USB CDC — ESP32-S3 / C3 / S2 (
303A:1001/303A:0002). Talks directly to the chip's USB-Serial-JTAG peripheral; bootloader entry/exit is driven bycdc_reset.py. - UART bridge — classic ESP32 and ESP8266 devkits behind a
CP2102, CH340/CH340G, CH9102, or FTDI FT232 bridge chip. The bridge
is opened over raw USB (same fd-wrapping as the native path — Termux
never gives you a
/dev/ttyUSB*node without root, so this isn't optional) and its DTR/RTS lines are pulsed in the classic GPIO0+EN pattern byuart_reset.py.
Both paths upload the real ESP-IDF RAM stub for faster block writes, and both fall back automatically to the plain ROM bootloader if the stub fails to load for any reason.
Why this exists instead of just running esptool.py
esptool.py talks over pyserial, which expects a /dev/ttyUSB* or
/dev/ttyACM* node. On stock no-root Termux there often isn't one —
Android hands USB access to apps as a raw file descriptor via
termux-usb, not a serial device node. This tool talks straight to the
USB endpoints (and, for UART-bridge boards, straight to the bridge
chip's own vendor registers) instead of assuming a tty exists.
If you're on a desktop/laptop Linux box with a real /dev/ttyUSB*
node, just use real esptool.py — it's more battle-tested. This tool
exists for the no-root-Termux gap esptool's pyserial transport can't
reach.
What's supported
- Chip auto-detection.
--chipis optional onprobe,write, andverify— omit it and the tool syncs with the ROM bootloader, reads the chip's magic-value register, and picks the right chip for you. Pass--chipexplicitly to override (still required forerase-info, which never touches hardware). - Stub loader. Both native-USB and UART-bridge sessions upload the
real ESP-IDF RAM stub after syncing, switching from 1 KiB ROM-only
blocks to 16 KiB stub blocks. Falls back to ROM-only if the stub
upload doesn't succeed for some reason — flashing still works, just
slower. Pass
--no-stubto skip the RAM stub entirely and stay on the plain ROM loader from the start. This is recommended on boards with no auto-reset circuit (e.g. native-USB S2 boards with only a BOOT button): a failed stub handshake jumps the chip out of the ROM bootloader with no way back in except physically re-holding BOOT, so on those boards it's safer to never attempt the jump at all. - Automatic baud renegotiation on UART-bridge boards. Once the stub
is running, the tool steps up through 921600 → 460800 → 230400 baud,
verifying each one actually holds (a cheap
sync()) before trusting it. If a candidate rate doesn't hold, it physically re-enters the ROM bootloader and rebuilds the whole session at 115200 before trying the next slower candidate — a bad guess costs a couple seconds, not the whole flash. Settles on whatever your specific board/cable can actually sustain, or stays at 115200 if nothing higher works. Native USB CDC boards skip this entirely — there's no real UART clock underneath USB CDC, so baud rate doesn't mean anything there. - Live progress. The flash progress bar streams in real time on the no-root Termux fd-bootstrap path — earlier builds buffered progress updates invisibly until the transfer finished, then dumped the whole bar at once, which looked like a stall.
- Multi-file writes in one session.
writeaccepts one or moreOFFSET:FILEpairs and flashes all of them under a single USB permission prompt / bootloader handshake.
Scope / limitations (read before filing an issue)
- No full-chip erase command.
erase-infoexplains why: an erase that gets interrupted by a flaky OTG connection mid-operation has no recovery path. Flash a full image (bootloader + partition table + app) at the correct offsets instead of erasing first.write --erasewill erase exactly the bytes about to be overwritten viaERASE_REGIONif the ROM supports it, falling back silently toflash_begin's own per-block erase otherwise. - Auto-detection is a best-effort heuristic, not a guarantee for
every silicon revision — it reads the chip magic value at a register
address that's held true across ESP32/S2/S3/C3 for years, but an
unrecognized value means "pass
--chipexplicitly," not a guess. - Stub data is vendored for more chips than are currently wired up
(
esp32c2,c5,c6,c61,h2,h4,p4,s31all have stub binaries already sitting instub_flasher_data.py), butCHIP_PARAMS/KNOWN_MAGIC/CHIP_CHOICESonly coveresp32,esp8266,esp32s2,esp32s3,esp32c3today. Wiring up one of the others means adding its real chip-magic value and SPI-attach params — open an issue (or ask) with the specific chip if you need one.
Install
pkg update && pkg install python termux-api libusb
pip install nrflash
pip install nrflash pulls in pyusb automatically. termux-api and
libusb are system packages pip can't install for you — they still need
pkg install, and the Termux:API app from
F-Droid (not the Play Store) must be installed
separately for the no-root USB-permission flow to work at all.
Installing from source instead
git clone https://github.com/7wp81x/Termux-ESP-Flasher
cd Termux-ESP-Flasher
pip install .
Updating
pip install --upgrade nrflash
If pip stubbornly reinstalls the same old version (common right after a fresh release, since pip caches wheels/metadata), force it to skip the cache:
pip install --upgrade --no-cache-dir nrflash
Check what you've got installed with nrflash --version. If you installed
from source instead of pip, git pull then pip install . again to pick
up the update.
--chip is optional everywhere except erase-info - omit it to auto-detect
nrflash probe nrflash write --offset 0x0 firmware.bin
Still works if you want to force a specific chip
nrflash write --chip esp32c3 --offset 0x0 firmware.bin
Flash + MD5-verify against the device afterward
nrflash write --chip esp32s3 --offset 0x0 firmware.bin --verify
Flash a sub-image at a partition offset (e.g. app partition only)
nrflash write --offset 0x10000 app.bin
Flash bootloader + partition table + app in one session
nrflash write 0x0:bootloader.bin 0x8000:partitions.bin 0x10000:app.bin
Stand-alone MD5 check against a file already on disk
nrflash verify --chip esp32c3 --offset 0x0 firmware.bin
Explicitly erase the write range first via ERASE_REGION
nrflash write --offset 0x0 firmware.bin --erase
Stay in the bootloader after flashing instead of rebooting
nrflash write --offset 0x0 firmware.bin --no-reboot
Skip the RAM stub and use the plain ROM loader (slower, but safer on
boards with no auto-reset circuit - see --no-stub note below)
nrflash write --offset 0x0 firmware.bin --no-stub
First run on no-root Termux pops the same USB permission dialog every
time — tap **OK**. The permission persists until you unplug.
## What actually flashes — single binary vs. esptool's three images
Real `esptool.py write_flash` usually takes **three** files at three
offsets (bootloader, partition table, app), e.g.:
0x0 bootloader.bin 0x8000 partition-table.bin 0x10000 app.bin
`nrflash write` can take all three in one call (see the multi-file
example above), or one file per invocation if you prefer. If your build
only produces a single merged/combined `.bin` (PlatformIO can do this —
check for a `firmware.factory.bin` or similar after `pio run`), one call
at `0x0` is all you need.
## How bootloader entry works
- **UART-bridge boards** (CP2102/CH340/CH9102/FTDI) have real DTR/RTS
lines wired into EN/BOOT. `usb_device.py`'s `init_uart_bridge()`
configures the bridge's line coding and baud divisor via its own
vendor control transfers, then `uart_reset.py` pulses DTR/RTS in the
classic auto-reset pattern.
- **Native USB CDC boards** have no bridge chip — the chip's internal
USB-Serial-JTAG peripheral watches the CDC class's
`SET_CONTROL_LINE_STATE` DTR/RTS bits in hardware and maps specific
transitions to an internal EN/BOOT reset, the software-only
equivalent of the same trick. That sequence lives in `cdc_reset.py`,
separate from `usb_device.py`'s bridge-chip logic since the two have
nothing in common at the wire level.
## Files
src/nrflash/
init.py package version
main.py enables python3 -m nrflash.cli
cli.py CLI: argv parsing, fd bootstrap, flash/verify/probe commands
rom_loader.py SLIP framing + ROM bootloader command/response protocol,
chip auto-detection, baud-change command
cdc_reset.py native-USB-CDC bootloader entry/exit (DTR/RTS bit tricks)
uart_reset.py UART-bridge bootloader entry/exit (DTR/RTS pulse pattern)
stub_flasher_data.py vendored ESP-IDF RAM stub binaries (Apache-2.0/MIT, Espressif)
USB backend detection, fd wrapping, and UART-bridge register access come
from the [`espbridge`](https://github.com/7wp81x/ESP-Bridge) package
(installed automatically as a dependency) rather than a vendored
`usb_device.py` — this keeps the tricky no-root Termux fd-bootstrap logic
in one place, shared with other tools instead of copy-pasted per repo.
`pip install nrflash` puts a `nrflash` console script on `PATH` — no
`chmod +x`, no flat-directory requirement. Log/state files live under
`~/.nrflash` (override with `NRFLASH_DATA_DIR`) rather than next to the
installed package, since site-packages isn't guaranteed writable.
## Troubleshooting
**`probe` reports no response from bootloader**
Some clone boards need the physical BOOT button held while plugging in,
or the CH340/CP2102/FTDI wired to EN+GPIO0 for auto-reset to work at
all. Try unplugging/replugging the OTG cable once before assuming it's a
protocol problem.
**Flashing is slow / progress bar looks stuck**
On UART-bridge boards, check the log for a `Baud rate raised to ... and
verified` line — if it's missing or stuck at 115200, your specific
board/cable couldn't hold anything higher and the tool already fell back
automatically. If the progress bar itself looks frozen and then jumps to
100% all at once, make sure you're on a build with the live-progress fix
(see "What's supported" above) rather than an older buffered-tail-thread
build.
**`Verify MISMATCH` after a successful-looking write**
Don't trust a partial flash. Re-run `write --verify` rather than
assuming the device is fine — a flaky OTG link or an unstable baud rate
can drop bytes mid-transfer in a way that still returns "success" status
on a given block.
## Author
[7wp81x](https://github.com/7wp81x)
## Legal
For use on hardware you own. Flashing arbitrary firmware to a device you
don't own or have written permission to modify can violate warranty
terms or, depending on the device and jurisdiction, the law.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file nrflash-1.2.0.tar.gz.
File metadata
- Download URL: nrflash-1.2.0.tar.gz
- Upload date:
- Size: 114.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
de19b5e3192183bac1eff82801d111a247f22dd9f087c7861f41d8c993114adb
|
|
| MD5 |
97f307e6f9f674bbf3915fed48cf225c
|
|
| BLAKE2b-256 |
418065d96e7ecaa6be7566033fc305334d4b3d30664a2b83b4e78ac7b1ed6751
|
Provenance
The following attestation bundles were made for nrflash-1.2.0.tar.gz:
Publisher:
python-publish.yml on 7wp81x/Termux-ESP-Flasher
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
nrflash-1.2.0.tar.gz -
Subject digest:
de19b5e3192183bac1eff82801d111a247f22dd9f087c7861f41d8c993114adb - Sigstore transparency entry: 2187064178
- Sigstore integration time:
-
Permalink:
7wp81x/Termux-ESP-Flasher@8265e74b193098290079d636be4304dd4960d095 -
Branch / Tag:
refs/tags/v1.2.0 - Owner: https://github.com/7wp81x
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@8265e74b193098290079d636be4304dd4960d095 -
Trigger Event:
release
-
Statement type:
File details
Details for the file nrflash-1.2.0-py3-none-any.whl.
File metadata
- Download URL: nrflash-1.2.0-py3-none-any.whl
- Upload date:
- Size: 111.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f7cdc8662fd71dbefc877025842a2c7d41aebb655714819dfa37cf631707f98d
|
|
| MD5 |
6a7e1f5c9c03491da1520ca8058277d8
|
|
| BLAKE2b-256 |
a57993e171cc879706be51522626ec30f3f3155fab124dfdfe207efb2db7582b
|
Provenance
The following attestation bundles were made for nrflash-1.2.0-py3-none-any.whl:
Publisher:
python-publish.yml on 7wp81x/Termux-ESP-Flasher
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
nrflash-1.2.0-py3-none-any.whl -
Subject digest:
f7cdc8662fd71dbefc877025842a2c7d41aebb655714819dfa37cf631707f98d - Sigstore transparency entry: 2187064183
- Sigstore integration time:
-
Permalink:
7wp81x/Termux-ESP-Flasher@8265e74b193098290079d636be4304dd4960d095 -
Branch / Tag:
refs/tags/v1.2.0 - Owner: https://github.com/7wp81x
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@8265e74b193098290079d636be4304dd4960d095 -
Trigger Event:
release
-
Statement type: