Support both Nano bootloaders for flashing over WiFi

- Uploader sends 'FLASH <baud>' 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 <noreply@anthropic.com>
This commit is contained in:
mangavalkandClaude Opus 5.5 committed 2026-10-13 00:44:35 +02:00
1 parent bea5b9daa1
commit cefb76c1ad
8 files changed
+220 -77

No files matched your search

+7 -3
View File
@@ -17,9 +17,13 @@ See README.md for the overview.
- If an ESP32 build fails with `No module named 'intelhex'`, run - If an ESP32 build fails with `No module named 'intelhex'`, run
`~/.platformio/penv/Scripts/python.exe -m pip install intelhex`. `~/.platformio/penv/Scripts/python.exe -m pip install intelhex`.
- `nano_ota` flashes the Nano over WiFi: `tools/nano_net_upload.py` (STK500v1 over TCP) - `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 talks to `NanoFlashBridge` on the ESP32 (port 2323). The uploader first sends
its old bootloader (57600 baud) and passes the bytes through. avrdude 6.3's `net:` port is `FLASH <baud>\n`. The ESP power cycles the Nano (plus an optional reset pulse on
broken on Windows (`ser_drain()` fails), which is why the uploader is a Python script. `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 The uploader only uses the Python standard library. Test it against a simulated
bootloader before changing it. bootloader before changing it.
- `src/esp32/libs.h` defines globals in the header, so it can only be included from - `src/esp32/libs.h` defines globals in the header, so it can only be included from
+32 -11
View File
@@ -92,10 +92,11 @@ pio run -e nano_ota -t upload
What happens: What happens:
1. The uploader ([tools/nano_net_upload.py](tools/nano_net_upload.py)) connects to the 1. The uploader ([tools/nano_net_upload.py](tools/nano_net_upload.py)) connects to the
ESP32 on TCP port **2323**. 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. This 2. The ESP32 pauses the clock and switches the relay off for 1 second, then on again. If
restarts the Nano, and its bootloader starts. the reset wire is fitted, it also pulses the Nano's RESET pin. This restarts the Nano,
3. The ESP32 switches its link to the bootloader's 57600 baud and passes bytes between and its bootloader starts.
3. The ESP32 switches its link to the bootloader's baud rate and passes bytes between
WiFi and the Nano. WiFi and the Nano.
4. The uploader writes the new firmware and reads it back to verify it. 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 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 - The reply wire must be connected: Nano D1 (TX) → voltage divider → ESP32 GPIO 16. The
uploader needs the bootloader's answers. uploader needs the bootloader's answers.
- The ESP32 must be running firmware with the flash bridge (this version or newer). - 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, **Bootloaders**
so the relay power cycle would not start it.
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** **Settings**
- ESP32 IP address: `upload_port` in the `nano_ota` environment in `platformio.ini`. You - 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 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`. `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 - Port, reset pin and timings: `NANO_FLASH_PORT`, `nano_reset`, `NANO_POWER_OFF_TIME` and
`NANO_POWER_OFF_TIME` in `src/esp32/libs.h`. `NANO_RESET_DELAY` in `src/esp32/libs.h`.
**If it fails** **If it fails**
| Message | Cause | | Message | Cause |
|---------|-------| |---------|-------|
| `can't connect to ...` | Wrong IP address, or the ESP32 is offline or running older firmware | | `can't connect to ...` | Wrong IP address, or the ESP32 is offline |
| `no answer from the Nano bootloader` | Reply wire missing, or the Nano has Optiboot | | `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. | | `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, 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 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 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 | | GND | GND | Common ground |
| GPIO 13 | Relay input | Relay powers the panels and the Nano (`LOW` = on) | | GPIO 13 | Relay input | Relay powers the panels and the Nano (`LOW` = on) |
+3 -1
View File
@@ -41,11 +41,13 @@ monitor_port = COM10
; Same Nano firmware, flashed over WiFi through the ESP32 (see README.md). ; 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. ; 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] [env:nano_ota]
extends = env:nano extends = env:nano
upload_protocol = custom upload_protocol = custom
upload_port = 10.1.1.198 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 ; ESP32 - controller, talks to the Nano over serial
+6 -1
View File
@@ -50,6 +50,9 @@ const int relay = 13; // Pin of the relay, powers the panels and the Nano (LOW =
HardwareSerial &nanoSerial = Serial2; HardwareSerial &nanoSerial = Serial2;
const int nano_rx = 16; // UART2 RX, wired to the Nano TX (through a 5V -> 3.3V divider) 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 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; 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) // 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; 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) // Flashing the Nano over WiFi (see README.md, pio run -e nano_ota -t upload)
const uint16_t NANO_FLASH_PORT = 2323; 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_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 hour = 0;
int minute = 0; int minute = 0;
+20 -4
View File
@@ -60,18 +60,29 @@ void relayOff()
pinMode(nano_tx, INPUT); 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. // 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) if (relay_on)
relayOff(); relayOff();
delay(NANO_POWER_OFF_TIME); 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()) while (nanoSerial.available())
nanoSerial.read(); nanoSerial.read();
digitalWrite(relay, LOW);
} }
// Flash session ended: switch off, the clock powers the Nano on again with the new firmware // Flash session ended: switch off, the clock powers the Nano on again with the new firmware
@@ -312,6 +323,11 @@ void setup()
otaPanicMode = false; otaPanicMode = false;
pinMode(relay, OUTPUT); pinMode(relay, OUTPUT);
if (nano_reset >= 0)
{
digitalWrite(nano_reset, LOW);
pinMode(nano_reset, OUTPUT);
}
relayOn(); relayOn();
display.begin(&nanoSerial); display.begin(&nanoSerial);
+39 -2
View File
@@ -1,6 +1,6 @@
#include "nano_flash_bridge.h" #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) : _server(port), _serial(serial), _onStart(onStart), _onEnd(onEnd)
{ {
} }
@@ -19,10 +19,19 @@ bool NanoFlashBridge::handle()
if (!client) if (!client)
return false; return false;
uint32_t baud = _readHeader(client);
if (!baud)
{
client.print("ERR expected FLASH <baud>\n");
client.stop();
return false;
}
_client = client; _client = client;
_client.setNoDelay(true); _client.setNoDelay(true);
_active = true; _active = true;
_onStart(); _onStart(baud);
_client.print("OK\n");
_last_activity = millis(); _last_activity = millis();
} }
@@ -58,6 +67,34 @@ bool NanoFlashBridge::handle()
return true; return true;
} }
// Read the "FLASH <baud>" 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() void NanoFlashBridge::_end()
{ {
_client.stop(); _client.stop();
+10 -5
View File
@@ -5,14 +5,16 @@
// WiFi to serial bridge for flashing the Nano from the PC (pio run -e nano_ota -t upload). // 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. // The uploader (tools/nano_net_upload.py) connects and first sends one line:
// After that, bytes are copied between the TCP connection and the Nano's serial port // FLASH <baud>\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. // 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 class NanoFlashBridge
{ {
public: 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(); void begin();
@@ -21,16 +23,19 @@ public:
bool handle(); bool handle();
private: 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 // Give up when no data moves in either direction for this long
static const uint32_t IDLE_TIMEOUT = 10000; static const uint32_t IDLE_TIMEOUT = 10000;
WiFiServer _server; WiFiServer _server;
WiFiClient _client; WiFiClient _client;
HardwareSerial &_serial; HardwareSerial &_serial;
void (*_onStart)(); void (*_onStart)(uint32_t baud);
void (*_onEnd)(); void (*_onEnd)();
bool _active = false; bool _active = false;
uint32_t _last_activity = 0; uint32_t _last_activity = 0;
uint32_t _readHeader(WiFiClient &client);
void _end(); void _end();
}; };
+103 -50
View File
@@ -1,25 +1,37 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Flash the Arduino Nano over WiFi, through the ESP32's flash bridge. """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 --baud is the Nano bootloader's baud rate:
passes bytes between this TCP connection and the Nano's serial port. This script talks 57600 old Nano bootloader (ATmegaBOOT)
the STK500v1 protocol of the Arduino bootloader directly. avrdude can't be used here: 115200 Optiboot ("new bootloader"); needs the ESP32 reset wire
its network ports are unreliable on Windows. 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 Normally run by PlatformIO: pio run -e nano_ota -t upload
""" """
import argparse
import socket import socket
import sys import sys
import time import time
DEFAULT_PORT = 2323 DEFAULT_PORT = 2323
PAGE_SIZE = 128 # ATmega328P flash page in bytes PAGE_SIZE = 128 # ATmega328P flash page in bytes
FLASH_SIZE = 30720 # 32 KB minus the 2 KB bootloader
SIGNATURE = b"\x1e\x95\x0f" # ATmega328P 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 # STK500v1
STK_OK = 0x10 STK_OK = 0x10
STK_INSYNC = 0x14 STK_INSYNC = 0x14
@@ -32,7 +44,8 @@ PROG_PAGE = 0x64
READ_PAGE = 0x74 READ_PAGE = 0x74
READ_SIGN = 0x75 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 REPLY_TIMEOUT = 1.0
@@ -40,8 +53,12 @@ class UploadError(Exception):
pass pass
class NoBootloader(UploadError):
"""The bootloader didn't answer (wrong baud rate, missing wire, ...)."""
def read_hex(path): 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() image = bytearray()
base = 0 base = 0
with open(path) as f: with open(path) as f:
@@ -68,9 +85,6 @@ def read_hex(path):
base = ((data[0] << 8) | data[1]) << 4 base = ((data[0] << 8) | data[1]) << 4
elif rtype == 0x04: # extended linear address elif rtype == 0x04: # extended linear address
base = ((data[0] << 8) | data[1]) << 16 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: if len(image) % PAGE_SIZE:
image.extend(b"\xff" * (PAGE_SIZE - len(image) % PAGE_SIZE)) image.extend(b"\xff" * (PAGE_SIZE - len(image) % PAGE_SIZE))
return image return image
@@ -81,7 +95,6 @@ class Bootloader:
self.sock = sock self.sock = sock
def _recv(self, n, timeout=REPLY_TIMEOUT): def _recv(self, n, timeout=REPLY_TIMEOUT):
self.sock.settimeout(timeout)
data = b"" data = b""
deadline = time.monotonic() + timeout deadline = time.monotonic() + timeout
while len(data) < n: while len(data) < n:
@@ -112,7 +125,7 @@ class Bootloader:
return reply[1:-1] return reply[1:-1]
def sync(self): 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 deadline = time.monotonic() + SYNC_TIMEOUT
while time.monotonic() < deadline: while time.monotonic() < deadline:
self.sock.sendall(bytes([GET_SYNC, CRC_EOP])) self.sock.sendall(bytes([GET_SYNC, CRC_EOP]))
@@ -122,9 +135,7 @@ class Bootloader:
self._drain() self._drain()
self.command([GET_SYNC]) self.command([GET_SYNC])
return return
raise UploadError( raise NoBootloader(f"no answer from the Nano bootloader within {SYNC_TIMEOUT:.0f} s")
f"no answer from the Nano bootloader within {SYNC_TIMEOUT:.0f} s. "
"Is the Nano TX -> ESP32 GPIO 16 wire connected?")
def load_address(self, byte_addr): def load_address(self, byte_addr):
word = byte_addr // 2 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) print(f"{label} {percent:3d}% ({done}/{total} bytes)", flush=True)
def upload(hex_path, host, port): def connect(host, port, baud):
image = read_hex(hex_path) """Connect to the ESP32 bridge and have it restart the Nano into its bootloader."""
print(f"Firmware: {hex_path} ({len(image)} bytes)")
print(f"Connecting to the ESP32 flash bridge at {host}:{port} ...")
try: try:
sock = socket.create_connection((host, port), timeout=10) sock = socket.create_connection((host, port), timeout=10)
except OSError as e: except OSError as e:
raise UploadError(f"can't connect to {host}:{port}: {e}") raise UploadError(f"can't connect to {host}:{port}: {e}")
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
with sock: sock.sendall(f"FLASH {baud}\n".encode())
boot = Bootloader(sock) reply = b""
print("Waiting for the Nano bootloader (the ESP32 power cycles the display) ...") deadline = time.monotonic() + BRIDGE_TIMEOUT
boot.sync() 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 = Bootloader(sock)
boot.write_page(addr, image[addr:addr + PAGE_SIZE]) signature = boot.command([READ_SIGN], reply_len=3)
progress("Writing ", addr + PAGE_SIZE, len(image)) 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): boot.command([ENTER_PROGMODE])
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]) 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(): def main():
if len(sys.argv) != 3: parser = argparse.ArgumentParser(description="Flash the Nano over WiFi through the ESP32.")
print(__doc__) parser.add_argument("hex", help="firmware .hex file")
return 2 parser.add_argument("target", help=f"ESP32 address, HOST[:PORT] (default port {DEFAULT_PORT})")
hex_path, target = sys.argv[1], sys.argv[2] parser.add_argument("--baud", default="auto", choices=["auto"] + [str(b) for b in BOOTLOADERS],
host, _, port = target.partition(":") 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: 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: except UploadError as e:
sys.stdout.flush() sys.stdout.flush()
print(f"ERROR: {e}", file=sys.stderr) print(f"ERROR: {e}", file=sys.stderr)