Power-cycle-friendly Nano boot and ESP relay handling

Nano / driver:
- No full on/off flip at boot; the first display() after boot is a forced
  full redraw instead, since the physical dot state is unknown
- Send READY when booted

ESP32:
- relayOn() only switches and waits when the relay was off, and waits for
  the Nano's READY (3 s timeout) instead of a fixed 3 s on every frame
- relayOff() ends Serial and releases the TX pin so the idle-high line no
  longer back-powers the Nano through its RX pin
- No display commands are sent while the relay is off
- Switching the relay on cancels a pending power-off countdown

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mangavalkandClaude Opus 5.5 committed 2026-10-13 00:12:28 +02:00
1 parent f7ffcf4ab3
commit 6e4d5e152b
8 files changed
+113 -23

No files matched your search

+11 -5
View File
@@ -30,9 +30,14 @@ See README.md for the overview.
- `src/esp32/secrets.h` holds the real credentials and is gitignored. Never commit it. - `src/esp32/secrets.h` holds the real credentials and is gitignored. Never commit it.
Add new secrets to `secrets.example.h` as placeholders. Add new secrets to `secrets.example.h` as placeholders.
- The relay (ESP32 GPIO 13, `LOW` = on) powers both the panels and the Nano. Each time it - The relay (ESP32 GPIO 13, `LOW` = on) powers both the panels and the Nano. Each time it
switches on, the Nano cold-boots: bootloader, then `begin()` flips every dot on and off. switches on, the Nano cold-boots and then sends `READY`. The ESP's `relayOn()` waits for
Any data the ESP sends during that time is lost. The Nano keeps no state over a power that message, at most `NANO_BOOT_TIMEOUT`. The timeout also covers the case where the
cycle except the flip time in EEPROM. Nano's TX isn't wired to the ESP's RX.
- `relayOff()` ends `Serial` and sets the TX pin to input. Otherwise the idle-high TX line
powers the Nano through its RX pin. Never send display commands while `relay_on` is
false.
- The Nano keeps no state over a power cycle except the flip time in EEPROM. The driver
forces a full redraw on the first `display()` after boot (`_force_next`).
- Hardware is not available here. Builds can be checked, but runtime behaviour can't. - Hardware is not available here. Builds can be checked, but runtime behaviour can't.
## Nano serial parser (`src/nano/main.cpp`) ## Nano serial parser (`src/nano/main.cpp`)
@@ -45,8 +50,9 @@ See README.md for the overview.
2 s with no serial data. The ESP sends one frame per second, ending with `D`. 2 s with no serial data. The ESP sends one frame per second, ending with `D`.
- Flip time is stored in EEPROM as a magic word at address 0 and a `uint16_t` at - Flip time is stored in EEPROM as a magic word at address 0 and a `uint16_t` at
address 2. Values outside 50–2000 µs are rejected. address 2. Values outside 50–2000 µs are rejected.
- The Nano doesn't send an acknowledgement. Whether the Nano's TX is wired to the ESP's RX - The Nano only replies `READY` (at boot) and `ERR: ...`. There is no per-frame
is unknown, so don't make the ESP wait for replies. acknowledgement. Whether the Nano's TX is wired to the ESP's RX is unconfirmed, so every
wait for a reply needs a timeout.
- To test the parser without hardware, compile `main.cpp` with g++ (Strawberry Perl - To test the parser without hardware, compile `main.cpp` with g++ (Strawberry Perl
provides it at `C:\Strawberry\c\bin`) and mock headers for `Arduino.h`, `EEPROM.h` and provides it at `C:\Strawberry\c\bin`) and mock headers for `Arduino.h`, `EEPROM.h` and
`BROSE9323.h`. `BROSE9323.h`.
+6 -4
View File
@@ -65,10 +65,6 @@ void BROSE9323::begin(void)
pinMode(ENABLE, OUTPUT); pinMode(ENABLE, OUTPUT);
_setData(1); _setData(1);
_setData(0); _setData(0);
fillScreen(1);
display();
fillScreen(0);
display();
} }
#endif #endif
@@ -80,6 +76,11 @@ void BROSE9323::display(bool force)
#else #else
if (_direct_mode) if (_direct_mode)
return; return;
if (_force_next)
{
force = true;
_force_next = false;
}
for (uint8_t x = 0; x < width(); x++) for (uint8_t x = 0; x < width(); x++)
{ {
_selectPanel(x / _panel_width); _selectPanel(x / _panel_width);
@@ -188,6 +189,7 @@ void BROSE9323::fillScreen(uint16_t color)
} }
#if !defined(__AVR_ATmega168P__) && !defined(__AVR_ATmega168PB__) && !defined(__AVR_ATmega168__) #if !defined(__AVR_ATmega168P__) && !defined(__AVR_ATmega168PB__) && !defined(__AVR_ATmega168__)
memset(_old_buffer, color ? 0xFF : 0x00, _buffer_size); memset(_old_buffer, color ? 0xFF : 0x00, _buffer_size);
_force_next = false;
#endif #endif
} }
#endif #endif
+2
View File
@@ -24,6 +24,8 @@ private:
uint8_t _active_col = 255; uint8_t _active_col = 255;
uint8_t _active_row = 255; uint8_t _active_row = 255;
bool _active_data; bool _active_data;
// After boot the physical dot state is unknown, so the first display() redraws everything
bool _force_next = true;
#ifndef FLIPDOT_PLCC_ADAPTER #ifndef FLIPDOT_PLCC_ADAPTER
const uint8_t ENABLE = 7, const uint8_t ENABLE = 7,
+7 -1
View File
@@ -26,7 +26,13 @@ BROSE9323 driver is built with `-D ESPSERIAL`, so every GFX call is sent over `S
- `FlipDotDisplayClock`: clock on, or the Nintendo logo when off - `FlipDotDisplayClock`: clock on, or the Nintendo logo when off
- `FlipDotDisplayPower`: turns the display off through the relay - `FlipDotDisplayPower`: turns the display off through the relay
- **Relay on GPIO 13** switches the power for both the panels and the Nano. `LOW` turns it - **Relay on GPIO 13** switches the power for both the panels and the Nano. `LOW` turns it
on. After switching on, the ESP waits 3 s so the Nano can boot before frames are sent. on.
- **On:** the ESP waits for the Nano's `READY` message, at most 3 s
(`NANO_BOOT_TIMEOUT`), before it sends frames. The relay stays on while it is needed;
the ESP only waits when it actually switches the relay on.
- **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.
- **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.
+5 -1
View File
@@ -44,7 +44,11 @@ HASwitch powerSwitch("FlipDotDisplayPower");
bool power = true; bool power = true;
const int led1 = BUILTIN_LED; // Pin of the LED const int led1 = BUILTIN_LED; // Pin of the LED
const int relay = 13; // Pin of the relay const int relay = 13; // Pin of the relay, powers the panels and the Nano (LOW = on)
const int nano_tx = 1; // UART0 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;
int hour = 0; int hour = 0;
int minute = 0; int minute = 0;
+70 -9
View File
@@ -5,6 +5,60 @@ void toggleLED(void *parameter);
uint32_t power_off_delay; uint32_t power_off_delay;
int triggered_power_off = false; int triggered_power_off = false;
int turn_hassio_power_off = false; int turn_hassio_power_off = false;
// Wait until the Nano reports READY after booting, or until NANO_BOOT_TIMEOUT
void waitForNano()
{
char line[16];
uint8_t len = 0;
uint32_t start = millis();
while (millis() - start < NANO_BOOT_TIMEOUT)
{
while (Serial.available())
{
char c = Serial.read();
if (c == '\n')
{
line[len] = '\0';
if (strstr(line, "READY"))
return;
len = 0;
}
else if (len < sizeof(line) - 1)
line[len++] = c;
}
delay(10);
}
}
void relayOn()
{
// cancel a pending power off countdown
triggered_power_off = false;
if (relay_on)
return;
Serial.begin(115200);
// drop old data (e.g. ERR replies) so only a new READY counts
while (Serial.available())
Serial.read();
digitalWrite(relay, LOW);
waitForNano();
relay_on = true;
}
void relayOff()
{
digitalWrite(relay, HIGH);
relay_on = false;
// An idle UART TX line is high and would power the Nano through its RX pin
Serial.flush();
Serial.end();
pinMode(nano_tx, INPUT);
}
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)
@@ -25,7 +79,7 @@ void updatePower()
triggered_power_off = false; triggered_power_off = false;
// OFF // OFF
digitalWrite(relay, HIGH); relayOff();
if(turn_hassio_power_off) if(turn_hassio_power_off)
{ {
@@ -45,11 +99,14 @@ void onPowerSwitchStateChanged(bool state, HASwitch *s)
power = (state ? HIGH : LOW); power = (state ? HIGH : LOW);
if(power == false) if(power == false)
{
if (relay_on)
{ {
display.fillScreen(0); display.fillScreen(0);
display.display(); display.display();
delay(1000); delay(1000);
}
turnPowerOff(0); turnPowerOff(0);
@@ -230,9 +287,7 @@ void setup()
otaPanicMode = false; otaPanicMode = false;
pinMode(relay, OUTPUT); pinMode(relay, OUTPUT);
digitalWrite(relay, LOW); relayOn();
delay(4000);
display.begin(&Serial); display.begin(&Serial);
// display.setTiming(200); // display.setTiming(200);
@@ -379,11 +434,7 @@ void drawRotatedBitmap(int16_t x, int16_t y, const uint8_t *bitmap, int16_t w, i
void turnPowerOn(bool overide = false) void turnPowerOn(bool overide = false)
{ {
if(power == true || overide == true) if(power == true || overide == true)
{ relayOn();
// ON
digitalWrite(relay, LOW);
delay(3000);
}
} }
void loop() void loop()
@@ -433,9 +484,12 @@ void loop()
// display.setFont(&FreeSerifItalic12pt7b); // display.setFont(&FreeSerifItalic12pt7b);
if(hour <= 7 || (hour >= 23 && minute > 30)) if(hour <= 7 || (hour >= 23 && minute > 30))
{
if (relay_on)
{ {
display.fillScreen(0); display.fillScreen(0);
display.display(); display.display();
}
turnPowerOff(); turnPowerOff();
} }
@@ -446,6 +500,9 @@ void loop()
{ {
turnPowerOn(); turnPowerOn();
// power can be off (Home Assistant power switch): then nothing is sent
if (relay_on)
{
int xoffset = 3; int xoffset = 3;
display.fillScreen(0); display.fillScreen(0);
@@ -469,12 +526,15 @@ void loop()
display.display(); display.display();
} }
}
else else
{ {
turnPowerOff(); turnPowerOff();
} }
} }
else else
{
if (relay_on)
{ {
display.fillScreen(0); display.fillScreen(0);
display.drawBitmap(0, 0, nintendo_bitmap_Nintendo_new, 84, 19, 1); display.drawBitmap(0, 0, nintendo_bitmap_Nintendo_new, 84, 19, 1);
@@ -504,6 +564,7 @@ void loop()
// display.print("Carla & Jan Kragt"); // display.print("Carla & Jan Kragt");
display.display(); display.display();
}
turnPowerOff(); turnPowerOff();
} }
+9 -3
View File
@@ -8,8 +8,11 @@ logic.
## Behaviour ## Behaviour
- **Boot:** the Nano loads the flip time from EEPROM and flips the whole display on and - **Boot:** the Nano loads the flip time from EEPROM, sets up the pins and sends `READY`.
then off. It doesn't flip any dots at boot. The relay powers the Nano, so it boots every time the
display is switched on.
- **First `display()` after boot** redraws every dot, because the Nano doesn't know which
state the dots were left in.
- **`display()`:** compares the new buffer with the last one that was shown and strobes - **`display()`:** compares the new buffer with the last one that was shown and strobes
only the dots that changed. only the dots that changed.
- **Every 2 minutes** (`REFRESH_TIME`) it does a full redraw to fix dots that got stuck. - **Every 2 minutes** (`REFRESH_TIME`) it does a full redraw to fix dots that got stuck.
@@ -47,7 +50,10 @@ skipped and the next line starts clean. Coordinates are 2-digit **hex**. Color i
| `R` | `R` | Print the buffer as ASCII art on the serial port | | `R` | `R` | Print the buffer as ASCII art on the serial port |
| `T` | `Tnnn` | Set the flip pulse length in µs (decimal, 50–2000), stored in EEPROM | | `T` | `Tnnn` | Set the flip pulse length in µs (decimal, 50–2000), stored in EEPROM |
Invalid commands get a reply that starts with `ERR:` on the Nano's TX pin. Replies on the Nano's TX pin:
- `READY` after booting. The ESP waits for it before sending the first frame.
- `ERR: ...` for invalid commands.
Example (what the ESP32 sends for one pixel and a refresh): Example (what the ESP32 sends for one pixel and a refresh):
+3
View File
@@ -265,6 +265,9 @@ void setup()
last_refresh = millis(); last_refresh = millis();
last_rx = millis(); last_rx = millis();
// Tells the ESP that frames can be sent (the relay powers the Nano, so this happens at every power on)
Serial.println(F("READY"));
} }
void loop() void loop()