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:
mangavalkandClaude Opus 5.5 committed 2026-10-13 00:35:28 +02:00
1 parent 8415dee347
commit bea5b9daa1
10 files changed
+438 -7

No files matched your search

+4
View File
@@ -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 |
| `libs.h` | includes and global objects (display, MQTT, WiFi, pins, timezone) |
| `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**) |
## 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
to input). An idle UART line is high and would otherwise power the Nano through its RX
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
off at night or when the display is powered off.
+7
View File
@@ -1,6 +1,7 @@
#include <Arduino.h>
#include "BROSE9323.h"
#include "bitmaps.h"
#include "nano_flash_bridge.h"
// WiFi / MQTT credentials (gitignored, copy secrets.example.h -> 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;
// 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_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 minute = 0;
+31 -1
View File
@@ -39,7 +39,7 @@ void relayOn()
if (relay_on)
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
while (nanoSerial.available())
nanoSerial.read();
@@ -59,6 +59,30 @@ void relayOff()
nanoSerial.end();
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)
{
if(triggered_power_off == false)
@@ -269,6 +293,7 @@ void setup()
});
ArduinoOTA.begin();
nanoFlashBridge.begin();
do
{
@@ -441,6 +466,11 @@ void loop()
{
ArduinoOTA.handle();
mqtt.loop();
// While the Nano is being flashed, nothing else may talk to it
if (nanoFlashBridge.handle())
return;
updatePower();
wifiStatus = WiFi.waitForConnectResult() == WL_CONNECTED;
+66
View File
@@ -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();
}
+36
View File
@@ -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();
};