Skip to content

Commit 1feee95

Browse files
committed
flash: use esptool write-flash, always show port menu, add flash command
- Use esptool v5's `write-flash` (no more deprecation warning); pin esptool>=5. - Interactive runs always list the ports and wait for a choice (Enter = first) instead of silently auto-picking a lone port and flashing. - New `flash` console script so the one-off shortens to `uvx --from "python-esp-bridge[flash]" flash` (`espbridge flash` still works). - Bump to 0.12.1; update README/FIRMWARE docs.
1 parent 4d137bf commit 1feee95

8 files changed

Lines changed: 78 additions & 21 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ are implemented in Python where they are easy to read, test and extend.
3232
one, it writes a *Huge APP* image with esptool):
3333

3434
```sh
35-
uvx --from "python-esp-bridge[flash]" espbridge flash # or: espbridge flash
35+
uvx --from "python-esp-bridge[flash]" flash # zero-install via uv
3636
```
3737

3838
Prefer building it yourself? Install the **`python esp bridge`** library

docs/FIRMWARE.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -94,12 +94,12 @@ toolchain. It lists the serial ports, lets you pick one, and writes the image
9494
over USB with esptool:
9595

9696
```sh
97-
uvx --from "python-esp-bridge[flash]" espbridge flash # zero-install via uv
98-
# or, once installed: pip install "python-esp-bridge[flash]"
99-
espbridge flash # list ports and choose
100-
espbridge flash -p COM5 # flash a specific port
101-
espbridge flash --erase # wipe the whole flash first (clears NVS / name)
102-
espbridge flash --firmware my.bin # flash your own image instead
97+
uvx --from "python-esp-bridge[flash]" flash # zero-install via uv
98+
# or install once: uv tool install "python-esp-bridge[flash]" / pip install "python-esp-bridge[flash]"
99+
flash # list ports and choose (also: espbridge flash)
100+
flash -p COM5 # flash a specific port
101+
flash --erase # wipe the whole flash first (clears NVS / name)
102+
flash --firmware my.bin # flash your own image instead
103103
```
104104

