Public Access
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:
1 parent
bea5b9daa1
commit
cefb76c1ad
8 files changed
+204
-61
No files matched your search
@@ -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 <baud>\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
|
||||
|
||||
@@ -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) |
|
||||
|
||||
|
||||
+3
-1
@@ -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
|
||||
|
||||
+6
-1
@@ -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;
|
||||
|
||||
+20
-4
@@ -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);
|
||||
|
||||
@@ -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 <baud>\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 <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()
|
||||
{
|
||||
_client.stop();
|
||||
|
||||
@@ -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 <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.
|
||||
// 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();
|
||||
};
|
||||
+87
-34
@@ -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,26 +158,44 @@ 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
|
||||
|
||||
|
||||
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}")
|
||||
|
||||
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("Found ATmega328P")
|
||||
print(f"Found ATmega328P, bootloader: {name} ({baud} baud)")
|
||||
|
||||
boot.command([ENTER_PROGMODE])
|
||||
|
||||
@@ -180,23 +209,47 @@ def upload(hex_path, host, port):
|
||||
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}")
|
||||
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)
|
||||
|
||||
Reference in new issue
Block a user