From cefb76c1ad581304a3f5dca0e19ea9ffd715e7ff Mon Sep 17 00:00:00 2001 From: MangaValk Date: Tue, 13 Oct 2026 00:44:35 +0200 Subject: [PATCH] Support both Nano bootloaders for flashing over WiFi - Uploader sends 'FLASH ' first; the ESP32 restarts the Nano and bridges at that baud rate, then answers OK - --baud=auto (default) tries 57600 (old ATmegaBOOT) then 115200 (Optiboot) - Optional nano_reset GPIO (NPN to Nano RESET) pulses reset after power on, needed for Optiboot which skips the bootloader after a power cycle - Clear errors for outdated ESP32 firmware and missing wires - README: bootloader table, reset wire, troubleshooting Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 10 ++- README.md | 43 ++++++--- platformio.ini | 4 +- src/esp32/libs.h | 7 +- src/esp32/main.cpp | 24 ++++- src/esp32/nano_flash_bridge.cpp | 41 ++++++++- src/esp32/nano_flash_bridge.h | 15 ++-- tools/nano_net_upload.py | 153 +++++++++++++++++++++----------- 8 files changed, 220 insertions(+), 77 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 2893041..10856bd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,9 +17,13 @@ See README.md for the overview. - If an ESP32 build fails with `No module named 'intelhex'`, run `~/.platformio/penv/Scripts/python.exe -m pip install intelhex`. - `nano_ota` flashes the Nano over WiFi: `tools/nano_net_upload.py` (STK500v1 over TCP) - talks to `NanoFlashBridge` on the ESP32 (port 2323), which power cycles the Nano into - its old bootloader (57600 baud) and passes the bytes through. avrdude 6.3's `net:` port is - broken on Windows (`ser_drain()` fails), which is why the uploader is a Python script. + talks to `NanoFlashBridge` on the ESP32 (port 2323). The uploader first sends + `FLASH \n`. The ESP power cycles the Nano (plus an optional reset pulse on + `nano_reset`), answers `OK\n`, then passes the bytes through at that baud rate. + `--baud=auto` tries 57600 (old ATmegaBOOT, which also starts after a power cycle), then + 115200 (Optiboot, which only starts after a reset-pin reset, so it needs `nano_reset`). + avrdude 6.3's `net:` port is broken on Windows (`ser_drain()` fails), which is why the + uploader is a Python script. The uploader only uses the Python standard library. Test it against a simulated bootloader before changing it. - `src/esp32/libs.h` defines globals in the header, so it can only be included from diff --git a/README.md b/README.md index 01b0910..96bbbe8 100644 --- a/README.md +++ b/README.md @@ -92,10 +92,11 @@ pio run -e nano_ota -t upload What happens: 1. The uploader ([tools/nano_net_upload.py](tools/nano_net_upload.py)) connects to the - ESP32 on TCP port **2323**. -2. The ESP32 pauses the clock and switches the relay off for 1 second, then on again. This - restarts the Nano, and its bootloader starts. -3. The ESP32 switches its link to the bootloader's 57600 baud and passes bytes between + ESP32 on TCP port **2323** and tells it the bootloader's baud rate. +2. The ESP32 pauses the clock and switches the relay off for 1 second, then on again. If + the reset wire is fitted, it also pulses the Nano's RESET pin. This restarts the Nano, + and its bootloader starts. +3. The ESP32 switches its link to the bootloader's baud rate and passes bytes between WiFi and the Nano. 4. The uploader writes the new firmware and reads it back to verify it. 5. When the uploader disconnects, the ESP32 switches the relay off. The clock powers the @@ -108,24 +109,43 @@ The display goes dark for the duration of the upload, which takes a few seconds. - The reply wire must be connected: Nano D1 (TX) → voltage divider → ESP32 GPIO 16. The uploader needs the bootloader's answers. - The ESP32 must be running firmware with the flash bridge (this version or newer). -- The Nano must have the **old bootloader** (the `nanoatmega328` board, 57600 baud), which - also runs at power-on. The newer Optiboot bootloader only runs after a reset-pin reset, - so the relay power cycle would not start it. + +**Bootloaders** + +Flashing the Nano's firmware never changes its bootloader. Both common Nano bootloaders +are supported: + +| Bootloader | Baud | Starts after a power cycle | Reset wire | +|------------|------|----------------------------|------------| +| Old bootloader (ATmegaBOOT), the one this Nano has now | 57600 | Yes | Not needed | +| Optiboot ("new bootloader") | 115200 | No, only after a reset-pin reset | **Required** | + +By default (`--baud=auto` in `upload_flags`) the uploader tries 57600 first and then +115200. Set `--baud=57600` or `--baud=115200` to skip the other attempt. + +If you ever replace the bootloader with Optiboot: + +- Fit the reset wire: ESP32 GPIO → 1 kΩ → base of an NPN transistor (e.g. 2N2222 or + BC547). Connect the collector to Nano RESET and the emitter to GND. Set `nano_reset` in + `src/esp32/libs.h` to that GPIO, for example `4`. The transistor keeps the Nano's 5 V + reset line away from the 3.3 V ESP32 pin. +- For uploads over USB, change `board` in the `nano` environment to `nanoatmega328new`. **Settings** - ESP32 IP address: `upload_port` in the `nano_ota` environment in `platformio.ini`. You can add a port number: `10.1.1.198:2323`. For a one-time different address, use `pio run -e nano_ota -t upload --upload-port 10.1.1.50`. -- Port, baud rate and power-off time: `NANO_FLASH_PORT`, `NANO_BOOTLOADER_BAUD` and - `NANO_POWER_OFF_TIME` in `src/esp32/libs.h`. +- Port, reset pin and timings: `NANO_FLASH_PORT`, `nano_reset`, `NANO_POWER_OFF_TIME` and + `NANO_RESET_DELAY` in `src/esp32/libs.h`. **If it fails** | Message | Cause | |---------|-------| -| `can't connect to ...` | Wrong IP address, or the ESP32 is offline or running older firmware | -| `no answer from the Nano bootloader` | Reply wire missing, or the Nano has Optiboot | +| `can't connect to ...` | Wrong IP address, or the ESP32 is offline | +| `the ESP32 flash bridge didn't accept the request` | The ESP32 runs older firmware without the flash bridge. Flash it first (`esp32` or `esp32_usb`). | +| `no answer from the Nano bootloader` | Reply wire missing, or the Nano has Optiboot and the reset wire is missing | | `bad reply to command ...` / `verify failed` | Connection problem. Try again. A failed upload can leave the Nano without working firmware, but the bootloader itself is never overwritten, so flashing again (over WiFi or USB) always fixes it. | The ESP32 ends the session after 10 seconds without data, so the clock always resumes, @@ -143,6 +163,7 @@ The ESP32 talks to the Nano on **UART2** (`Serial2`, 115200 baud). The ESP32's o |-------|------|-------| | GPIO 17 (TX2) | D0 (RX) | Display commands | | GPIO 16 (RX2) | D1 (TX) | `READY` and `ERR` replies, and bootloader answers when flashing over WiFi. Use a 5 V → 3.3 V voltage divider (e.g. 1 kΩ / 2 kΩ), because the ESP32 is not 5 V tolerant. Without it, the ESP waits a fixed 3 s after power-on, and `nano_ota` doesn't work. | +| GPIO set in `nano_reset` | RESET (through an NPN transistor) | Optional, only needed to flash a Nano with Optiboot over WiFi. See [Bootloaders](#bootloaders). | | GND | GND | Common ground | | GPIO 13 | Relay input | Relay powers the panels and the Nano (`LOW` = on) | diff --git a/platformio.ini b/platformio.ini index 27296fd..35cb21d 100644 --- a/platformio.ini +++ b/platformio.ini @@ -41,11 +41,13 @@ monitor_port = COM10 ; Same Nano firmware, flashed over WiFi through the ESP32 (see README.md). ; Needs the Nano TX -> ESP32 GPIO 16 wire. upload_port = ESP32 IP[:port], default port 2323. +; --baud: bootloader baud rate. auto tries 57600 (old bootloader), then 115200 (Optiboot). [env:nano_ota] extends = env:nano upload_protocol = custom upload_port = 10.1.1.198 -upload_command = "$PYTHONEXE" "$PROJECT_DIR/tools/nano_net_upload.py" "$SOURCE" $UPLOAD_PORT +upload_flags = --baud=auto +upload_command = "$PYTHONEXE" "$PROJECT_DIR/tools/nano_net_upload.py" "$SOURCE" $UPLOAD_PORT $UPLOAD_FLAGS ; --------------------------------------------------------------------------- ; ESP32 - controller, talks to the Nano over serial diff --git a/src/esp32/libs.h b/src/esp32/libs.h index 2b3bddc..a98ad93 100644 --- a/src/esp32/libs.h +++ b/src/esp32/libs.h @@ -50,6 +50,9 @@ const int relay = 13; // Pin of the relay, powers the panels and the Nano (LOW = HardwareSerial &nanoSerial = Serial2; const int nano_rx = 16; // UART2 RX, wired to the Nano TX (through a 5V -> 3.3V divider) const int nano_tx = 17; // UART2 TX, wired to the Nano RX +// Optional: GPIO that resets the Nano through an NPN transistor (HIGH = reset), -1 = not wired. +// Needed to flash a Nano with the Optiboot bootloader, which doesn't start after a power cycle. +const int nano_reset = -1; bool relay_on = false; // Max wait for the Nano's READY after power on (also the wait when Nano TX is not wired to our RX) const uint32_t NANO_BOOT_TIMEOUT = 3000; @@ -57,8 +60,10 @@ const uint32_t NANO_BAUD = 115200; // Flashing the Nano over WiFi (see README.md, pio run -e nano_ota -t upload) const uint16_t NANO_FLASH_PORT = 2323; -const uint32_t NANO_BOOTLOADER_BAUD = 57600; // old Nano bootloader +// The bootloader baud rate is chosen by the uploader (57600 old bootloader, 115200 Optiboot) const uint32_t NANO_POWER_OFF_TIME = 1000; // relay off time to make sure the Nano resets +const uint32_t NANO_RESET_DELAY = 200; // after power on, before the reset pulse +const uint32_t NANO_RESET_PULSE = 50; int hour = 0; int minute = 0; diff --git a/src/esp32/main.cpp b/src/esp32/main.cpp index 4253cbc..be6d943 100644 --- a/src/esp32/main.cpp +++ b/src/esp32/main.cpp @@ -60,18 +60,29 @@ void relayOff() pinMode(nano_tx, INPUT); } -// Flash session started: power cycle the Nano so its bootloader runs, and talk to the +// Flash session started: restart the Nano so its bootloader runs, and talk to the // bootloader at its baud rate. The PC uploader keeps sending sync bytes until it answers. -void startNanoFlash() +// The old bootloader also starts after a power cycle; Optiboot only after a reset pulse. +void startNanoFlash(uint32_t baud) { if (relay_on) relayOff(); delay(NANO_POWER_OFF_TIME); - nanoSerial.begin(NANO_BOOTLOADER_BAUD, SERIAL_8N1, nano_rx, nano_tx); + nanoSerial.begin(baud, SERIAL_8N1, nano_rx, nano_tx); + digitalWrite(relay, LOW); + + if (nano_reset >= 0) + { + delay(NANO_RESET_DELAY); + digitalWrite(nano_reset, HIGH); + delay(NANO_RESET_PULSE); + digitalWrite(nano_reset, LOW); + } + + // drop anything the Nano sent before its bootloader started while (nanoSerial.available()) nanoSerial.read(); - digitalWrite(relay, LOW); } // Flash session ended: switch off, the clock powers the Nano on again with the new firmware @@ -312,6 +323,11 @@ void setup() otaPanicMode = false; pinMode(relay, OUTPUT); + if (nano_reset >= 0) + { + digitalWrite(nano_reset, LOW); + pinMode(nano_reset, OUTPUT); + } relayOn(); display.begin(&nanoSerial); diff --git a/src/esp32/nano_flash_bridge.cpp b/src/esp32/nano_flash_bridge.cpp index bb74389..e17f53d 100644 --- a/src/esp32/nano_flash_bridge.cpp +++ b/src/esp32/nano_flash_bridge.cpp @@ -1,6 +1,6 @@ #include "nano_flash_bridge.h" -NanoFlashBridge::NanoFlashBridge(uint16_t port, HardwareSerial &serial, void (*onStart)(), void (*onEnd)()) +NanoFlashBridge::NanoFlashBridge(uint16_t port, HardwareSerial &serial, void (*onStart)(uint32_t baud), void (*onEnd)()) : _server(port), _serial(serial), _onStart(onStart), _onEnd(onEnd) { } @@ -19,10 +19,19 @@ bool NanoFlashBridge::handle() if (!client) return false; + uint32_t baud = _readHeader(client); + if (!baud) + { + client.print("ERR expected FLASH \n"); + client.stop(); + return false; + } + _client = client; _client.setNoDelay(true); _active = true; - _onStart(); + _onStart(baud); + _client.print("OK\n"); _last_activity = millis(); } @@ -58,6 +67,34 @@ bool NanoFlashBridge::handle() return true; } +// Read the "FLASH " line. Returns the baud rate, or 0 when it is missing or invalid. +uint32_t NanoFlashBridge::_readHeader(WiFiClient &client) +{ + char line[32]; + uint8_t len = 0; + uint32_t start = millis(); + while (millis() - start < HEADER_TIMEOUT && client.connected()) + { + if (!client.available()) + { + delay(1); + continue; + } + char c = client.read(); + if (c == '\n') + { + line[len] = '\0'; + unsigned long baud; + if (sscanf(line, "FLASH %lu", &baud) == 1 && baud >= 9600 && baud <= 1000000) + return baud; + return 0; + } + if (c != '\r' && len < sizeof(line) - 1) + line[len++] = c; + } + return 0; +} + void NanoFlashBridge::_end() { _client.stop(); diff --git a/src/esp32/nano_flash_bridge.h b/src/esp32/nano_flash_bridge.h index c63f4a0..c617f14 100644 --- a/src/esp32/nano_flash_bridge.h +++ b/src/esp32/nano_flash_bridge.h @@ -5,14 +5,16 @@ // WiFi to serial bridge for flashing the Nano from the PC (pio run -e nano_ota -t upload). // -// When a TCP client connects, onStart() is called to restart the Nano into its bootloader. -// After that, bytes are copied between the TCP connection and the Nano's serial port +// The uploader (tools/nano_net_upload.py) connects and first sends one line: +// FLASH \n +// with the baud rate of the Nano's bootloader (57600 old bootloader, 115200 Optiboot). +// onStart(baud) then restarts the Nano into its bootloader, the bridge answers "OK\n", +// and from then on bytes are copied between the TCP connection and the Nano's serial port // until the client disconnects or is idle too long. onEnd() is then called. -// The uploader on the PC side is tools/nano_net_upload.py. class NanoFlashBridge { public: - NanoFlashBridge(uint16_t port, HardwareSerial &serial, void (*onStart)(), void (*onEnd)()); + NanoFlashBridge(uint16_t port, HardwareSerial &serial, void (*onStart)(uint32_t baud), void (*onEnd)()); void begin(); @@ -21,16 +23,19 @@ public: bool handle(); private: + // Max wait for the FLASH line after connecting + static const uint32_t HEADER_TIMEOUT = 2000; // Give up when no data moves in either direction for this long static const uint32_t IDLE_TIMEOUT = 10000; WiFiServer _server; WiFiClient _client; HardwareSerial &_serial; - void (*_onStart)(); + void (*_onStart)(uint32_t baud); void (*_onEnd)(); bool _active = false; uint32_t _last_activity = 0; + uint32_t _readHeader(WiFiClient &client); void _end(); }; diff --git a/tools/nano_net_upload.py b/tools/nano_net_upload.py index c9d204b..d920fef 100644 --- a/tools/nano_net_upload.py +++ b/tools/nano_net_upload.py @@ -1,25 +1,37 @@ #!/usr/bin/env python3 """Flash the Arduino Nano over WiFi, through the ESP32's flash bridge. -Usage: python nano_net_upload.py firmware.hex HOST[:PORT] +Usage: python nano_net_upload.py firmware.hex HOST[:PORT] [--baud auto|57600|115200] -The ESP32 power cycles the Nano when we connect (so its bootloader starts) and then -passes bytes between this TCP connection and the Nano's serial port. This script talks -the STK500v1 protocol of the Arduino bootloader directly. avrdude can't be used here: -its network ports are unreliable on Windows. +--baud is the Nano bootloader's baud rate: + 57600 old Nano bootloader (ATmegaBOOT) + 115200 Optiboot ("new bootloader"); needs the ESP32 reset wire + auto try 57600 first, then 115200 (default) + +The ESP32 restarts the Nano when we connect (so its bootloader starts) and then passes +bytes between this TCP connection and the Nano's serial port. This script talks the +STK500v1 protocol of the Arduino bootloaders directly. avrdude can't be used here: its +network ports are unreliable on Windows. Normally run by PlatformIO: pio run -e nano_ota -t upload """ +import argparse import socket import sys import time DEFAULT_PORT = 2323 PAGE_SIZE = 128 # ATmega328P flash page in bytes -FLASH_SIZE = 30720 # 32 KB minus the 2 KB bootloader SIGNATURE = b"\x1e\x95\x0f" # ATmega328P +# Bootloaders: baud rate -> (name, flash space left for the firmware) +BOOTLOADERS = { + 57600: ("old bootloader", 32768 - 2048), + 115200: ("Optiboot", 32768 - 512), +} +AUTO_ORDER = [57600, 115200] + # STK500v1 STK_OK = 0x10 STK_INSYNC = 0x14 @@ -32,7 +44,8 @@ PROG_PAGE = 0x64 READ_PAGE = 0x74 READ_SIGN = 0x75 -SYNC_TIMEOUT = 10.0 # seconds to wait for the bootloader after connecting +BRIDGE_TIMEOUT = 5.0 # wait for the ESP32's OK (it power cycles the Nano first) +SYNC_TIMEOUT = 4.0 # wait for the bootloader to answer after the restart REPLY_TIMEOUT = 1.0 @@ -40,8 +53,12 @@ class UploadError(Exception): pass +class NoBootloader(UploadError): + """The bootloader didn't answer (wrong baud rate, missing wire, ...).""" + + def read_hex(path): - """Parse an Intel HEX file into a bytearray (gaps filled with 0xFF).""" + """Parse an Intel HEX file into a bytearray (gaps filled with 0xFF), padded to whole pages.""" image = bytearray() base = 0 with open(path) as f: @@ -68,9 +85,6 @@ def read_hex(path): base = ((data[0] << 8) | data[1]) << 4 elif rtype == 0x04: # extended linear address base = ((data[0] << 8) | data[1]) << 16 - if len(image) > FLASH_SIZE: - raise UploadError(f"firmware is {len(image)} bytes, the Nano has room for {FLASH_SIZE}") - # pad to whole pages if len(image) % PAGE_SIZE: image.extend(b"\xff" * (PAGE_SIZE - len(image) % PAGE_SIZE)) return image @@ -81,7 +95,6 @@ class Bootloader: self.sock = sock def _recv(self, n, timeout=REPLY_TIMEOUT): - self.sock.settimeout(timeout) data = b"" deadline = time.monotonic() + timeout while len(data) < n: @@ -112,7 +125,7 @@ class Bootloader: return reply[1:-1] def sync(self): - """Keep sending GET_SYNC until the bootloader answers (it starts after the power cycle).""" + """Keep sending GET_SYNC until the bootloader answers (it starts after the restart).""" deadline = time.monotonic() + SYNC_TIMEOUT while time.monotonic() < deadline: self.sock.sendall(bytes([GET_SYNC, CRC_EOP])) @@ -122,9 +135,7 @@ class Bootloader: self._drain() self.command([GET_SYNC]) return - raise UploadError( - f"no answer from the Nano bootloader within {SYNC_TIMEOUT:.0f} s. " - "Is the Nano TX -> ESP32 GPIO 16 wire connected?") + raise NoBootloader(f"no answer from the Nano bootloader within {SYNC_TIMEOUT:.0f} s") def load_address(self, byte_addr): word = byte_addr // 2 @@ -147,56 +158,98 @@ def progress(label, done, total, step=PAGE_SIZE): print(f"{label} {percent:3d}% ({done}/{total} bytes)", flush=True) -def upload(hex_path, host, port): - image = read_hex(hex_path) - print(f"Firmware: {hex_path} ({len(image)} bytes)") - - print(f"Connecting to the ESP32 flash bridge at {host}:{port} ...") +def connect(host, port, baud): + """Connect to the ESP32 bridge and have it restart the Nano into its bootloader.""" try: sock = socket.create_connection((host, port), timeout=10) except OSError as e: raise UploadError(f"can't connect to {host}:{port}: {e}") sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) - with sock: - boot = Bootloader(sock) - print("Waiting for the Nano bootloader (the ESP32 power cycles the display) ...") - boot.sync() + sock.sendall(f"FLASH {baud}\n".encode()) + reply = b"" + deadline = time.monotonic() + BRIDGE_TIMEOUT + while not reply.endswith(b"\n") and time.monotonic() < deadline: + sock.settimeout(max(deadline - time.monotonic(), 0.01)) + try: + chunk = sock.recv(64) + except socket.timeout: + break + if not chunk: + break + reply += chunk + if reply != b"OK\n": + sock.close() + raise UploadError( + f"the ESP32 flash bridge didn't accept the request (reply: {reply!r}). " + "Is the ESP32 running up-to-date firmware?") + return sock - signature = boot.command([READ_SIGN], reply_len=3) - if signature != SIGNATURE: - raise UploadError(f"unexpected device signature {signature.hex()} (expected ATmega328P)") - print("Found ATmega328P") - boot.command([ENTER_PROGMODE]) +def flash(sock, image, baud): + name, flash_size = BOOTLOADERS[baud] + if len(image) > flash_size: + raise UploadError(f"firmware is {len(image)} bytes, but with the {name} the Nano has room for {flash_size}") - for addr in range(0, len(image), PAGE_SIZE): - boot.write_page(addr, image[addr:addr + PAGE_SIZE]) - progress("Writing ", addr + PAGE_SIZE, len(image)) + boot = Bootloader(sock) + signature = boot.command([READ_SIGN], reply_len=3) + if signature != SIGNATURE: + raise UploadError(f"unexpected device signature {signature.hex()} (expected ATmega328P)") + print(f"Found ATmega328P, bootloader: {name} ({baud} baud)") - for addr in range(0, len(image), PAGE_SIZE): - expected = image[addr:addr + PAGE_SIZE] - actual = boot.read_page(addr, PAGE_SIZE) - if actual != expected: - for i, (a, b) in enumerate(zip(actual, expected)): - if a != b: - raise UploadError( - f"verify failed at 0x{addr + i:04x}: read 0x{a:02x}, expected 0x{b:02x}") - progress("Verifying", addr + PAGE_SIZE, len(image)) + boot.command([ENTER_PROGMODE]) - boot.command([LEAVE_PROGMODE]) + for addr in range(0, len(image), PAGE_SIZE): + boot.write_page(addr, image[addr:addr + PAGE_SIZE]) + progress("Writing ", addr + PAGE_SIZE, len(image)) - print("Done. The ESP32 restarts the Nano with the new firmware.") + for addr in range(0, len(image), PAGE_SIZE): + expected = image[addr:addr + PAGE_SIZE] + actual = boot.read_page(addr, PAGE_SIZE) + if actual != expected: + for i, (a, b) in enumerate(zip(actual, expected)): + if a != b: + raise UploadError(f"verify failed at 0x{addr + i:04x}: read 0x{a:02x}, expected 0x{b:02x}") + progress("Verifying", addr + PAGE_SIZE, len(image)) + + boot.command([LEAVE_PROGMODE]) + + +def upload(hex_path, host, port, bauds): + image = read_hex(hex_path) + print(f"Firmware: {hex_path} ({len(image)} bytes)") + + for i, baud in enumerate(bauds): + print(f"Connecting to the ESP32 flash bridge at {host}:{port}, " + f"restarting the Nano into its bootloader at {baud} baud ...", flush=True) + with connect(host, port, baud) as sock: + try: + Bootloader(sock).sync() + except NoBootloader: + if i + 1 < len(bauds): + print(f"No answer at {baud} baud, trying {bauds[i + 1]} ...", flush=True) + continue + raise NoBootloader( + f"no answer from the Nano bootloader at {', '.join(map(str, bauds))} baud. " + "Check the Nano TX -> ESP32 GPIO 16 wire. A Nano with Optiboot also needs " + "the reset wire (nano_reset in src/esp32/libs.h).") + flash(sock, image, baud) + print("Done. The ESP32 restarts the Nano with the new firmware.") + return def main(): - if len(sys.argv) != 3: - print(__doc__) - return 2 - hex_path, target = sys.argv[1], sys.argv[2] - host, _, port = target.partition(":") + parser = argparse.ArgumentParser(description="Flash the Nano over WiFi through the ESP32.") + parser.add_argument("hex", help="firmware .hex file") + parser.add_argument("target", help=f"ESP32 address, HOST[:PORT] (default port {DEFAULT_PORT})") + parser.add_argument("--baud", default="auto", choices=["auto"] + [str(b) for b in BOOTLOADERS], + help="bootloader baud rate (default: auto)") + args = parser.parse_args() + + host, _, port = args.target.partition(":") + bauds = AUTO_ORDER if args.baud == "auto" else [int(args.baud)] try: - upload(hex_path, host, int(port) if port else DEFAULT_PORT) + upload(args.hex, host, int(port) if port else DEFAULT_PORT, bauds) except UploadError as e: sys.stdout.flush() print(f"ERROR: {e}", file=sys.stderr)