From 6e4d5e152b52ad879c1647409046b2a4eeae0dc7 Mon Sep 17 00:00:00 2001 From: MangaValk Date: Tue, 13 Oct 2026 00:12:28 +0200 Subject: [PATCH] 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 --- CLAUDE.md | 16 ++-- lib/BROSE9323/BROSE9323.cpp | 10 ++- lib/BROSE9323/BROSE9323.h | 2 + src/esp32/README.md | 8 +- src/esp32/libs.h | 6 +- src/esp32/main.cpp | 169 ++++++++++++++++++++++++------------ src/nano/README.md | 12 ++- src/nano/main.cpp | 3 + 8 files changed, 158 insertions(+), 68 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index d2be757..b3b740e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -30,9 +30,14 @@ See README.md for the overview. - `src/esp32/secrets.h` holds the real credentials and is gitignored. Never commit it. 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 - switches on, the Nano cold-boots: bootloader, then `begin()` flips every dot on and off. - Any data the ESP sends during that time is lost. The Nano keeps no state over a power - cycle except the flip time in EEPROM. + switches on, the Nano cold-boots and then sends `READY`. The ESP's `relayOn()` waits for + that message, at most `NANO_BOOT_TIMEOUT`. The timeout also covers the case where the + 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. ## 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`. - 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. -- The Nano doesn't send an acknowledgement. Whether the Nano's TX is wired to the ESP's RX - is unknown, so don't make the ESP wait for replies. +- The Nano only replies `READY` (at boot) and `ERR: ...`. There is no per-frame + 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 provides it at `C:\Strawberry\c\bin`) and mock headers for `Arduino.h`, `EEPROM.h` and `BROSE9323.h`. diff --git a/lib/BROSE9323/BROSE9323.cpp b/lib/BROSE9323/BROSE9323.cpp index aa72c84..47c2b34 100644 --- a/lib/BROSE9323/BROSE9323.cpp +++ b/lib/BROSE9323/BROSE9323.cpp @@ -65,10 +65,6 @@ void BROSE9323::begin(void) pinMode(ENABLE, OUTPUT); _setData(1); _setData(0); - fillScreen(1); - display(); - fillScreen(0); - display(); } #endif @@ -80,6 +76,11 @@ void BROSE9323::display(bool force) #else if (_direct_mode) return; + if (_force_next) + { + force = true; + _force_next = false; + } for (uint8_t x = 0; x < width(); x++) { _selectPanel(x / _panel_width); @@ -188,6 +189,7 @@ void BROSE9323::fillScreen(uint16_t color) } #if !defined(__AVR_ATmega168P__) && !defined(__AVR_ATmega168PB__) && !defined(__AVR_ATmega168__) memset(_old_buffer, color ? 0xFF : 0x00, _buffer_size); + _force_next = false; #endif } #endif diff --git a/lib/BROSE9323/BROSE9323.h b/lib/BROSE9323/BROSE9323.h index da8cebe..ad842e0 100644 --- a/lib/BROSE9323/BROSE9323.h +++ b/lib/BROSE9323/BROSE9323.h @@ -24,6 +24,8 @@ private: uint8_t _active_col = 255; uint8_t _active_row = 255; 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 const uint8_t ENABLE = 7, diff --git a/src/esp32/README.md b/src/esp32/README.md index 7683fd8..a2ea866 100644 --- a/src/esp32/README.md +++ b/src/esp32/README.md @@ -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 - `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 - 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 off at night or when the display is powered off. diff --git a/src/esp32/libs.h b/src/esp32/libs.h index cb39b11..f4bc75f 100644 --- a/src/esp32/libs.h +++ b/src/esp32/libs.h @@ -44,7 +44,11 @@ HASwitch powerSwitch("FlipDotDisplayPower"); bool power = true; 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 minute = 0; diff --git a/src/esp32/main.cpp b/src/esp32/main.cpp index de96a8e..1ba7236 100644 --- a/src/esp32/main.cpp +++ b/src/esp32/main.cpp @@ -5,6 +5,60 @@ void toggleLED(void *parameter); uint32_t power_off_delay; int triggered_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) { if(triggered_power_off == false) @@ -25,7 +79,7 @@ void updatePower() triggered_power_off = false; // OFF - digitalWrite(relay, HIGH); + relayOff(); if(turn_hassio_power_off) { @@ -46,10 +100,13 @@ void onPowerSwitchStateChanged(bool state, HASwitch *s) if(power == false) { - display.fillScreen(0); - display.display(); + if (relay_on) + { + display.fillScreen(0); + display.display(); - delay(1000); + delay(1000); + } turnPowerOff(0); @@ -230,9 +287,7 @@ void setup() otaPanicMode = false; pinMode(relay, OUTPUT); - digitalWrite(relay, LOW); - - delay(4000); + relayOn(); display.begin(&Serial); // 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) { if(power == true || overide == true) - { - // ON - digitalWrite(relay, LOW); - delay(3000); - } + relayOn(); } void loop() @@ -434,8 +485,11 @@ void loop() if(hour <= 7 || (hour >= 23 && minute > 30)) { - display.fillScreen(0); - display.display(); + if (relay_on) + { + display.fillScreen(0); + display.display(); + } turnPowerOff(); } @@ -446,28 +500,32 @@ void loop() { turnPowerOn(); - int xoffset = 3; + // power can be off (Home Assistant power switch): then nothing is sent + if (relay_on) + { + int xoffset = 3; - display.fillScreen(0); - // display.setTextSize(2); - display.setTextSize(1); - // display.setCursor(7, 3); - display.setCursor(xoffset + 0, 19 - 2); - display.setTextWrap(false); - // display.print(t); + display.fillScreen(0); + // display.setTextSize(2); + display.setTextSize(1); + // display.setCursor(7, 3); + display.setCursor(xoffset + 0, 19 - 2); + display.setTextWrap(false); + // display.print(t); - // if(minute % 2 == 0) - if(hour >= 17 || hour <= 6) - display.drawBitmap(xoffset + 0, 0, yawning_pikachu_bitmap_yawning_pikachu_black_border, 35, 19, 1); - else - display.drawBitmap(xoffset + 0, 0, pikachu_bitmap_pikachu_black_border, 35, 19, 1); + // if(minute % 2 == 0) + if(hour >= 17 || hour <= 6) + display.drawBitmap(xoffset + 0, 0, yawning_pikachu_bitmap_yawning_pikachu_black_border, 35, 19, 1); + else + display.drawBitmap(xoffset + 0, 0, pikachu_bitmap_pikachu_black_border, 35, 19, 1); - display.drawBitmap(xoffset + 35, 0, clock_thought_bubble_bitmap_clock_thought_bubble, 46, 19, 1); + display.drawBitmap(xoffset + 35, 0, clock_thought_bubble_bitmap_clock_thought_bubble, 46, 19, 1); - display.setCursor(xoffset + 43, (19/2) - 5); - display.print(timeStringBuff); + display.setCursor(xoffset + 43, (19/2) - 5); + display.print(timeStringBuff); - display.display(); + display.display(); + } } else { @@ -476,34 +534,37 @@ void loop() } else { - display.fillScreen(0); - display.drawBitmap(0, 0, nintendo_bitmap_Nintendo_new, 84, 19, 1); + if (relay_on) + { + display.fillScreen(0); + display.drawBitmap(0, 0, nintendo_bitmap_Nintendo_new, 84, 19, 1); - // display.drawBitmap(0, 0, nintendo_bitmap_panda_inverted, 17, 18, 1); - // display.drawBitmap(18, 0, nintendo_bitmap_panda_inverted, 17, 18, 1); - // display.drawBitmap(18*2, 0, nintendo_bitmap_panda_inverted, 17, 18, 1); - // display.drawBitmap(18*3, 0, nintendo_bitmap_panda_inverted, 17, 18, 1); - // display.drawBitmap(18*4, 0, nintendo_bitmap_panda_inverted, 17, 18, 1); + // display.drawBitmap(0, 0, nintendo_bitmap_panda_inverted, 17, 18, 1); + // display.drawBitmap(18, 0, nintendo_bitmap_panda_inverted, 17, 18, 1); + // display.drawBitmap(18*2, 0, nintendo_bitmap_panda_inverted, 17, 18, 1); + // display.drawBitmap(18*3, 0, nintendo_bitmap_panda_inverted, 17, 18, 1); + // display.drawBitmap(18*4, 0, nintendo_bitmap_panda_inverted, 17, 18, 1); - // drawRotatedBitmap(0, 0, nintendo_bitmap_Nintendo_new, 17, 18, 1, 90); + // drawRotatedBitmap(0, 0, nintendo_bitmap_Nintendo_new, 17, 18, 1, 90); - // Text inside border. - // display.drawBitmap(0, 0, nintendo_bitmap_nintendo_border, 84, 19, 1); - // display.setFont(); - // display.setTextSize(1); - // display.setTextWrap(false); - // display.setCursor(5, 4); - // display.print("Nintendo"); + // Text inside border. + // display.drawBitmap(0, 0, nintendo_bitmap_nintendo_border, 84, 19, 1); + // display.setFont(); + // display.setTextSize(1); + // display.setTextWrap(false); + // display.setCursor(5, 4); + // display.print("Nintendo"); - // display.setFont(); - // // display.setTextSize(2); - // display.setTextSize(1); - // display.setCursor(0, 0); - // display.setTextWrap(true); - // // display.print(t); - // display.print("Carla & Jan Kragt"); + // display.setFont(); + // // display.setTextSize(2); + // display.setTextSize(1); + // display.setCursor(0, 0); + // display.setTextWrap(true); + // // display.print(t); + // display.print("Carla & Jan Kragt"); - display.display(); + display.display(); + } turnPowerOff(); } diff --git a/src/nano/README.md b/src/nano/README.md index 963a625..4f6f0cd 100644 --- a/src/nano/README.md +++ b/src/nano/README.md @@ -8,8 +8,11 @@ logic. ## Behaviour -- **Boot:** the Nano loads the flip time from EEPROM and flips the whole display on and - then off. +- **Boot:** the Nano loads the flip time from EEPROM, sets up the pins and sends `READY`. + 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 only the dots that changed. - **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 | | `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): diff --git a/src/nano/main.cpp b/src/nano/main.cpp index 256a570..71daadf 100644 --- a/src/nano/main.cpp +++ b/src/nano/main.cpp @@ -265,6 +265,9 @@ void setup() last_refresh = 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()