105105
The bundled image is a **classic-ESP32 Huge APP** build (no OTA — for cable-free

library.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name=python esp bridge
2-
version=0.12.0
2+
version=0.12.1
33
author=Hamza Yesilmen <resmiyslmn@gmail.com>
44
maintainer=Hamza Yesilmen <resmiyslmn@gmail.com>
55
sentence=Flash-once ESP32 firmware that exposes every peripheral to Python over USB serial or Bluetooth.

python/espbridge/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def __getattr__(name):
5656
return AsyncBridge
5757
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
5858

59-
__version__ = "0.12.0"
59+
__version__ = "0.12.1"
6060

6161
__all__ = [
6262
"Bridge",

python/espbridge/flash.py

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"""
2222
from __future__ import annotations
2323

24+
import sys
2425
from importlib import resources
2526
from pathlib import Path
2627

@@ -70,20 +71,23 @@ def _all_serial_ports() -> list[PortInfo]:
7071

7172

7273
def _choose_port(ports: list[PortInfo]) -> str:
73-
"""Print a numbered menu and read the operator's choice."""
74+
"""Print a numbered menu and read the operator's choice (Enter picks #1)."""
7475
print("Available serial ports:")
7576
for i, p in enumerate(ports, 1):
7677
chip = f" [{p.usb_chip}]" if p.usb_chip else ""
7778
desc = f" {p.description}" if p.description else ""
7879
print(f" {i}) {p.device}{chip}{desc}")
80+
prompt = f"Select a port [1-{len(ports)}, Enter = 1, q = cancel]: "
7981
while True:
8082
try:
81-
raw = input(f"Select a port [1-{len(ports)}] (q to cancel): ").strip()
83+
raw = input(prompt).strip()
8284
except EOFError:
8385
raise BridgeError("no port selected (no interactive console; "
8486
"pass -p PORT explicitly)")
8587
if raw.lower() in ("q", "quit"):
8688
raise BridgeError("cancelled")
89+
if raw == "": # bare Enter → the first (most likely) port
90+
return ports[0].device
8791
if raw.isdigit() and 1 <= int(raw) <= len(ports):
8892
return ports[int(raw) - 1].device
8993
for p in ports: # also accept typing the port name directly
@@ -93,16 +97,22 @@ def _choose_port(ports: list[PortInfo]) -> str:
9397

9498

9599
def select_port(port: str | None = None) -> str:
96-
"""Resolve a port: honour an explicit one, else auto-pick or prompt."""
100+
"""Resolve a port: honour an explicit one, else show the menu (or, with no
101+
interactive console, auto-pick a lone port)."""
97102
if port:
98103
return port
99104
ports = find_ports() or _all_serial_ports()
100105
if not ports:
101106
raise BridgeError("no serial ports found — plug the ESP32 in over USB, "
102107
"or pass -p PORT")
103-
if len(ports) == 1:
104-
log.info(f"using the only serial port found: {ports[0].device}")
105-
return ports[0].device
108+
interactive = bool(getattr(sys.stdin, "isatty", lambda: False)())
109+
if not interactive: # piped/CI: can't prompt, so a lone port or bust
110+
if len(ports) == 1:
111+
log.info(f"using the only serial port found: {ports[0].device}")
112+
return ports[0].device
113+
names = ", ".join(p.device for p in ports)
114+
raise BridgeError(f"several serial ports ({names}); pass -p PORT "
115+
"(no interactive console to choose from)")
106116
return _choose_port(ports)
107117

108118

@@ -131,8 +141,16 @@ def flash_firmware(port: str | None = None, *,
131141

132142
port = select_port(port)
133143

144+
# esptool v5 renamed `write_flash` → `write-flash` (the underscore form
145+
# still works but prints a deprecation warning); pick by version so we stay
146+
# quiet on v5 and keep working on an older v4.
147+
try:
148+
major = int(esptool.__version__.split(".")[0])
149+
except (AttributeError, ValueError):
150+
major = 5
151+
write_cmd = "write-flash" if major >= 5 else "write_flash"
134152
argv = ["--chip", chip, "--port", port, "--baud", str(baud),
135-
"--before", "default-reset", "--after", "hard-reset", "write_flash"]
153+
"--before", "default-reset", "--after", "hard-reset", write_cmd]
136154
if erase:
137155
argv.append("--erase-all")
138156
argv += ["--flash-size", "keep", FIRMWARE_OFFSET, str(fw)]
@@ -150,3 +168,41 @@ def flash_firmware(port: str | None = None, *,
150168
raise BridgeError(f"flash failed: {e}")
151169
log.info("done — the board will reboot into the bridge firmware "
152170
"(connect with `espbridge info`)")
171+
172+
173+
def main(argv: list[str] | None = None) -> int:
174+
"""Entry point for the standalone `flash` command, so the one-off is just
175+
176+
uvx --from "python-esp-bridge[flash]" flash
177+
178+
`espbridge flash` runs the same thing through the main CLI.
179+
"""
180+
import argparse
181+
182+
from . import __version__
183+
from .cli import _force_utf8_output
184+
185+
_force_utf8_output()
186+
ap = argparse.ArgumentParser(
187+
prog="flash",
188+
description="Flash the bundled bridge firmware to an ESP32 over USB.")
189+
ap.add_argument("--version", action="version", version=f"espbridge {__version__}")
190+
ap.add_argument("-p", "--port", help="serial port to flash (default: list ports and choose)")
191+
ap.add_argument("--baud", type=int, default=DEFAULT_BAUD,
192+
help=f"flash baud rate (default: {DEFAULT_BAUD})")
193+
ap.add_argument("--erase", action="store_true",
194+
help="erase the whole flash before writing (clears NVS / stored name)")
195+
ap.add_argument("--firmware", help="flash this .bin instead of the bundled image")
196+
ap.add_argument("--chip", default=FIRMWARE_CHIP, help=f"target chip (default: {FIRMWARE_CHIP})")
197+
args = ap.parse_args(argv)
198+
try:
199+
flash_firmware(args.port, baud=args.baud, erase=args.erase,
200+
firmware=args.firmware, chip=args.chip)
201+
except BridgeError as e:
202+
log.error(str(e))
203+
return 1
204+
return 0
205+
206+
207+
if __name__ == "__main__":
208+
raise SystemExit(main())

python/pyproject.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "python-esp-bridge"
3-
version = "0.12.0"
3+
version = "0.12.1"
44
description = "Control every ESP32 peripheral from Python over USB serial or Bluetooth — GPIO, ADC, DAC, PWM, touch, I2C, SPI, UART, Wi-Fi sockets, BLE, ESP-NOW, RMT, 1-Wire, CAN, I2S, files, NVS, OTA"
55
readme = "README.md"
66
requires-python = ">=3.10"
@@ -15,7 +15,7 @@ dependencies = [
1515
oled = ["pillow>=10"] # espbridge.oled: PIL drawing for SSD1306/SH1106 displays
1616
ble = ["bleak>=0.22"] # kept for back-compat; bleak now ships by default (BLE is the default transport)
1717
mcp = ["fastmcp>=2.3"] # espbridge.mcp: MCP server exposing the bridge as tools
18-
flash = ["esptool>=4.8"] # espbridge flash: write the bundled firmware over USB serial
18+
flash = ["esptool>=5"] # espbridge flash / `flash`: write the bundled firmware over USB serial
1919
all = ["python-esp-bridge[oled,ble,mcp,flash]"] # every optional feature at once
2020

2121
[project.urls]
@@ -24,6 +24,7 @@ Homepage = "https://github.com/HamzaYslmn/python-esp-bridge"
2424
[project.scripts]
2525
espbridge = "espbridge.cli:main"
2626
espbridge-mcp = "espbridge.mcp.server:main"
27+
flash = "espbridge.flash:main" # `uvx --from "python-esp-bridge[flash]" flash`
2728

2829
# Third-party device drivers: a package advertises its driver classes in this
2930
# group and they become available as esp.<name>(...) on every bridge, with no

python/uv.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/espbridge/commands.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
// tests/test_contract_sync.py enforces the lockstep.
1212
#define FW_VERSION_MAJOR 0
1313
#define FW_VERSION_MINOR 12
14-
#define FW_VERSION_PATCH 0
14+
#define FW_VERSION_PATCH 1
1515

1616
// Frame (logical, pre-COBS):
1717
// flags u8 | seq u8 | cmd u16 BE | payload .. | crc16 BE

0 commit comments

Comments
 (0)