Public Access
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:
1 parent
f7ffcf4ab3
commit
6e4d5e152b
8 files changed
+158
-68
No files matched your search
@@ -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`.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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;
|
||||||
|
|||||||
+115
-54
@@ -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)
|
||||||
{
|
{
|
||||||
@@ -46,10 +100,13 @@ void onPowerSwitchStateChanged(bool state, HASwitch *s)
|
|||||||
|
|
||||||
if(power == false)
|
if(power == false)
|
||||||
{
|
{
|
||||||
display.fillScreen(0);
|
if (relay_on)
|
||||||
display.display();
|
{
|
||||||
|
display.fillScreen(0);
|
||||||
|
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()
|
||||||
@@ -434,8 +485,11 @@ void loop()
|
|||||||
|
|
||||||
if(hour <= 7 || (hour >= 23 && minute > 30))
|
if(hour <= 7 || (hour >= 23 && minute > 30))
|
||||||
{
|
{
|
||||||
display.fillScreen(0);
|
if (relay_on)
|
||||||
display.display();
|
{
|
||||||
|
display.fillScreen(0);
|
||||||
|
display.display();
|
||||||
|
}
|
||||||
|
|
||||||
turnPowerOff();
|
turnPowerOff();
|
||||||
}
|
}
|
||||||
@@ -446,28 +500,32 @@ void loop()
|
|||||||
{
|
{
|
||||||
turnPowerOn();
|
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.fillScreen(0);
|
||||||
// display.setTextSize(2);
|
// display.setTextSize(2);
|
||||||
display.setTextSize(1);
|
display.setTextSize(1);
|
||||||
// display.setCursor(7, 3);
|
// display.setCursor(7, 3);
|
||||||
display.setCursor(xoffset + 0, 19 - 2);
|
display.setCursor(xoffset + 0, 19 - 2);
|
||||||
display.setTextWrap(false);
|
display.setTextWrap(false);
|
||||||
// display.print(t);
|
// display.print(t);
|
||||||
|
|
||||||
// if(minute % 2 == 0)
|
// if(minute % 2 == 0)
|
||||||
if(hour >= 17 || hour <= 6)
|
if(hour >= 17 || hour <= 6)
|
||||||
display.drawBitmap(xoffset + 0, 0, yawning_pikachu_bitmap_yawning_pikachu_black_border, 35, 19, 1);
|
display.drawBitmap(xoffset + 0, 0, yawning_pikachu_bitmap_yawning_pikachu_black_border, 35, 19, 1);
|
||||||
else
|
else
|
||||||
display.drawBitmap(xoffset + 0, 0, pikachu_bitmap_pikachu_black_border, 35, 19, 1);
|
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.setCursor(xoffset + 43, (19/2) - 5);
|
||||||
display.print(timeStringBuff);
|
display.print(timeStringBuff);
|
||||||
|
|
||||||
display.display();
|
display.display();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -476,34 +534,37 @@ void loop()
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
display.fillScreen(0);
|
if (relay_on)
|
||||||
display.drawBitmap(0, 0, nintendo_bitmap_Nintendo_new, 84, 19, 1);
|
{
|
||||||
|
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(0, 0, nintendo_bitmap_panda_inverted, 17, 18, 1);
|
||||||
// display.drawBitmap(18, 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*2, 0, nintendo_bitmap_panda_inverted, 17, 18, 1);
|
||||||
// display.drawBitmap(18*3, 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(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.
|
// Text inside border.
|
||||||
// display.drawBitmap(0, 0, nintendo_bitmap_nintendo_border, 84, 19, 1);
|
// display.drawBitmap(0, 0, nintendo_bitmap_nintendo_border, 84, 19, 1);
|
||||||
// display.setFont();
|
// display.setFont();
|
||||||
// display.setTextSize(1);
|
// display.setTextSize(1);
|
||||||
// display.setTextWrap(false);
|
// display.setTextWrap(false);
|
||||||
// display.setCursor(5, 4);
|
// display.setCursor(5, 4);
|
||||||
// display.print("Nintendo");
|
// display.print("Nintendo");
|
||||||
|
|
||||||
// display.setFont();
|
// display.setFont();
|
||||||
// // display.setTextSize(2);
|
// // display.setTextSize(2);
|
||||||
// display.setTextSize(1);
|
// display.setTextSize(1);
|
||||||
// display.setCursor(0, 0);
|
// display.setCursor(0, 0);
|
||||||
// display.setTextWrap(true);
|
// display.setTextWrap(true);
|
||||||
// // display.print(t);
|
// // display.print(t);
|
||||||
// display.print("Carla & Jan Kragt");
|
// display.print("Carla & Jan Kragt");
|
||||||
|
|
||||||
display.display();
|
display.display();
|
||||||
|
}
|
||||||
|
|
||||||
turnPowerOff();
|
turnPowerOff();
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-3
@@ -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):
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in new issue
Block a user