Public Access
Flash the Nano over WiFi through the ESP32
- ESP32: NanoFlashBridge on TCP port 2323. A connection power cycles the Nano into its bootloader and bridges TCP <-> Serial2 at 57600 baud; the clock pauses during the session, 10 s idle timeout - tools/nano_net_upload.py: STK500v1 uploader over TCP (standard library only), with sync retries, write and read-back verification. avrdude 6.3 net: ports are broken on Windows, so avrdude is not used - New nano_ota environment: pio run -e nano_ota -t upload - README: usage, requirements, troubleshooting; TODO for hardware test Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8415dee347
commit
bea5b9daa1
10 files changed
+437
-6
No files matched your search
@@ -20,3 +20,6 @@ desktop.ini
|
|||||||
*.swp
|
*.swp
|
||||||
*~
|
*~
|
||||||
*.bak
|
*.bak
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
@@ -16,6 +16,14 @@ See README.md for the overview.
|
|||||||
or `$env:USERPROFILE\.platformio\penv\Scripts\pio.exe run` (PowerShell).
|
or `$env:USERPROFILE\.platformio\penv\Scripts\pio.exe run` (PowerShell).
|
||||||
- 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)
|
||||||
|
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.
|
||||||
|
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
|
||||||
|
`main.cpp`. New ESP32 modules must be self-contained (see `nano_flash_bridge.*`).
|
||||||
- After any change, build both targets (`pio run`). After Nano changes, also run
|
- After any change, build both targets (`pio run`). After Nano changes, also run
|
||||||
`pio test -e native`.
|
`pio test -e native`.
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ src/esp32/ ESP32 firmware
|
|||||||
secrets.example.h template for WiFi / MQTT credentials
|
secrets.example.h template for WiFi / MQTT credentials
|
||||||
test/test_nano/ unit tests for the Nano firmware (run on the PC)
|
test/test_nano/ unit tests for the Nano firmware (run on the PC)
|
||||||
test/mocks/ fake Arduino, EEPROM and display driver for the tests
|
test/mocks/ fake Arduino, EEPROM and display driver for the tests
|
||||||
|
tools/nano_net_upload.py uploader for flashing the Nano over WiFi
|
||||||
archive/ old, unused sketches kept for reference
|
archive/ old, unused sketches kept for reference
|
||||||
CLAUDE.md project notes for Claude Code
|
CLAUDE.md project notes for Claude Code
|
||||||
```
|
```
|
||||||
@@ -57,12 +58,14 @@ this folder.
|
|||||||
| Environment | Board | Upload method |
|
| Environment | Board | Upload method |
|
||||||
|-------------|-------|---------------|
|
|-------------|-------|---------------|
|
||||||
| `nano` | Arduino Nano (ATmega328P) | USB (`COM10`) |
|
| `nano` | Arduino Nano (ATmega328P) | USB (`COM10`) |
|
||||||
|
| `nano_ota` | Arduino Nano (ATmega328P) | Over WiFi through the ESP32 (`10.1.1.198`). See [Flashing the Nano over WiFi](#flashing-the-nano-over-wifi). |
|
||||||
| `esp32` | ESP32 DevKit v1 | OTA over WiFi (`10.1.1.198`) |
|
| `esp32` | ESP32 DevKit v1 | OTA over WiFi (`10.1.1.198`) |
|
||||||
| `esp32_usb` | ESP32 DevKit v1 | USB (`COM11`). Use it for the first flash or for recovery. |
|
| `esp32_usb` | ESP32 DevKit v1 | USB (`COM11`). Use it for the first flash or for recovery. |
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
pio run # build nano + esp32
|
pio run # build nano + esp32
|
||||||
pio run -e nano -t upload # flash the Nano
|
pio run -e nano -t upload # flash the Nano over USB
|
||||||
|
pio run -e nano_ota -t upload # flash the Nano over WiFi, through the ESP32
|
||||||
pio run -e esp32 -t upload # flash the ESP32 over the air
|
pio run -e esp32 -t upload # flash the ESP32 over the air
|
||||||
pio run -e esp32_usb -t upload # flash the ESP32 over USB
|
pio run -e esp32_usb -t upload # flash the ESP32 over USB
|
||||||
pio device monitor -e nano # serial monitor
|
pio device monitor -e nano # serial monitor
|
||||||
@@ -76,7 +79,60 @@ In VS Code you can also pick the environment in the PlatformIO status bar.
|
|||||||
|
|
||||||
> **Tip:** The ESP32 → Nano wire goes to the Nano's RX pin, which the Nano's USB chip
|
> **Tip:** The ESP32 → Nano wire goes to the Nano's RX pin, which the Nano's USB chip
|
||||||
> also uses. Disconnect that wire before you flash the Nano over USB, or the upload may
|
> also uses. Disconnect that wire before you flash the Nano over USB, or the upload may
|
||||||
> fail.
|
> fail. Flashing over WiFi (`nano_ota`) doesn't have this problem.
|
||||||
|
|
||||||
|
## Flashing the Nano over WiFi
|
||||||
|
|
||||||
|
The ESP32 can flash the Nano for you, so you don't need a USB cable at the display:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
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
|
||||||
|
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
|
||||||
|
Nano on again with the new firmware and continues.
|
||||||
|
|
||||||
|
The display goes dark for the duration of the upload, which takes a few seconds.
|
||||||
|
|
||||||
|
**Requirements**
|
||||||
|
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
**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`.
|
||||||
|
|
||||||
|
**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 |
|
||||||
|
| `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,
|
||||||
|
even if the uploader is interrupted.
|
||||||
|
|
||||||
|
> **Security:** anyone on your network can connect to port 2323 and flash the Nano, just
|
||||||
|
> like the ESP32's own OTA has no password. Keep the display on a trusted network.
|
||||||
|
|
||||||
## Wiring between ESP32 and Nano
|
## Wiring between ESP32 and Nano
|
||||||
|
|
||||||
@@ -86,7 +142,7 @@ The ESP32 talks to the Nano on **UART2** (`Serial2`, 115200 baud). The ESP32's o
|
|||||||
| ESP32 | Nano | Notes |
|
| ESP32 | Nano | Notes |
|
||||||
|-------|------|-------|
|
|-------|------|-------|
|
||||||
| GPIO 17 (TX2) | D0 (RX) | Display commands |
|
| GPIO 17 (TX2) | D0 (RX) | Display commands |
|
||||||
| GPIO 16 (RX2) | D1 (TX) | `READY` and `ERR` replies. Use a 5 V → 3.3 V voltage divider (e.g. 1 kΩ / 2 kΩ), because the ESP32 is not 5 V tolerant. Optional: without it, the ESP waits a fixed 3 s after power-on. |
|
| 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. |
|
||||||
| 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) |
|
||||||
|
|
||||||
@@ -118,7 +174,11 @@ The serial protocol is described in [src/nano/README.md](src/nano/README.md).
|
|||||||
you flash this firmware, or the display gets no data. See
|
you flash this firmware, or the display gets no data. See
|
||||||
[Wiring between ESP32 and Nano](#wiring-between-esp32-and-nano).
|
[Wiring between ESP32 and Nano](#wiring-between-esp32-and-nano).
|
||||||
- [ ] **Add the reply wire:** Nano D1 (TX) → voltage divider → ESP32 GPIO 16 (RX2). With it,
|
- [ ] **Add the reply wire:** Nano D1 (TX) → voltage divider → ESP32 GPIO 16 (RX2). With it,
|
||||||
the ESP sends the first frame as soon as the Nano is ready, instead of after 3 s.
|
the ESP sends the first frame as soon as the Nano is ready, instead of after 3 s, and
|
||||||
|
the Nano can be flashed over WiFi (`nano_ota`).
|
||||||
|
- [ ] **Test flashing over WiFi on the real hardware.** The uploader is tested against a
|
||||||
|
simulated bootloader, not yet against the real Nano. Check especially that the old
|
||||||
|
bootloader starts after the relay power cycle.
|
||||||
- [ ] **Check the boot timing.** Build the Nano with `MEASURE_REFRESH` to see how long a
|
- [ ] **Check the boot timing.** Build the Nano with `MEASURE_REFRESH` to see how long a
|
||||||
full redraw takes. If the reply wire isn't fitted, check that 3 s
|
full redraw takes. If the reply wire isn't fitted, check that 3 s
|
||||||
(`NANO_BOOT_TIMEOUT`) is enough for the Nano to boot.
|
(`NANO_BOOT_TIMEOUT`) is enough for the Nano to boot.
|
||||||
+10
-1
@@ -1,6 +1,7 @@
|
|||||||
; Flipdot display - one PlatformIO project, two firmware targets.
|
; Flipdot display - one PlatformIO project, two firmware targets.
|
||||||
;
|
;
|
||||||
; nano Arduino Nano that drives the Brose flipdot panels directly
|
; nano Arduino Nano that drives the Brose flipdot panels directly (USB upload)
|
||||||
|
; nano_ota Same Nano firmware, flashed over WiFi through the ESP32
|
||||||
; esp32 ESP32 "brain" (WiFi, NTP clock, Home Assistant) - OTA upload
|
; esp32 ESP32 "brain" (WiFi, NTP clock, Home Assistant) - OTA upload
|
||||||
; esp32_usb Same ESP32 firmware, flashed over USB (first flash / recovery)
|
; esp32_usb Same ESP32 firmware, flashed over USB (first flash / recovery)
|
||||||
; native Unit tests for the Nano firmware, run on the PC (needs gcc on PATH)
|
; native Unit tests for the Nano firmware, run on the PC (needs gcc on PATH)
|
||||||
@@ -38,6 +39,14 @@ build_src_filter = +<nano/>
|
|||||||
upload_port = COM10
|
upload_port = COM10
|
||||||
monitor_port = COM10
|
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.
|
||||||
|
[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
|
||||||
|
|
||||||
; ---------------------------------------------------------------------------
|
; ---------------------------------------------------------------------------
|
||||||
; ESP32 - controller, talks to the Nano over serial
|
; ESP32 - controller, talks to the Nano over serial
|
||||||
; ---------------------------------------------------------------------------
|
; ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ debug output. Wiring is described in the [main README](../../README.md#wiring-be
|
|||||||
| `main.cpp` | setup, OTA, clock / power logic, Home Assistant callbacks |
|
| `main.cpp` | setup, OTA, clock / power logic, Home Assistant callbacks |
|
||||||
| `libs.h` | includes and global objects (display, MQTT, WiFi, pins, timezone) |
|
| `libs.h` | includes and global objects (display, MQTT, WiFi, pins, timezone) |
|
||||||
| `bitmaps.h` | 1-bit bitmaps (Pikachu, thought bubble, Nintendo logo, ...) |
|
| `bitmaps.h` | 1-bit bitmaps (Pikachu, thought bubble, Nintendo logo, ...) |
|
||||||
|
| `nano_flash_bridge.h/.cpp` | WiFi to serial bridge for flashing the Nano over WiFi (TCP port 2323) |
|
||||||
| `secrets.example.h` | template for `secrets.h` (WiFi and MQTT credentials, **gitignored**) |
|
| `secrets.example.h` | template for `secrets.h` (WiFi and MQTT credentials, **gitignored**) |
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
@@ -34,6 +35,9 @@ debug output. Wiring is described in the [main README](../../README.md#wiring-be
|
|||||||
- **Off:** the ESP stops sending and releases its TX pin (`Serial.end()` and the pin set
|
- **Off:** the ESP stops sending and releases its TX pin (`Serial.end()` and the pin set
|
||||||
to input). An idle UART line is high and would otherwise power the Nano through its RX
|
to input). An idle UART line is high and would otherwise power the Nano through its RX
|
||||||
pin.
|
pin.
|
||||||
|
- **Nano flash bridge:** `pio run -e nano_ota -t upload` flashes the Nano through the ESP32.
|
||||||
|
While a flash session runs, the clock is paused. See the
|
||||||
|
[main README](../../README.md#flashing-the-nano-over-wifi).
|
||||||
- **Status LED** (built-in) blinks fast when WiFi is down and slowly when connected. It is
|
- **Status LED** (built-in) blinks fast when WiFi is down and slowly when connected. It is
|
||||||
off at night or when the display is powered off.
|
off at night or when the display is powered off.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include "BROSE9323.h"
|
#include "BROSE9323.h"
|
||||||
#include "bitmaps.h"
|
#include "bitmaps.h"
|
||||||
|
#include "nano_flash_bridge.h"
|
||||||
|
|
||||||
// WiFi / MQTT credentials (gitignored, copy secrets.example.h -> secrets.h)
|
// WiFi / MQTT credentials (gitignored, copy secrets.example.h -> secrets.h)
|
||||||
#if __has_include("secrets.h")
|
#if __has_include("secrets.h")
|
||||||
@@ -52,6 +53,12 @@ const int nano_tx = 17; // UART2 TX, wired to the Nano RX
|
|||||||
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;
|
||||||
|
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
|
||||||
|
const uint32_t NANO_POWER_OFF_TIME = 1000; // relay off time to make sure the Nano resets
|
||||||
|
|
||||||
int hour = 0;
|
int hour = 0;
|
||||||
int minute = 0;
|
int minute = 0;
|
||||||
|
|||||||
+31
-1
@@ -39,7 +39,7 @@ void relayOn()
|
|||||||
if (relay_on)
|
if (relay_on)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
nanoSerial.begin(115200, SERIAL_8N1, nano_rx, nano_tx);
|
nanoSerial.begin(NANO_BAUD, SERIAL_8N1, nano_rx, nano_tx);
|
||||||
// drop old data (e.g. ERR replies) so only a new READY counts
|
// drop old data (e.g. ERR replies) so only a new READY counts
|
||||||
while (nanoSerial.available())
|
while (nanoSerial.available())
|
||||||
nanoSerial.read();
|
nanoSerial.read();
|
||||||
@@ -59,6 +59,30 @@ void relayOff()
|
|||||||
nanoSerial.end();
|
nanoSerial.end();
|
||||||
pinMode(nano_tx, INPUT);
|
pinMode(nano_tx, INPUT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Flash session started: power cycle 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()
|
||||||
|
{
|
||||||
|
if (relay_on)
|
||||||
|
relayOff();
|
||||||
|
delay(NANO_POWER_OFF_TIME);
|
||||||
|
|
||||||
|
nanoSerial.begin(NANO_BOOTLOADER_BAUD, SERIAL_8N1, nano_rx, nano_tx);
|
||||||
|
while (nanoSerial.available())
|
||||||
|
nanoSerial.read();
|
||||||
|
digitalWrite(relay, LOW);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flash session ended: switch off, the clock powers the Nano on again with the new firmware
|
||||||
|
void endNanoFlash()
|
||||||
|
{
|
||||||
|
relayOff();
|
||||||
|
// skip the frames that were missed while flashing
|
||||||
|
next_display = millis();
|
||||||
|
}
|
||||||
|
|
||||||
|
NanoFlashBridge nanoFlashBridge(NANO_FLASH_PORT, nanoSerial, startNanoFlash, endNanoFlash);
|
||||||
void turnPowerOff(uint32_t delay = 30000, bool _turn_hassio_power_off = false)
|
void turnPowerOff(uint32_t delay = 30000, bool _turn_hassio_power_off = false)
|
||||||
{
|
{
|
||||||
if(triggered_power_off == false)
|
if(triggered_power_off == false)
|
||||||
@@ -269,6 +293,7 @@ void setup()
|
|||||||
});
|
});
|
||||||
|
|
||||||
ArduinoOTA.begin();
|
ArduinoOTA.begin();
|
||||||
|
nanoFlashBridge.begin();
|
||||||
|
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
@@ -441,6 +466,11 @@ void loop()
|
|||||||
{
|
{
|
||||||
ArduinoOTA.handle();
|
ArduinoOTA.handle();
|
||||||
mqtt.loop();
|
mqtt.loop();
|
||||||
|
|
||||||
|
// While the Nano is being flashed, nothing else may talk to it
|
||||||
|
if (nanoFlashBridge.handle())
|
||||||
|
return;
|
||||||
|
|
||||||
updatePower();
|
updatePower();
|
||||||
|
|
||||||
wifiStatus = WiFi.waitForConnectResult() == WL_CONNECTED;
|
wifiStatus = WiFi.waitForConnectResult() == WL_CONNECTED;
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
#include "nano_flash_bridge.h"
|
||||||
|
|
||||||
|
NanoFlashBridge::NanoFlashBridge(uint16_t port, HardwareSerial &serial, void (*onStart)(), void (*onEnd)())
|
||||||
|
: _server(port), _serial(serial), _onStart(onStart), _onEnd(onEnd)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void NanoFlashBridge::begin()
|
||||||
|
{
|
||||||
|
_server.begin();
|
||||||
|
_server.setNoDelay(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NanoFlashBridge::handle()
|
||||||
|
{
|
||||||
|
if (!_active)
|
||||||
|
{
|
||||||
|
WiFiClient client = _server.available();
|
||||||
|
if (!client)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
_client = client;
|
||||||
|
_client.setNoDelay(true);
|
||||||
|
_active = true;
|
||||||
|
_onStart();
|
||||||
|
_last_activity = millis();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_client.connected() || millis() - _last_activity > IDLE_TIMEOUT)
|
||||||
|
{
|
||||||
|
_end();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t buf[256];
|
||||||
|
|
||||||
|
// PC -> Nano
|
||||||
|
int n = _client.available();
|
||||||
|
if (n > 0)
|
||||||
|
{
|
||||||
|
n = _client.read(buf, min(n, (int)sizeof(buf)));
|
||||||
|
if (n > 0)
|
||||||
|
{
|
||||||
|
_serial.write(buf, n);
|
||||||
|
_last_activity = millis();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nano -> PC
|
||||||
|
n = _serial.available();
|
||||||
|
if (n > 0)
|
||||||
|
{
|
||||||
|
n = _serial.readBytes(buf, min(n, (int)sizeof(buf)));
|
||||||
|
_client.write(buf, n);
|
||||||
|
_last_activity = millis();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NanoFlashBridge::_end()
|
||||||
|
{
|
||||||
|
_client.stop();
|
||||||
|
_active = false;
|
||||||
|
_onEnd();
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <WiFi.h>
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// 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)());
|
||||||
|
|
||||||
|
void begin();
|
||||||
|
|
||||||
|
// Call from loop(). Returns true while a flash session is running; the caller must not
|
||||||
|
// send anything to the Nano then.
|
||||||
|
bool handle();
|
||||||
|
|
||||||
|
private:
|
||||||
|
// 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 (*_onEnd)();
|
||||||
|
bool _active = false;
|
||||||
|
uint32_t _last_activity = 0;
|
||||||
|
|
||||||
|
void _end();
|
||||||
|
};
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
#!/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]
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Normally run by PlatformIO: pio run -e nano_ota -t upload
|
||||||
|
"""
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# STK500v1
|
||||||
|
STK_OK = 0x10
|
||||||
|
STK_INSYNC = 0x14
|
||||||
|
CRC_EOP = 0x20
|
||||||
|
GET_SYNC = 0x30
|
||||||
|
ENTER_PROGMODE = 0x50
|
||||||
|
LEAVE_PROGMODE = 0x51
|
||||||
|
LOAD_ADDRESS = 0x55
|
||||||
|
PROG_PAGE = 0x64
|
||||||
|
READ_PAGE = 0x74
|
||||||
|
READ_SIGN = 0x75
|
||||||
|
|
||||||
|
SYNC_TIMEOUT = 10.0 # seconds to wait for the bootloader after connecting
|
||||||
|
REPLY_TIMEOUT = 1.0
|
||||||
|
|
||||||
|
|
||||||
|
class UploadError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def read_hex(path):
|
||||||
|
"""Parse an Intel HEX file into a bytearray (gaps filled with 0xFF)."""
|
||||||
|
image = bytearray()
|
||||||
|
base = 0
|
||||||
|
with open(path) as f:
|
||||||
|
for line_no, line in enumerate(f, 1):
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
if not line.startswith(":"):
|
||||||
|
raise UploadError(f"{path}:{line_no}: not an Intel HEX line")
|
||||||
|
raw = bytes.fromhex(line[1:])
|
||||||
|
if sum(raw) & 0xFF:
|
||||||
|
raise UploadError(f"{path}:{line_no}: checksum error")
|
||||||
|
count, addr, rtype, data = raw[0], (raw[1] << 8) | raw[2], raw[3], raw[4:4 + raw[0]]
|
||||||
|
if len(data) != count:
|
||||||
|
raise UploadError(f"{path}:{line_no}: length mismatch")
|
||||||
|
if rtype == 0x00: # data
|
||||||
|
start = base + addr
|
||||||
|
if start + count > len(image):
|
||||||
|
image.extend(b"\xff" * (start + count - len(image)))
|
||||||
|
image[start:start + count] = data
|
||||||
|
elif rtype == 0x01: # end of file
|
||||||
|
break
|
||||||
|
elif rtype == 0x02: # extended segment address
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
class Bootloader:
|
||||||
|
def __init__(self, sock):
|
||||||
|
self.sock = sock
|
||||||
|
|
||||||
|
def _recv(self, n, timeout=REPLY_TIMEOUT):
|
||||||
|
self.sock.settimeout(timeout)
|
||||||
|
data = b""
|
||||||
|
deadline = time.monotonic() + timeout
|
||||||
|
while len(data) < n:
|
||||||
|
remaining = deadline - time.monotonic()
|
||||||
|
if remaining <= 0:
|
||||||
|
break
|
||||||
|
self.sock.settimeout(remaining)
|
||||||
|
try:
|
||||||
|
chunk = self.sock.recv(n - len(data))
|
||||||
|
except socket.timeout:
|
||||||
|
break
|
||||||
|
if not chunk:
|
||||||
|
raise UploadError("connection closed by the ESP32")
|
||||||
|
data += chunk
|
||||||
|
return data
|
||||||
|
|
||||||
|
def _drain(self):
|
||||||
|
"""Throw away everything that is still arriving (e.g. answers to earlier syncs)."""
|
||||||
|
while self._recv(256, timeout=0.1):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def command(self, payload, reply_len=0):
|
||||||
|
"""Send a command and return its reply data (between INSYNC and OK)."""
|
||||||
|
self.sock.sendall(bytes(payload) + bytes([CRC_EOP]))
|
||||||
|
reply = self._recv(reply_len + 2)
|
||||||
|
if len(reply) != reply_len + 2 or reply[0] != STK_INSYNC or reply[-1] != STK_OK:
|
||||||
|
raise UploadError(f"bad reply to command 0x{payload[0]:02x}: {reply.hex() or 'nothing'}")
|
||||||
|
return reply[1:-1]
|
||||||
|
|
||||||
|
def sync(self):
|
||||||
|
"""Keep sending GET_SYNC until the bootloader answers (it starts after the power cycle)."""
|
||||||
|
deadline = time.monotonic() + SYNC_TIMEOUT
|
||||||
|
while time.monotonic() < deadline:
|
||||||
|
self.sock.sendall(bytes([GET_SYNC, CRC_EOP]))
|
||||||
|
if self._recv(2, timeout=0.2) == bytes([STK_INSYNC, STK_OK]):
|
||||||
|
# Answers to earlier syncs may still be on their way: clear them, then
|
||||||
|
# sync once more so the replies line up with our commands.
|
||||||
|
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?")
|
||||||
|
|
||||||
|
def load_address(self, byte_addr):
|
||||||
|
word = byte_addr // 2
|
||||||
|
self.command([LOAD_ADDRESS, word & 0xFF, word >> 8])
|
||||||
|
|
||||||
|
def write_page(self, byte_addr, data):
|
||||||
|
self.load_address(byte_addr)
|
||||||
|
self.command([PROG_PAGE, len(data) >> 8, len(data) & 0xFF, ord("F")] + list(data))
|
||||||
|
|
||||||
|
def read_page(self, byte_addr, length):
|
||||||
|
self.load_address(byte_addr)
|
||||||
|
return self.command([READ_PAGE, length >> 8, length & 0xFF, ord("F")], reply_len=length)
|
||||||
|
|
||||||
|
|
||||||
|
def progress(label, done, total, step=PAGE_SIZE):
|
||||||
|
"""Print a line every 10% (PlatformIO shows each update on its own line)."""
|
||||||
|
percent = 100 * done // total
|
||||||
|
previous = 100 * (done - step) // total
|
||||||
|
if done == step or done == total or percent // 10 != previous // 10:
|
||||||
|
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} ...")
|
||||||
|
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()
|
||||||
|
|
||||||
|
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])
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
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])
|
||||||
|
|
||||||
|
print("Done. The ESP32 restarts the Nano with the new firmware.")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) != 3:
|
||||||
|
print(__doc__)
|
||||||
|
return 2
|
||||||
|
hex_path, target = sys.argv[1], sys.argv[2]
|
||||||
|
host, _, port = target.partition(":")
|
||||||
|
try:
|
||||||
|
upload(hex_path, host, int(port) if port else DEFAULT_PORT)
|
||||||
|
except UploadError as e:
|
||||||
|
sys.stdout.flush()
|
||||||
|
print(f"ERROR: {e}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
Reference in new issue
Block a user