Public Access
Restructure ESP32 firmware into modules, fix bugs, add tests
Structure (src/esp32/): - logic/: DisplayController, schedule, power timer, LED pattern; pure C++ - display/: FlipdotFrames and bitmaps - nano_link/: NanoLink (relay, serial, READY, bootloader) and flash bridge - net/: WiFi/OTA/panic window, Home Assistant - config.h with all settings; libs.h (globals in a header) removed Bug fixes: - Home Assistant switches now report their state back and start as ON - loop() no longer blocks: WiFi.waitForConnectResult() (up to 60 s without WiFi) and getLocalTime() (5 s before NTP sync) were called every iteration - Power switch off now powers off after 1 s even when a 30 s countdown is already running - millis() overflow safe power-off timer; no frame catch-up bursts - No garbage time string before NTP sync; 'Connecting...' stays instead - Frames are only sent when the screen changes instead of every second - Reset reasons are logged once instead of every 100 ms - Removed dead code (unused fonts, WiFiMulti, rotated bitmap, ...) Tests: new test_esp32 environment (26 tests); native renamed to test_nano Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
cefb76c1ad
commit
683a47c055
29 files changed
+1288
-754
No files matched your search
@@ -5,9 +5,9 @@ See README.md for the overview.
|
|||||||
|
|
||||||
## Layout and build
|
## Layout and build
|
||||||
|
|
||||||
- `platformio.ini` has the environments `nano`, `esp32` (OTA to 10.1.1.198) and
|
- `platformio.ini` has the environments `nano`, `nano_ota`, `esp32` (OTA to 10.1.1.198),
|
||||||
`esp32_usb` (COM11). The sources are split with `build_src_filter` (`src/nano/`,
|
`esp32_usb` (COM11), and the PC test environments `test_nano` and `test_esp32`. The
|
||||||
`src/esp32/`).
|
sources are split with `build_src_filter` (`src/nano/`, `src/esp32/`).
|
||||||
- `lib/BROSE9323/` is the shared driver. Its behaviour depends on build flags set in
|
- `lib/BROSE9323/` is the shared driver. Its behaviour depends on build flags set in
|
||||||
`platformio.ini`, not in the header:
|
`platformio.ini`, not in the header:
|
||||||
- `ESPSERIAL` (ESP32): draw calls become serial commands.
|
- `ESPSERIAL` (ESP32): draw calls become serial commands.
|
||||||
@@ -19,36 +19,34 @@ See README.md for the overview.
|
|||||||
- `nano_ota` flashes the Nano over WiFi: `tools/nano_net_upload.py` (STK500v1 over TCP)
|
- `nano_ota` flashes the Nano over WiFi: `tools/nano_net_upload.py` (STK500v1 over TCP)
|
||||||
talks to `NanoFlashBridge` on the ESP32 (port 2323). The uploader first sends
|
talks to `NanoFlashBridge` on the ESP32 (port 2323). The uploader first sends
|
||||||
`FLASH <baud>\n`. The ESP power cycles the Nano (plus an optional reset pulse on
|
`FLASH <baud>\n`. The ESP power cycles the Nano (plus an optional reset pulse on
|
||||||
`nano_reset`), answers `OK\n`, then passes the bytes through at that baud rate.
|
`NANO_RESET_PIN`), answers `OK\n`, then passes the bytes through at that baud rate.
|
||||||
`--baud=auto` tries 57600 (old ATmegaBOOT, which also starts after a power cycle), then
|
`--baud=auto` tries 57600 (old ATmegaBOOT, which also starts after a power cycle), then
|
||||||
115200 (Optiboot, which only starts after a reset-pin reset, so it needs `nano_reset`).
|
115200 (Optiboot, which only starts after a reset-pin reset, so it needs `NANO_RESET_PIN`).
|
||||||
avrdude 6.3's `net:` port is broken on Windows (`ser_drain()` fails), which is why the
|
avrdude 6.3's `net:` port is broken on Windows (`ser_drain()` fails), which is why the
|
||||||
uploader is a Python script.
|
uploader is a Python script.
|
||||||
The uploader only uses the Python standard library. Test it against a simulated
|
The uploader only uses the Python standard library. Test it against a simulated
|
||||||
bootloader before changing it.
|
bootloader before changing it.
|
||||||
- `src/esp32/libs.h` defines globals in the header, so it can only be included from
|
- After any change, build both targets (`pio run`) and run the tests
|
||||||
`main.cpp`. New ESP32 modules must be self-contained (see `nano_flash_bridge.*`).
|
(`pio test -e test_nano -e test_esp32`).
|
||||||
- After any change, build both targets (`pio run`). After Nano changes, also run
|
|
||||||
`pio test -e native`.
|
|
||||||
|
|
||||||
## Constraints
|
## Constraints
|
||||||
|
|
||||||
- **Nano RAM is tight** (ATmega328P, 2 KB). The frame buffers are allocated with
|
- **Nano RAM is tight** (ATmega328P, 2 KB). The frame buffers are allocated with
|
||||||
`calloc`. Avoid `String`, large stack buffers and new globals in Nano code. Put constant
|
`calloc`. Avoid `String`, large stack buffers and new globals in Nano code. Put constant
|
||||||
data in `PROGMEM`.
|
data in `PROGMEM`.
|
||||||
- The ESP32 talks to the Nano on `Serial2` (`nanoSerial` in `libs.h`, GPIO 17 TX / GPIO 16
|
- The ESP32 talks to the Nano on `Serial2` (owned by `NanoLink`, GPIO 17 TX / GPIO 16 RX).
|
||||||
RX). `Serial` (UART0/USB) is free for debug output. Never write debug text to
|
`Serial` (UART0/USB) is free for debug output. Never write debug text to `Serial2`: the
|
||||||
`nanoSerial`: the Nano runs lines that start with s/S/F/D/P/B/R/T as commands.
|
Nano runs lines that start with s/S/F/D/P/B/R/T as commands.
|
||||||
- Open hardware TODOs are listed in the README `TODO` section. Update it when one is done.
|
- Open hardware TODOs are listed in the README `TODO` section. Update it when one is done.
|
||||||
- `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 and then sends `READY`. The ESP's `relayOn()` waits for
|
switches on, the Nano cold-boots and then sends `READY`. `NanoLink::on()` waits for
|
||||||
that message, at most `NANO_BOOT_TIMEOUT`. The timeout also covers the case where the
|
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.
|
Nano's TX isn't wired to the ESP's RX.
|
||||||
- `relayOff()` ends `nanoSerial` and sets its TX pin to input. Otherwise the idle-high TX line
|
- `NanoLink::off()` ends `Serial2` and sets its TX pin to input. Otherwise the idle-high TX
|
||||||
powers the Nano through its RX pin. Never send display commands while `relay_on` is
|
line powers the Nano through its RX pin. Never send display commands while
|
||||||
false.
|
`NanoLink::isOn()` is false (`DisplayController` takes care of this).
|
||||||
- The Nano keeps no state over a power cycle except the flip time in EEPROM. The driver
|
- 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`).
|
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.
|
||||||
@@ -62,19 +60,36 @@ See README.md for the overview.
|
|||||||
while it arrives, so it can be longer than the buffer. Keep `loop()` free of busy-waits.
|
while it arrives, so it can be longer than the buffer. Keep `loop()` free of busy-waits.
|
||||||
- A full redraw (`display(true)`) blocks for about 0.5 s, which overflows the 64-byte
|
- A full redraw (`display(true)`) blocks for about 0.5 s, which overflows the 64-byte
|
||||||
serial receive buffer. Only run it when no data is expected: right after a `D`, or after
|
serial receive buffer. Only run it when no data is expected: right after a `D`, or after
|
||||||
2 s with no serial data. The ESP sends one frame per second, ending with `D`.
|
2 s with no serial data. The ESP only sends a frame (ending with `D`) when the screen
|
||||||
|
changes, mostly once a minute.
|
||||||
- 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 only replies `READY` (at boot) and `ERR: ...`. There is no per-frame
|
- 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
|
acknowledgement. Whether the Nano's TX is wired to the ESP's RX is unconfirmed, so every
|
||||||
wait for a reply needs a timeout.
|
wait for a reply needs a timeout.
|
||||||
- Unit tests: `pio test -e native` runs `test/test_nano/` on the PC with the fakes in
|
- Unit tests: `pio test -e test_nano` runs `test/test_nano/` on the PC with the fakes in
|
||||||
`test/mocks/` (Arduino, EEPROM, and a `BROSE9323` that records calls). It needs gcc on
|
`test/mocks/` (Arduino, EEPROM, and a `BROSE9323` that records calls). It needs gcc on
|
||||||
PATH (Strawberry Perl provides it at `C:\Strawberry\c\bin`). Add a test for every
|
PATH (Strawberry Perl provides it at `C:\Strawberry\c\bin`). Add a test for every
|
||||||
protocol change. The mocks only implement what the Nano code uses, so extend them when
|
protocol change. The mocks only implement what the Nano code uses, so extend them when
|
||||||
new Arduino or driver functions are called.
|
new Arduino or driver functions are called.
|
||||||
- The board environments have `test_ignore = *`, so test firmware is never uploaded. Plain
|
- The board environments have `test_ignore = *`, so test firmware is never uploaded. Plain
|
||||||
`pio test` runs nothing; always pass `-e native`.
|
`pio test` runs nothing; always pass `-e test_nano -e test_esp32`.
|
||||||
|
|
||||||
|
## ESP32 structure (`src/esp32/`)
|
||||||
|
|
||||||
|
- Folders: `logic/` (pure C++, no Arduino includes, tested in `test/test_esp32`),
|
||||||
|
`display/` (frames and bitmaps), `nano_link/` (relay, serial, flash bridge), `net/`
|
||||||
|
(WiFi/OTA, Home Assistant). Includes are relative to `src/esp32` (`-I src/esp32`), e.g.
|
||||||
|
`#include "logic/schedule.h"`.
|
||||||
|
- `DisplayController` holds the display logic and only uses the `DisplayPower` and
|
||||||
|
`Frames` interfaces. Put new behaviour there, behind those interfaces, so it stays
|
||||||
|
testable. `logic/` must not include Arduino headers, or `test_esp32` stops building.
|
||||||
|
- All settings are in `config.h`. Keep it free of Arduino types for the same reason.
|
||||||
|
- Frames are only sent when the screen changes. The schedule uses 08:00 as the start of
|
||||||
|
the day on purpose (that is what the old code did, and the README documents it).
|
||||||
|
- Home Assistant switches are not optimistic: callbacks must call `sender->setState()`.
|
||||||
|
The unique ids `FlipDotDisplayClock` / `FlipDotDisplayPower` and `HA_DEVICE_ID` must stay
|
||||||
|
the same, or Home Assistant creates new entities.
|
||||||
|
|
||||||
## Git
|
## Git
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,8 @@ src/nano/ Arduino Nano firmware
|
|||||||
src/esp32/ ESP32 firmware
|
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/test_esp32/ unit tests for the ESP32 logic (run on the PC)
|
||||||
|
test/mocks/ fake Arduino, EEPROM and display driver for the Nano tests
|
||||||
tools/nano_net_upload.py uploader for flashing the Nano over WiFi
|
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
|
||||||
@@ -69,7 +70,7 @@ 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
|
||||||
pio test -e native # run the Nano unit tests on the PC
|
pio test -e test_nano -e test_esp32 # run the unit tests on the PC
|
||||||
```
|
```
|
||||||
|
|
||||||
The unit tests need `gcc`/`g++` on your PATH. On Windows you can get it from
|
The unit tests need `gcc`/`g++` on your PATH. On Windows you can get it from
|
||||||
@@ -126,8 +127,8 @@ By default (`--baud=auto` in `upload_flags`) the uploader tries 57600 first and
|
|||||||
If you ever replace the bootloader with Optiboot:
|
If you ever replace the bootloader with Optiboot:
|
||||||
|
|
||||||
- Fit the reset wire: ESP32 GPIO → 1 kΩ → base of an NPN transistor (e.g. 2N2222 or
|
- Fit the reset wire: ESP32 GPIO → 1 kΩ → base of an NPN transistor (e.g. 2N2222 or
|
||||||
BC547). Connect the collector to Nano RESET and the emitter to GND. Set `nano_reset` in
|
BC547). Connect the collector to Nano RESET and the emitter to GND. Set `NANO_RESET_PIN` in
|
||||||
`src/esp32/libs.h` to that GPIO, for example `4`. The transistor keeps the Nano's 5 V
|
`src/esp32/config.h` to that GPIO, for example `4`. The transistor keeps the Nano's 5 V
|
||||||
reset line away from the 3.3 V ESP32 pin.
|
reset line away from the 3.3 V ESP32 pin.
|
||||||
- For uploads over USB, change `board` in the `nano` environment to `nanoatmega328new`.
|
- For uploads over USB, change `board` in the `nano` environment to `nanoatmega328new`.
|
||||||
|
|
||||||
@@ -136,8 +137,8 @@ If you ever replace the bootloader with Optiboot:
|
|||||||
- ESP32 IP address: `upload_port` in the `nano_ota` environment in `platformio.ini`. You
|
- 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
|
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`.
|
`pio run -e nano_ota -t upload --upload-port 10.1.1.50`.
|
||||||
- Port, reset pin and timings: `NANO_FLASH_PORT`, `nano_reset`, `NANO_POWER_OFF_TIME` and
|
- Port, reset pin and timings: `NANO_FLASH_PORT`, `NANO_RESET_PIN`, `NANO_POWER_OFF_TIME`
|
||||||
`NANO_RESET_DELAY` in `src/esp32/libs.h`.
|
and `NANO_RESET_DELAY` in `src/esp32/config.h`.
|
||||||
|
|
||||||
**If it fails**
|
**If it fails**
|
||||||
|
|
||||||
@@ -163,7 +164,7 @@ The ESP32 talks to the Nano on **UART2** (`Serial2`, 115200 baud). The ESP32's o
|
|||||||
|-------|------|-------|
|
|-------|------|-------|
|
||||||
| GPIO 17 (TX2) | D0 (RX) | Display commands |
|
| GPIO 17 (TX2) | D0 (RX) | Display commands |
|
||||||
| 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. |
|
| 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. |
|
||||||
| GPIO set in `nano_reset` | RESET (through an NPN transistor) | Optional, only needed to flash a Nano with Optiboot over WiFi. See [Bootloaders](#bootloaders). |
|
| GPIO set in `NANO_RESET_PIN` | RESET (through an NPN transistor) | Optional, only needed to flash a Nano with Optiboot over WiFi. See [Bootloaders](#bootloaders). |
|
||||||
| 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) |
|
||||||
|
|
||||||
@@ -172,8 +173,10 @@ The ESP32 talks to the Nano on **UART2** (`Serial2`, 115200 baud). The ESP32's o
|
|||||||
1. **ESP32 boot.** The ESP32 connects to WiFi and starts ArduinoOTA. After a crash or
|
1. **ESP32 boot.** The ESP32 connects to WiFi and starts ArduinoOTA. After a crash or
|
||||||
unexpected reset it stays in an *OTA panic window* for 15 seconds, so you can flash a
|
unexpected reset it stays in an *OTA panic window* for 15 seconds, so you can flash a
|
||||||
fix before the main code runs again. It then switches the display relay on, waits for
|
fix before the main code runs again. It then switches the display relay on, waits for
|
||||||
the Nano's `READY` message (at most 3 s), starts NTP and connects to MQTT.
|
the Nano's `READY` message (at most 3 s), shows "Connecting...", starts NTP and
|
||||||
2. **Clock loop (every second).**
|
connects to MQTT.
|
||||||
|
2. **Clock.** Once the time is synced, the ESP32 sends a new frame whenever the screen
|
||||||
|
changes, mostly once a minute:
|
||||||
- **08:00–22:00:** Pikachu with a thought bubble showing `HH:MM`. A yawning Pikachu is
|
- **08:00–22:00:** Pikachu with a thought bubble showing `HH:MM`. A yawning Pikachu is
|
||||||
shown from 17:00.
|
shown from 17:00.
|
||||||
- **22:00–23:30:** the clock is only shown during the minutes :00, :15, :30 and :45.
|
- **22:00–23:30:** the clock is only shown during the minutes :00, :15, :30 and :45.
|
||||||
|
|||||||
+20
-7
@@ -4,13 +4,14 @@
|
|||||||
; nano_ota Same Nano firmware, flashed over WiFi through the ESP32
|
; 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)
|
; test_nano Unit tests for the Nano firmware, run on the PC (needs gcc on PATH)
|
||||||
|
; test_esp32 Unit tests for the ESP32 logic, run on the PC
|
||||||
;
|
;
|
||||||
; Build all: pio run
|
; Build all: pio run
|
||||||
; Build one: pio run -e nano
|
; Build one: pio run -e nano
|
||||||
; Upload one: pio run -e nano -t upload
|
; Upload one: pio run -e nano -t upload
|
||||||
; Serial monitor: pio device monitor -e nano
|
; Serial monitor: pio device monitor -e nano
|
||||||
; Run tests: pio test -e native
|
; Run tests: pio test -e test_nano -e test_esp32
|
||||||
;
|
;
|
||||||
; https://docs.platformio.org/page/projectconf.html
|
; https://docs.platformio.org/page/projectconf.html
|
||||||
|
|
||||||
@@ -22,7 +23,7 @@ framework = arduino
|
|||||||
monitor_speed = 115200
|
monitor_speed = 115200
|
||||||
lib_deps =
|
lib_deps =
|
||||||
adafruit/Adafruit GFX Library@^1.10.12
|
adafruit/Adafruit GFX Library@^1.10.12
|
||||||
; Tests only run in the native environment, never on the boards
|
; Tests only run in the test_* environments on the PC, never on the boards
|
||||||
test_ignore = *
|
test_ignore = *
|
||||||
|
|
||||||
; ---------------------------------------------------------------------------
|
; ---------------------------------------------------------------------------
|
||||||
@@ -58,7 +59,7 @@ board = esp32doit-devkit-v1
|
|||||||
build_src_filter = +<esp32/>
|
build_src_filter = +<esp32/>
|
||||||
; ESPSERIAL turns the BROSE9323 driver into a serial client: every draw call is
|
; ESPSERIAL turns the BROSE9323 driver into a serial client: every draw call is
|
||||||
; sent to the Nano as a text command instead of toggling GPIO pins.
|
; sent to the Nano as a text command instead of toggling GPIO pins.
|
||||||
build_flags = -D ESPSERIAL
|
build_flags = -D ESPSERIAL -I src/esp32
|
||||||
lib_deps =
|
lib_deps =
|
||||||
${env.lib_deps}
|
${env.lib_deps}
|
||||||
https://github.com/robjen/GFX_fonts.git
|
https://github.com/robjen/GFX_fonts.git
|
||||||
@@ -74,15 +75,27 @@ upload_protocol = esptool
|
|||||||
upload_port = COM11
|
upload_port = COM11
|
||||||
|
|
||||||
; ---------------------------------------------------------------------------
|
; ---------------------------------------------------------------------------
|
||||||
; Native - unit tests for the Nano firmware on the PC (test/test_nano)
|
; Unit tests on the PC (pio test -e test_nano -e test_esp32)
|
||||||
; The Arduino core, EEPROM and the display driver are replaced by test/mocks.
|
|
||||||
; ---------------------------------------------------------------------------
|
; ---------------------------------------------------------------------------
|
||||||
[env:native]
|
[native]
|
||||||
platform = native
|
platform = native
|
||||||
framework =
|
framework =
|
||||||
lib_deps =
|
lib_deps =
|
||||||
lib_ignore = BROSE9323
|
lib_ignore = BROSE9323
|
||||||
test_ignore =
|
test_ignore =
|
||||||
test_build_src = yes
|
test_build_src = yes
|
||||||
|
|
||||||
|
; Nano firmware (test/test_nano). The Arduino core, EEPROM and the display driver are
|
||||||
|
; replaced by test/mocks.
|
||||||
|
[env:test_nano]
|
||||||
|
extends = native
|
||||||
|
test_filter = test_nano
|
||||||
build_src_filter = +<nano/>
|
build_src_filter = +<nano/>
|
||||||
build_flags = -std=gnu++17 -I test/mocks
|
build_flags = -std=gnu++17 -I test/mocks
|
||||||
|
|
||||||
|
; ESP32 logic (test/test_esp32): only the modules without Arduino dependencies
|
||||||
|
[env:test_esp32]
|
||||||
|
extends = native
|
||||||
|
test_filter = test_esp32
|
||||||
|
build_src_filter = +<esp32/logic/>
|
||||||
|
build_flags = -std=gnu++17 -I src/esp32
|
||||||
+52
-23
@@ -1,6 +1,6 @@
|
|||||||
# ESP32: controller
|
# ESP32: controller
|
||||||
|
|
||||||
PlatformIO environments: `esp32` (OTA) and `esp32_usb` (USB)
|
PlatformIO environments: `esp32` (OTA) and `esp32_usb` (USB). Tests: `test_esp32`.
|
||||||
|
|
||||||
The ESP32 decides what is shown on the display. It never touches the panel itself. The
|
The ESP32 decides what is shown on the display. It never touches the panel itself. The
|
||||||
BROSE9323 driver is built with `-D ESPSERIAL`, so every GFX call is sent to the Nano over
|
BROSE9323 driver is built with `-D ESPSERIAL`, so every GFX call is sent to the Nano over
|
||||||
@@ -11,35 +11,55 @@ debug output. Wiring is described in the [main README](../../README.md#wiring-be
|
|||||||
|
|
||||||
| File | Contents |
|
| File | Contents |
|
||||||
|------|----------|
|
|------|----------|
|
||||||
| `main.cpp` | setup, OTA, clock / power logic, Home Assistant callbacks |
|
| `main.cpp` | Creates the objects, `setup()` and `loop()` |
|
||||||
| `libs.h` | includes and global objects (display, MQTT, WiFi, pins, timezone) |
|
| `config.h` | All settings: pins, timings, timezone, Home Assistant device id |
|
||||||
| `bitmaps.h` | 1-bit bitmaps (Pikachu, thought bubble, Nintendo logo, ...) |
|
| `credentials.h` | Includes `secrets.h` (WiFi and MQTT credentials, **gitignored**); template: `secrets.example.h` |
|
||||||
| `nano_flash_bridge.h/.cpp` | WiFi to serial bridge for flashing the Nano over WiFi (TCP port 2323) |
|
| **`logic/`** | **Pure logic without Arduino code, tested on the PC** |
|
||||||
| `secrets.example.h` | template for `secrets.h` (WiFi and MQTT credentials, **gitignored**) |
|
| `logic/display_controller.*` | Decides what is shown and when the display is powered (`DisplayController`) |
|
||||||
|
| `logic/schedule.*` | When the clock is shown (night, clock, standby) |
|
||||||
|
| `logic/power_timer.*` | Countdown to switching the display off |
|
||||||
|
| `logic/status_led.h` | Blink pattern of the built-in LED |
|
||||||
|
| **`display/`** | **What is drawn** |
|
||||||
|
| `display/frames.*` | The screens: clock, logo, blank, text (`FlipdotFrames`) |
|
||||||
|
| `display/bitmaps.h` | 1-bit bitmaps (Pikachu, thought bubble, Nintendo logo, ...) |
|
||||||
|
| **`nano_link/`** | **Connection to the Nano** |
|
||||||
|
| `nano_link/nano_link.*` | Relay and serial link: power on (waits for `READY`), power off, bootloader start (`NanoLink`) |
|
||||||
|
| `nano_link/nano_flash_bridge.*` | WiFi to serial bridge for flashing the Nano over WiFi (TCP port 2323) |
|
||||||
|
| **`net/`** | **Network** |
|
||||||
|
| `net/wifi_ota.*` | WiFi, ESP32 OTA updates and the OTA panic window |
|
||||||
|
| `net/home_assistant.*` | MQTT and the two Home Assistant switches |
|
||||||
|
|
||||||
|
`DisplayController` only talks to two small interfaces, `DisplayPower` (implemented by
|
||||||
|
`NanoLink`) and `Frames` (implemented by `FlipdotFrames`). The tests replace both with
|
||||||
|
fakes.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
- **Frames only when something changes:** a new frame is sent when the minute changes, the
|
||||||
|
screen changes (clock, logo, blank) or the Nano was just powered on. The serial line is
|
||||||
|
quiet in between, which also gives the Nano time for its periodic full redraw.
|
||||||
- **OTA updates** through ArduinoOTA. Hostname: `FlipDotDisplay`.
|
- **OTA updates** through ArduinoOTA. Hostname: `FlipDotDisplay`.
|
||||||
- **OTA panic window:** after a crash or watchdog reset, the ESP32 runs only the OTA
|
- **OTA panic window:** after a crash or watchdog reset, the ESP32 runs only the OTA
|
||||||
handler for 15 seconds before it starts the main code. A firmware that crashes on boot
|
handler for 15 seconds before it starts the main code, with the LED blinking fast. A
|
||||||
can still be replaced over the air.
|
firmware that crashes on boot can still be replaced over the air.
|
||||||
- **NTP time** with timezone `CET-1CEST,M3.5.0,M10.5.0/3` (Europe/Amsterdam).
|
- **NTP time** with timezone `CET-1CEST,M3.5.0,M10.5.0/3` (Europe/Amsterdam). Until the
|
||||||
- **Home Assistant** through MQTT (`home-assistant-integration` library) with two switches:
|
time is synced, the display shows "Connecting...".
|
||||||
- `FlipDotDisplayClock`: clock on, or the Nintendo logo when off
|
- **Home Assistant** through MQTT (`home-assistant-integration` library) with two switches.
|
||||||
- `FlipDotDisplayPower`: turns the display off through the relay
|
Each switch reports its new state back, so Home Assistant always shows the real state.
|
||||||
|
- `FlipDot Display Clock`: on = clock; off = Nintendo logo while the display is on,
|
||||||
|
then power off after 30 s
|
||||||
|
- `FlipDot Display Power`: off = clear the display and power it off after 1 s
|
||||||
- **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.
|
on.
|
||||||
- **On:** the ESP waits for the Nano's `READY` message, at most 3 s
|
- **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;
|
(`NANO_BOOT_TIMEOUT`), before it sends frames.
|
||||||
the ESP only waits when it actually switches the relay on.
|
- **Off:** the ESP releases its TX pin (`Serial2.end()` and the pin set to input). An
|
||||||
- **Off:** the ESP stops sending and releases its TX pin (`Serial.end()` and the pin set
|
idle UART line is high and would otherwise power the Nano through its RX pin.
|
||||||
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.
|
- **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
|
While a flash session runs, the clock is paused. See the
|
||||||
[main README](../../README.md#flashing-the-nano-over-wifi).
|
[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): off while the display is off, slow blink with WiFi, fast blink
|
||||||
off at night or when the display is powered off.
|
without WiFi.
|
||||||
|
|
||||||
## Schedule
|
## Schedule
|
||||||
|
|
||||||
@@ -47,11 +67,20 @@ debug output. Wiring is described in the [main README](../../README.md#wiring-be
|
|||||||
|------|---------|
|
|------|---------|
|
||||||
| 08:00 – 17:00 | Pikachu and the clock, updated every minute |
|
| 08:00 – 17:00 | Pikachu and the clock, updated every minute |
|
||||||
| 17:00 – 22:00 | Yawning Pikachu and the clock |
|
| 17:00 – 22:00 | Yawning Pikachu and the clock |
|
||||||
| 22:00 – 23:30 | Clock shown only during the minutes :00, :15, :30 and :45; power is off in between |
|
| 22:00 – 23:30 | Clock shown only during the minutes :00, :15, :30 and :45. In between the power is off, and the last time stays visible (flipdots keep their image without power). |
|
||||||
| 23:31 – 08:00 | Screen cleared, power off |
|
| 23:31 – 08:00 | Screen cleared, power off after 30 s |
|
||||||
|
|
||||||
|
The times are in `logic/schedule.cpp`.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
`pio test -e test_esp32` runs [test/test_esp32](../../test/test_esp32/test_main.cpp) on
|
||||||
|
the PC: the schedule boundaries, the power timer (including `millis()` overflow), the LED
|
||||||
|
pattern and the display controller (power on/off, redraws, both Home Assistant switches).
|
||||||
|
|
||||||
## Adding bitmaps
|
## Adding bitmaps
|
||||||
|
|
||||||
Convert an image with [image2cpp](https://javl.github.io/image2cpp/) to "Arduino code",
|
Convert an image with [image2cpp](https://javl.github.io/image2cpp/) to "Arduino code",
|
||||||
horizontal, 1 bit per pixel. Paste the array into `bitmaps.h` and draw it with
|
horizontal, 1 bit per pixel. Paste the array into `display/bitmaps.h` and draw it in
|
||||||
`display.drawBitmap(x, y, bitmap, w, h, 1)`. The display is 84 × 19 pixels.
|
`display/frames.cpp` with `_display.drawBitmap(x, y, bitmap, w, h, 1)`. The display is
|
||||||
|
84 × 19 pixels.
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// All ESP32 settings. Credentials are in secrets.h (see secrets.example.h).
|
||||||
|
// Kept free of Arduino types so the logic modules can be tested on the PC.
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Pins
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const int LED_PIN = 2; // built-in LED of the ESP32 DevKit v1
|
||||||
|
const int RELAY_PIN = 13; // powers the panels and the Nano (LOW = on)
|
||||||
|
|
||||||
|
// Link to the Nano on UART2. Serial (UART0) stays free for USB / debug output.
|
||||||
|
const int NANO_RX_PIN = 16; // wired to the Nano TX (through a 5V -> 3.3V divider)
|
||||||
|
const int NANO_TX_PIN = 17; // wired to the Nano RX
|
||||||
|
|
||||||
|
// Optional: GPIO that resets the Nano through an NPN transistor (HIGH = reset), -1 = not wired.
|
||||||
|
// Needed to flash a Nano with the Optiboot bootloader, which doesn't start after a power cycle.
|
||||||
|
const int NANO_RESET_PIN = -1;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Display and Nano
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const uint8_t DISPLAY_WIDTH = 84;
|
||||||
|
const uint8_t DISPLAY_HEIGHT = 19;
|
||||||
|
const uint8_t PANEL_WIDTH = 28;
|
||||||
|
|
||||||
|
const uint16_t FLIP_TIME = 225; // flip pulse length in us, sent to the Nano at boot
|
||||||
|
|
||||||
|
const uint32_t NANO_BAUD = 115200;
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
// After a frame is cleared, wait this long before cutting the power, so the dots can flip
|
||||||
|
const uint32_t CLEAR_BEFORE_OFF_TIME = 1000;
|
||||||
|
// Power off this long after the display is no longer needed (night, standby, logo)
|
||||||
|
const uint32_t POWER_OFF_DELAY = 30000;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Flashing the Nano over WiFi (see README.md, pio run -e nano_ota -t upload)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const uint16_t NANO_FLASH_PORT = 2323;
|
||||||
|
// The bootloader baud rate is chosen by the uploader (57600 old bootloader, 115200 Optiboot)
|
||||||
|
const uint32_t NANO_POWER_OFF_TIME = 1000; // relay off time to make sure the Nano resets
|
||||||
|
const uint32_t NANO_RESET_DELAY = 200; // after power on, before the reset pulse
|
||||||
|
const uint32_t NANO_RESET_PULSE = 50;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Network, time and Home Assistant
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#define HOSTNAME "FlipDotDisplay"
|
||||||
|
|
||||||
|
// Europe/Amsterdam
|
||||||
|
#define TIMEZONE "CET-1CEST,M3.5.0,M10.5.0/3"
|
||||||
|
#define NTP_SERVER "pool.ntp.org"
|
||||||
|
|
||||||
|
// After a crash or unexpected reset, only run OTA for this long before starting the
|
||||||
|
// main code, so a firmware that crashes at boot can still be replaced
|
||||||
|
const uint32_t OTA_PANIC_WINDOW = 15000;
|
||||||
|
|
||||||
|
// Home Assistant device id (do not change: Home Assistant's entities are linked to it)
|
||||||
|
const uint8_t HA_DEVICE_ID[] = {0x34, 0x86, 0x5d, 0x3a, 0x08, 0x28};
|
||||||
|
#define HA_DEVICE_NAME "FlipDotDisplay"
|
||||||
|
#define HA_SOFTWARE_VERSION "2.0.0"
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// WiFi / MQTT credentials from secrets.h, which is gitignored
|
||||||
|
#if __has_include("secrets.h")
|
||||||
|
#include "secrets.h"
|
||||||
|
#else
|
||||||
|
#error "Missing src/esp32/secrets.h - copy src/esp32/secrets.example.h to secrets.h and fill it in"
|
||||||
|
#endif
|
||||||
File renamed without changes.
@@ -0,0 +1,55 @@
|
|||||||
|
#include "display/frames.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
#include "display/bitmaps.h"
|
||||||
|
#include "logic/schedule.h"
|
||||||
|
|
||||||
|
// Horizontal offset that centers the 81 pixel wide clock on the 84 pixel display
|
||||||
|
static const int CLOCK_X = 3;
|
||||||
|
|
||||||
|
FlipdotFrames::FlipdotFrames(BROSE9323 &display) : _display(display)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void FlipdotFrames::clock(int hour, int minute)
|
||||||
|
{
|
||||||
|
char time[6];
|
||||||
|
snprintf(time, sizeof(time), "%02d:%02d", hour, minute);
|
||||||
|
|
||||||
|
_display.fillScreen(0);
|
||||||
|
if (isEvening(hour))
|
||||||
|
_display.drawBitmap(CLOCK_X, 0, yawning_pikachu_bitmap_yawning_pikachu_black_border, 35, 19, 1);
|
||||||
|
else
|
||||||
|
_display.drawBitmap(CLOCK_X, 0, pikachu_bitmap_pikachu_black_border, 35, 19, 1);
|
||||||
|
_display.drawBitmap(CLOCK_X + 35, 0, clock_thought_bubble_bitmap_clock_thought_bubble, 46, 19, 1);
|
||||||
|
|
||||||
|
_display.setTextSize(1);
|
||||||
|
_display.setTextWrap(false);
|
||||||
|
_display.setCursor(CLOCK_X + 43, 4);
|
||||||
|
_display.print(time);
|
||||||
|
_display.display();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FlipdotFrames::logo()
|
||||||
|
{
|
||||||
|
_display.fillScreen(0);
|
||||||
|
_display.drawBitmap(0, 0, nintendo_bitmap_Nintendo_new, 84, 19, 1);
|
||||||
|
_display.display();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FlipdotFrames::blank()
|
||||||
|
{
|
||||||
|
_display.fillScreen(0);
|
||||||
|
_display.display();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FlipdotFrames::message(const char *text)
|
||||||
|
{
|
||||||
|
_display.fillScreen(0);
|
||||||
|
_display.setTextSize(1);
|
||||||
|
_display.setTextWrap(true);
|
||||||
|
_display.setCursor(0, 0);
|
||||||
|
_display.print(text);
|
||||||
|
_display.display();
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "BROSE9323.h"
|
||||||
|
#include "logic/display_controller.h"
|
||||||
|
|
||||||
|
// Draws the screens on the flipdot display. Each method sends one complete frame.
|
||||||
|
class FlipdotFrames : public Frames
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit FlipdotFrames(BROSE9323 &display);
|
||||||
|
|
||||||
|
// Pikachu with a thought bubble showing HH:MM (yawning in the evening)
|
||||||
|
void clock(int hour, int minute) override;
|
||||||
|
// Nintendo logo
|
||||||
|
void logo() override;
|
||||||
|
void blank() override;
|
||||||
|
// A line of text, e.g. while connecting
|
||||||
|
void message(const char *text);
|
||||||
|
|
||||||
|
private:
|
||||||
|
BROSE9323 &_display;
|
||||||
|
};
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
#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")
|
|
||||||
#include "secrets.h"
|
|
||||||
#else
|
|
||||||
#error "Missing src/esp32/secrets.h - copy src/esp32/secrets.example.h to secrets.h and fill it in"
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// fixes
|
|
||||||
#include <SPI.h>
|
|
||||||
#include <Adafruit_I2CDevice.h>
|
|
||||||
// fonts
|
|
||||||
#include <Fonts/FreeSerif12pt7b.h>
|
|
||||||
#include <Fonts/FreeSerifItalic12pt7b.h>
|
|
||||||
// #include <Font2x5FixedMonoNum.h>
|
|
||||||
|
|
||||||
#include <WiFi.h>
|
|
||||||
#include <ESPmDNS.h>
|
|
||||||
#include <WiFiUdp.h>
|
|
||||||
#include <ArduinoOTA.h>
|
|
||||||
#include <WiFiMulti.h>
|
|
||||||
|
|
||||||
// wdt?
|
|
||||||
#include <esp_task_wdt.h>
|
|
||||||
#include <rom/rtc.h>
|
|
||||||
|
|
||||||
// hassio
|
|
||||||
#include <ArduinoHA.h>
|
|
||||||
|
|
||||||
byte mac[] = {0x34, 0x86, 0x5d, 0x3a, 0x08, 0x28};
|
|
||||||
|
|
||||||
// EthernetClient client;
|
|
||||||
WiFiClient client;
|
|
||||||
HADevice device(mac, sizeof(mac));
|
|
||||||
HAMqtt mqtt(client, device);
|
|
||||||
|
|
||||||
HASwitch flipDotDisplay("FlipDotDisplayClock");
|
|
||||||
bool hassioDisplayStatus = true;
|
|
||||||
|
|
||||||
HASwitch powerSwitch("FlipDotDisplayPower");
|
|
||||||
bool power = true;
|
|
||||||
|
|
||||||
const int led1 = BUILTIN_LED; // Pin of the LED
|
|
||||||
const int relay = 13; // Pin of the relay, powers the panels and the Nano (LOW = on)
|
|
||||||
// Link to the Nano on UART2. Serial (UART0) stays free for USB / debug output.
|
|
||||||
HardwareSerial &nanoSerial = Serial2;
|
|
||||||
const int nano_rx = 16; // UART2 RX, wired to the Nano TX (through a 5V -> 3.3V divider)
|
|
||||||
const int nano_tx = 17; // UART2 TX, wired to the Nano RX
|
|
||||||
// Optional: GPIO that resets the Nano through an NPN transistor (HIGH = reset), -1 = not wired.
|
|
||||||
// Needed to flash a Nano with the Optiboot bootloader, which doesn't start after a power cycle.
|
|
||||||
const int nano_reset = -1;
|
|
||||||
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;
|
|
||||||
// The bootloader baud rate is chosen by the uploader (57600 old bootloader, 115200 Optiboot)
|
|
||||||
const uint32_t NANO_POWER_OFF_TIME = 1000; // relay off time to make sure the Nano resets
|
|
||||||
const uint32_t NANO_RESET_DELAY = 200; // after power on, before the reset pulse
|
|
||||||
const uint32_t NANO_RESET_PULSE = 50;
|
|
||||||
|
|
||||||
int hour = 0;
|
|
||||||
int minute = 0;
|
|
||||||
int second = 0;
|
|
||||||
|
|
||||||
//
|
|
||||||
WiFiMulti wifiMulti;
|
|
||||||
|
|
||||||
const char *ssid = WIFI_SSID;
|
|
||||||
const char *password = WIFI_PASSWORD;
|
|
||||||
|
|
||||||
BROSE9323 display(84, 19, 28);
|
|
||||||
|
|
||||||
uint32_t next_display;
|
|
||||||
|
|
||||||
// ntp
|
|
||||||
#define MYTZ "CET-1CEST,M3.5.0,M10.5.0/3"
|
|
||||||
const char *ntpServer = "pool.ntp.org";
|
|
||||||
const long gmtOffset_sec = 3600;
|
|
||||||
const int daylightOffset_sec = 3600;
|
|
||||||
// const int daylightOffset_sec = 0;
|
|
||||||
|
|
||||||
bool wifiStatus = false;
|
|
||||||
bool otaPanicMode = true;
|
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
#include "logic/display_controller.h"
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
#include "logic/schedule.h"
|
||||||
|
|
||||||
|
DisplayController::DisplayController(DisplayPower &power, Frames &frames) : _power(power), _frames(frames)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void DisplayController::setClockEnabled(bool enabled)
|
||||||
|
{
|
||||||
|
_clock_enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DisplayController::setPowerEnabled(bool enabled)
|
||||||
|
{
|
||||||
|
_power_enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DisplayController::clockEnabled() const
|
||||||
|
{
|
||||||
|
return _clock_enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DisplayController::powerEnabled() const
|
||||||
|
{
|
||||||
|
return _power_enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DisplayController::update(uint32_t now, bool time_valid, int hour, int minute)
|
||||||
|
{
|
||||||
|
// Switched off elsewhere (power timer, Nano flashing): the Nano lost its frame
|
||||||
|
if (!_power.isOn())
|
||||||
|
_screen = Screen::Unknown;
|
||||||
|
|
||||||
|
if (_off_timer.expired(now))
|
||||||
|
{
|
||||||
|
_power.off();
|
||||||
|
_screen = Screen::Unknown;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_power_enabled)
|
||||||
|
{
|
||||||
|
if (_power.isOn())
|
||||||
|
{
|
||||||
|
_showBlank();
|
||||||
|
_off_timer.request(now, CLEAR_BEFORE_OFF_TIME);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep the boot screen until the time is known
|
||||||
|
if (!time_valid)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!_clock_enabled)
|
||||||
|
{
|
||||||
|
// The logo is only shown while the display is on; it never switches the power on
|
||||||
|
if (_power.isOn())
|
||||||
|
{
|
||||||
|
_showLogo();
|
||||||
|
_off_timer.request(now, POWER_OFF_DELAY);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (scheduleFor(hour, minute))
|
||||||
|
{
|
||||||
|
case ScheduleMode::Night:
|
||||||
|
if (_power.isOn())
|
||||||
|
{
|
||||||
|
_showBlank();
|
||||||
|
_off_timer.request(now, POWER_OFF_DELAY);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ScheduleMode::Clock:
|
||||||
|
_off_timer.cancel();
|
||||||
|
if (!_power.isOn())
|
||||||
|
{
|
||||||
|
_power.on();
|
||||||
|
_screen = Screen::Unknown;
|
||||||
|
}
|
||||||
|
_showClock(hour, minute);
|
||||||
|
break;
|
||||||
|
case ScheduleMode::Standby:
|
||||||
|
// Flipdots keep their image without power, so the last time stays visible
|
||||||
|
if (_power.isOn())
|
||||||
|
_off_timer.request(now, POWER_OFF_DELAY);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DisplayController::_showClock(int hour, int minute)
|
||||||
|
{
|
||||||
|
if (_screen == Screen::Clock && _shown_hour == hour && _shown_minute == minute)
|
||||||
|
return;
|
||||||
|
_frames.clock(hour, minute);
|
||||||
|
_screen = Screen::Clock;
|
||||||
|
_shown_hour = hour;
|
||||||
|
_shown_minute = minute;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DisplayController::_showLogo()
|
||||||
|
{
|
||||||
|
if (_screen == Screen::Logo)
|
||||||
|
return;
|
||||||
|
_frames.logo();
|
||||||
|
_screen = Screen::Logo;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DisplayController::_showBlank()
|
||||||
|
{
|
||||||
|
if (_screen == Screen::Blank)
|
||||||
|
return;
|
||||||
|
_frames.blank();
|
||||||
|
_screen = Screen::Blank;
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include "logic/power_timer.h"
|
||||||
|
|
||||||
|
// Power for the panels and the Nano (implemented by NanoLink)
|
||||||
|
class DisplayPower
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual bool isOn() = 0;
|
||||||
|
// Switch on and wait until the Nano is ready
|
||||||
|
virtual void on() = 0;
|
||||||
|
virtual void off() = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The screens the display can show (implemented by FlipdotFrames)
|
||||||
|
class Frames
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual void clock(int hour, int minute) = 0;
|
||||||
|
virtual void logo() = 0;
|
||||||
|
virtual void blank() = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Decides what the display shows and when it is powered, based on the time and the two
|
||||||
|
// Home Assistant switches. A frame is only sent when it changes. Pure logic, tested in
|
||||||
|
// test/test_esp32.
|
||||||
|
class DisplayController
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
DisplayController(DisplayPower &power, Frames &frames);
|
||||||
|
|
||||||
|
// Home Assistant "FlipDot Display Clock" switch: off shows the logo instead of the clock
|
||||||
|
void setClockEnabled(bool enabled);
|
||||||
|
// Home Assistant "FlipDot Display Power" switch: off clears the display and powers it off
|
||||||
|
void setPowerEnabled(bool enabled);
|
||||||
|
|
||||||
|
bool clockEnabled() const;
|
||||||
|
bool powerEnabled() const;
|
||||||
|
|
||||||
|
// Call often (every loop). time_valid is false until the time is synced over NTP.
|
||||||
|
void update(uint32_t now, bool time_valid, int hour, int minute);
|
||||||
|
|
||||||
|
private:
|
||||||
|
enum class Screen
|
||||||
|
{
|
||||||
|
Unknown, // nothing sent since the last power on
|
||||||
|
Clock,
|
||||||
|
Logo,
|
||||||
|
Blank,
|
||||||
|
};
|
||||||
|
|
||||||
|
DisplayPower &_power;
|
||||||
|
Frames &_frames;
|
||||||
|
PowerTimer _off_timer;
|
||||||
|
|
||||||
|
bool _clock_enabled = true;
|
||||||
|
bool _power_enabled = true;
|
||||||
|
|
||||||
|
Screen _screen = Screen::Unknown;
|
||||||
|
int _shown_hour = -1;
|
||||||
|
int _shown_minute = -1;
|
||||||
|
|
||||||
|
void _showClock(int hour, int minute);
|
||||||
|
void _showLogo();
|
||||||
|
void _showBlank();
|
||||||
|
};
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
#include "logic/power_timer.h"
|
||||||
|
|
||||||
|
// a is before b, also when millis() overflowed in between
|
||||||
|
static bool before(uint32_t a, uint32_t b)
|
||||||
|
{
|
||||||
|
return (int32_t)(a - b) < 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PowerTimer::request(uint32_t now, uint32_t delay)
|
||||||
|
{
|
||||||
|
uint32_t deadline = now + delay;
|
||||||
|
if (!_pending || before(deadline, _deadline))
|
||||||
|
_deadline = deadline;
|
||||||
|
_pending = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PowerTimer::cancel()
|
||||||
|
{
|
||||||
|
_pending = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PowerTimer::pending() const
|
||||||
|
{
|
||||||
|
return _pending;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PowerTimer::expired(uint32_t now)
|
||||||
|
{
|
||||||
|
if (!_pending || before(now, _deadline))
|
||||||
|
return false;
|
||||||
|
_pending = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
// Countdown to switching the display off. Works across millis() overflow.
|
||||||
|
class PowerTimer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// Switch off `delay` ms from `now`. An earlier pending deadline is kept.
|
||||||
|
void request(uint32_t now, uint32_t delay);
|
||||||
|
|
||||||
|
void cancel();
|
||||||
|
|
||||||
|
bool pending() const;
|
||||||
|
|
||||||
|
// True once when the deadline has passed (the timer is then cleared)
|
||||||
|
bool expired(uint32_t now);
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool _pending = false;
|
||||||
|
uint32_t _deadline = 0;
|
||||||
|
};
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
#include "logic/schedule.h"
|
||||||
|
|
||||||
|
static const int DAY_START_HOUR = 8;
|
||||||
|
static const int LATE_START_HOUR = 22;
|
||||||
|
static const int NIGHT_START_HOUR = 23;
|
||||||
|
static const int NIGHT_START_MINUTE = 31;
|
||||||
|
static const int EVENING_START_HOUR = 17;
|
||||||
|
|
||||||
|
ScheduleMode scheduleFor(int hour, int minute)
|
||||||
|
{
|
||||||
|
if (hour < DAY_START_HOUR || (hour >= NIGHT_START_HOUR && minute >= NIGHT_START_MINUTE))
|
||||||
|
return ScheduleMode::Night;
|
||||||
|
if (hour < LATE_START_HOUR || minute % 15 == 0)
|
||||||
|
return ScheduleMode::Clock;
|
||||||
|
return ScheduleMode::Standby;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isEvening(int hour)
|
||||||
|
{
|
||||||
|
return hour >= EVENING_START_HOUR;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// When the clock is shown. Pure logic, tested in test/test_esp32.
|
||||||
|
//
|
||||||
|
// 00:00 - 07:59 Night screen cleared, power off
|
||||||
|
// 08:00 - 21:59 Clock clock updated every minute
|
||||||
|
// 22:00 - 23:30 Clock only during the minutes :00, :15, :30 and :45
|
||||||
|
// Standby in between: power off, the last time stays on the dots
|
||||||
|
// 23:31 - 23:59 Night
|
||||||
|
|
||||||
|
enum class ScheduleMode
|
||||||
|
{
|
||||||
|
Night,
|
||||||
|
Clock,
|
||||||
|
Standby,
|
||||||
|
};
|
||||||
|
|
||||||
|
ScheduleMode scheduleFor(int hour, int minute);
|
||||||
|
|
||||||
|
// Evening: the yawning Pikachu is shown
|
||||||
|
bool isEvening(int hour);
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
// Built-in LED: off while the display is off, slow blink (1 s) when connected to WiFi,
|
||||||
|
// fast blink (200 ms) without WiFi. Pure logic, tested in test/test_esp32.
|
||||||
|
inline bool statusLedLevel(uint32_t now, bool wifi_connected, bool display_on)
|
||||||
|
{
|
||||||
|
if (!display_on)
|
||||||
|
return false;
|
||||||
|
uint32_t period = wifi_connected ? 1000 : 200;
|
||||||
|
return now % period < period / 2;
|
||||||
|
}
|
||||||
+46
-603
@@ -1,621 +1,64 @@
|
|||||||
#include "libs.h"
|
// ESP32 firmware: decides what the flipdot display shows (clock, Home Assistant switches)
|
||||||
|
// and sends it to the Nano, which drives the panels. See README.md.
|
||||||
|
|
||||||
void toggleLED(void *parameter);
|
#include <Arduino.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
uint32_t power_off_delay;
|
#include "BROSE9323.h"
|
||||||
int triggered_power_off = false;
|
#include "config.h"
|
||||||
int turn_hassio_power_off = false;
|
#include "display/frames.h"
|
||||||
|
#include "logic/display_controller.h"
|
||||||
|
#include "logic/status_led.h"
|
||||||
|
#include "nano_link/nano_flash_bridge.h"
|
||||||
|
#include "nano_link/nano_link.h"
|
||||||
|
#include "net/home_assistant.h"
|
||||||
|
#include "net/wifi_ota.h"
|
||||||
|
|
||||||
// Wait until the Nano reports READY after booting, or until NANO_BOOT_TIMEOUT
|
NanoLink nano(Serial2, NANO_RX_PIN, NANO_TX_PIN, RELAY_PIN, NANO_RESET_PIN);
|
||||||
void waitForNano()
|
BROSE9323 display(DISPLAY_WIDTH, DISPLAY_HEIGHT, PANEL_WIDTH);
|
||||||
{
|
FlipdotFrames frames(display);
|
||||||
char line[16];
|
DisplayController controller(nano, frames);
|
||||||
uint8_t len = 0;
|
|
||||||
uint32_t start = millis();
|
|
||||||
while (millis() - start < NANO_BOOT_TIMEOUT)
|
|
||||||
{
|
|
||||||
while (nanoSerial.available())
|
|
||||||
{
|
|
||||||
char c = nanoSerial.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()
|
// Flashing the Nano over WiFi: restart it into its bootloader, switch it off afterwards.
|
||||||
{
|
// The controller then powers it on again with the new firmware.
|
||||||
// cancel a pending power off countdown
|
NanoFlashBridge flashBridge(
|
||||||
triggered_power_off = false;
|
NANO_FLASH_PORT, Serial2,
|
||||||
|
[](uint32_t baud) { nano.startBootloader(baud); },
|
||||||
if (relay_on)
|
[]() { nano.off(); });
|
||||||
return;
|
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
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
|
|
||||||
nanoSerial.flush();
|
|
||||||
nanoSerial.end();
|
|
||||||
pinMode(nano_tx, INPUT);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flash session started: restart 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.
|
|
||||||
// The old bootloader also starts after a power cycle; Optiboot only after a reset pulse.
|
|
||||||
void startNanoFlash(uint32_t baud)
|
|
||||||
{
|
|
||||||
if (relay_on)
|
|
||||||
relayOff();
|
|
||||||
delay(NANO_POWER_OFF_TIME);
|
|
||||||
|
|
||||||
nanoSerial.begin(baud, SERIAL_8N1, nano_rx, nano_tx);
|
|
||||||
digitalWrite(relay, LOW);
|
|
||||||
|
|
||||||
if (nano_reset >= 0)
|
|
||||||
{
|
|
||||||
delay(NANO_RESET_DELAY);
|
|
||||||
digitalWrite(nano_reset, HIGH);
|
|
||||||
delay(NANO_RESET_PULSE);
|
|
||||||
digitalWrite(nano_reset, LOW);
|
|
||||||
}
|
|
||||||
|
|
||||||
// drop anything the Nano sent before its bootloader started
|
|
||||||
while (nanoSerial.available())
|
|
||||||
nanoSerial.read();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
{
|
|
||||||
// start new countdown
|
|
||||||
turn_hassio_power_off = _turn_hassio_power_off;
|
|
||||||
power_off_delay = millis() + delay;
|
|
||||||
|
|
||||||
triggered_power_off = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void updatePower()
|
|
||||||
{
|
|
||||||
if(millis() > power_off_delay && triggered_power_off == true)
|
|
||||||
{
|
|
||||||
// finished countdown
|
|
||||||
triggered_power_off = false;
|
|
||||||
|
|
||||||
// OFF
|
|
||||||
relayOff();
|
|
||||||
|
|
||||||
if(turn_hassio_power_off)
|
|
||||||
{
|
|
||||||
power = false;
|
|
||||||
powerSwitch.setState(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void onSwitchStateChanged(bool state, HASwitch *s)
|
|
||||||
{
|
|
||||||
hassioDisplayStatus = (state ? HIGH : LOW);
|
|
||||||
}
|
|
||||||
|
|
||||||
void onPowerSwitchStateChanged(bool state, HASwitch *s)
|
|
||||||
{
|
|
||||||
power = (state ? HIGH : LOW);
|
|
||||||
|
|
||||||
if(power == false)
|
|
||||||
{
|
|
||||||
if (relay_on)
|
|
||||||
{
|
|
||||||
display.fillScreen(0);
|
|
||||||
display.display();
|
|
||||||
|
|
||||||
delay(1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
turnPowerOff(0);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void print_reset_reason(RESET_REASON reason)
|
|
||||||
{
|
|
||||||
switch (reason)
|
|
||||||
{
|
|
||||||
case POWERON_RESET:
|
|
||||||
Serial.println("POWERON_RESET");
|
|
||||||
break; /**<1, Vbat power on reset*/
|
|
||||||
case 3:
|
|
||||||
Serial.println("SW_RESET");
|
|
||||||
break; /**<3, Software reset digital core*/
|
|
||||||
case 4:
|
|
||||||
Serial.println("OWDT_RESET");
|
|
||||||
break; /**<4, Legacy watch dog reset digital core*/
|
|
||||||
case 5:
|
|
||||||
Serial.println("DEEPSLEEP_RESET");
|
|
||||||
break; /**<5, Deep Sleep reset digital core*/
|
|
||||||
case 6:
|
|
||||||
Serial.println("SDIO_RESET");
|
|
||||||
break; /**<6, Reset by SLC module, reset digital core*/
|
|
||||||
case 7:
|
|
||||||
Serial.println("TG0WDT_SYS_RESET");
|
|
||||||
break; /**<7, Timer Group0 Watch dog reset digital core*/
|
|
||||||
case 8:
|
|
||||||
Serial.println("TG1WDT_SYS_RESET");
|
|
||||||
break; /**<8, Timer Group1 Watch dog reset digital core*/
|
|
||||||
case 9:
|
|
||||||
Serial.println("RTCWDT_SYS_RESET");
|
|
||||||
break; /**<9, RTC Watch dog Reset digital core*/
|
|
||||||
case 10:
|
|
||||||
Serial.println("INTRUSION_RESET");
|
|
||||||
break; /**<10, Instrusion tested to reset CPU*/
|
|
||||||
case 11:
|
|
||||||
Serial.println("TGWDT_CPU_RESET");
|
|
||||||
break; /**<11, Time Group reset CPU*/
|
|
||||||
case SW_CPU_RESET:
|
|
||||||
Serial.println("SW_CPU_RESET");
|
|
||||||
break; /**<12, Software reset CPU*/
|
|
||||||
case 13:
|
|
||||||
Serial.println("RTCWDT_CPU_RESET");
|
|
||||||
break; /**<13, RTC Watch dog Reset CPU*/
|
|
||||||
case EXT_CPU_RESET:
|
|
||||||
Serial.println("EXT_CPU_RESET");
|
|
||||||
break; /**<14, for APP CPU, reseted by PRO CPU*/
|
|
||||||
case 15:
|
|
||||||
Serial.println("RTCWDT_BROWN_OUT_RESET");
|
|
||||||
break; /**<15, Reset when the vdd voltage is not stable*/
|
|
||||||
case 16:
|
|
||||||
Serial.println("RTCWDT_RTC_RESET");
|
|
||||||
break; /**<16, RTC Watch dog reset digital core and rtc module*/
|
|
||||||
default:
|
|
||||||
Serial.println("NO_MEAN");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool checkResetStatus()
|
|
||||||
{
|
|
||||||
Serial.println("CPU0 reset reason: ");
|
|
||||||
RESET_REASON reason0 = rtc_get_reset_reason(0);
|
|
||||||
print_reset_reason(reason0);
|
|
||||||
|
|
||||||
// if(reason0 == SW_CPU_RESET)
|
|
||||||
// return true;
|
|
||||||
|
|
||||||
Serial.println("CPU1 reset reason: ");
|
|
||||||
RESET_REASON reason1 = rtc_get_reset_reason(1);
|
|
||||||
print_reset_reason(reason1);
|
|
||||||
|
|
||||||
// if(reason1 == SW_CPU_RESET)
|
|
||||||
// return true;
|
|
||||||
|
|
||||||
if (reason0 != POWERON_RESET && reason1 != EXT_CPU_RESET)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setup()
|
void setup()
|
||||||
{
|
{
|
||||||
otaPanicMode = true;
|
Serial.begin(115200);
|
||||||
pinMode(led1, OUTPUT);
|
pinMode(LED_PIN, OUTPUT);
|
||||||
|
nano.begin();
|
||||||
|
|
||||||
Serial.begin(115200);
|
wifiBegin();
|
||||||
|
otaBegin();
|
||||||
|
flashBridge.begin();
|
||||||
|
otaPanicWindow(LED_PIN);
|
||||||
|
|
||||||
WiFi.setHostname("FlipDotDisplay");
|
nano.on();
|
||||||
wifiMulti.addAP(ssid, password);
|
display.begin(&nano.stream());
|
||||||
|
display.setTiming(FLIP_TIME);
|
||||||
|
// Shown until the time is synced
|
||||||
|
frames.message("Connecting...");
|
||||||
|
|
||||||
Serial.println("Booting");
|
configTzTime(TIMEZONE, NTP_SERVER);
|
||||||
WiFi.mode(WIFI_STA);
|
homeAssistantBegin(controller);
|
||||||
WiFi.begin(ssid, password);
|
|
||||||
while (WiFi.waitForConnectResult() != WL_CONNECTED)
|
|
||||||
{
|
|
||||||
Serial.println("Connection Failed! Rebooting...");
|
|
||||||
delay(5000);
|
|
||||||
ESP.restart();
|
|
||||||
}
|
|
||||||
|
|
||||||
ArduinoOTA
|
|
||||||
.onStart([]()
|
|
||||||
{
|
|
||||||
String type;
|
|
||||||
if (ArduinoOTA.getCommand() == U_FLASH)
|
|
||||||
type = "sketch";
|
|
||||||
else // U_SPIFFS
|
|
||||||
type = "filesystem";
|
|
||||||
|
|
||||||
// NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end()
|
|
||||||
// USE_SERIAL.write(0x0C); //CLEAR SCREEN CMD
|
|
||||||
// Serial.println("Start updating " + type);
|
|
||||||
|
|
||||||
// if (!otaPanicMode)
|
|
||||||
// {
|
|
||||||
// display.begin(&Serial);
|
|
||||||
// display.fillScreen(0);
|
|
||||||
// display.setTextSize(1);
|
|
||||||
// display.setCursor(0, 0);
|
|
||||||
// display.print("ota starting!");
|
|
||||||
// display.display();
|
|
||||||
// }
|
|
||||||
})
|
|
||||||
.onEnd([]()
|
|
||||||
{
|
|
||||||
// USE_SERIAL.write(0x0C); //CLEAR SCREEN CMD
|
|
||||||
// Serial.println("\nEnd");
|
|
||||||
})
|
|
||||||
.onProgress([](unsigned int progress, unsigned int total)
|
|
||||||
{
|
|
||||||
// USE_SERIAL.write(0x0C); //CLEAR SCREEN CMD
|
|
||||||
// USE_SERIAL.printf("Progress: %u%%\r", (progress / (total / 100)));
|
|
||||||
// if (!otaPanicMode)
|
|
||||||
// {
|
|
||||||
// display.begin(&Serial);
|
|
||||||
// display.fillScreen(0);
|
|
||||||
// display.setTextSize(1);
|
|
||||||
// display.setCursor(0, 0);
|
|
||||||
// display.printf("ota: %u%%\r", (progress / (total / 100)));
|
|
||||||
// display.display();
|
|
||||||
// }
|
|
||||||
})
|
|
||||||
.onError([](ota_error_t error)
|
|
||||||
{
|
|
||||||
// // USE_SERIAL.write(0x0C); //CLEAR SCREEN CMD
|
|
||||||
// Serial.printf("Error[%u]: ", error);
|
|
||||||
// if (error == OTA_AUTH_ERROR)
|
|
||||||
// Serial.println("Auth Failed");
|
|
||||||
// else if (error == OTA_BEGIN_ERROR)
|
|
||||||
// Serial.println("Begin Failed");
|
|
||||||
// else if (error == OTA_CONNECT_ERROR)
|
|
||||||
// Serial.println("Connect Failed");
|
|
||||||
// else if (error == OTA_RECEIVE_ERROR)
|
|
||||||
// Serial.println("Receive Failed");
|
|
||||||
// else if (error == OTA_END_ERROR)
|
|
||||||
// Serial.println("End Failed");
|
|
||||||
});
|
|
||||||
|
|
||||||
ArduinoOTA.begin();
|
|
||||||
nanoFlashBridge.begin();
|
|
||||||
|
|
||||||
do
|
|
||||||
{
|
|
||||||
ArduinoOTA.handle();
|
|
||||||
|
|
||||||
digitalWrite(led1, HIGH);
|
|
||||||
|
|
||||||
vTaskDelay(50 / portTICK_PERIOD_MS);
|
|
||||||
|
|
||||||
// Turn the LED off
|
|
||||||
digitalWrite(led1, LOW);
|
|
||||||
|
|
||||||
vTaskDelay(50 / portTICK_PERIOD_MS);
|
|
||||||
|
|
||||||
} while (checkResetStatus() && millis() < 1000 * 15);
|
|
||||||
otaPanicMode = false;
|
|
||||||
|
|
||||||
pinMode(relay, OUTPUT);
|
|
||||||
if (nano_reset >= 0)
|
|
||||||
{
|
|
||||||
digitalWrite(nano_reset, LOW);
|
|
||||||
pinMode(nano_reset, OUTPUT);
|
|
||||||
}
|
|
||||||
relayOn();
|
|
||||||
|
|
||||||
display.begin(&nanoSerial);
|
|
||||||
// display.setTiming(200);
|
|
||||||
// display.setTiming(180);
|
|
||||||
// display.setTiming(220); // works best so far
|
|
||||||
display.setTiming(225);
|
|
||||||
|
|
||||||
// delay(4000);
|
|
||||||
display.fillScreen(0);
|
|
||||||
display.setCursor(0, 0);
|
|
||||||
display.setTextSize(1);
|
|
||||||
display.println("Connecting...");
|
|
||||||
display.display();
|
|
||||||
|
|
||||||
// display.setFont(&FreeSerif12pt7b);
|
|
||||||
// display.setFont(&FreeSerifItalic12pt7b);
|
|
||||||
|
|
||||||
//init and get the time
|
|
||||||
// configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
|
|
||||||
configTzTime(MYTZ, ntpServer);
|
|
||||||
|
|
||||||
xTaskCreate(
|
|
||||||
toggleLED, // Function that should be called
|
|
||||||
"Toggle LED", // Name of the task (for debugging)
|
|
||||||
1000, // Stack size (bytes)
|
|
||||||
NULL, // Parameter to pass
|
|
||||||
1, // Task priority
|
|
||||||
NULL // Task handle
|
|
||||||
);
|
|
||||||
|
|
||||||
next_display = millis() / 1000 * 1000;
|
|
||||||
|
|
||||||
// // TEST CODE
|
|
||||||
// do{
|
|
||||||
// ArduinoOTA.handle();
|
|
||||||
|
|
||||||
// digitalWrite(led1, HIGH);
|
|
||||||
|
|
||||||
// vTaskDelay(50 / portTICK_PERIOD_MS);
|
|
||||||
|
|
||||||
// // Turn the LED off
|
|
||||||
// digitalWrite(led1, LOW);
|
|
||||||
|
|
||||||
// vTaskDelay(50 / portTICK_PERIOD_MS);
|
|
||||||
|
|
||||||
// }while(true);
|
|
||||||
|
|
||||||
// hassio
|
|
||||||
// you don't need to verify return status
|
|
||||||
// client.begin(mac);
|
|
||||||
|
|
||||||
// set device's details (optional)
|
|
||||||
device.setName("FlipDotDisplay");
|
|
||||||
device.setSoftwareVersion("1.0.0");
|
|
||||||
|
|
||||||
// set icon (optional)
|
|
||||||
flipDotDisplay.setIcon("mdi:lightbulb");
|
|
||||||
flipDotDisplay.setName("FlipDot Display Clock");
|
|
||||||
|
|
||||||
// handle switch state
|
|
||||||
flipDotDisplay.onCommand(onSwitchStateChanged);
|
|
||||||
|
|
||||||
|
|
||||||
powerSwitch.setIcon("mdi:lightbulb");
|
|
||||||
powerSwitch.setName("FlipDot Display Power");
|
|
||||||
powerSwitch.onCommand(onPowerSwitchStateChanged);
|
|
||||||
|
|
||||||
mqtt.begin(BROKER_ADDR, MQTT_USER, MQTT_PASSWORD);
|
|
||||||
|
|
||||||
display.fillScreen(0);
|
|
||||||
display.display();
|
|
||||||
|
|
||||||
hour = millis() / 1000 / 60 / 60;
|
|
||||||
minute = (millis() / 1000 / 60) % 60;
|
|
||||||
second = (millis() / 1000 % 60);
|
|
||||||
}
|
|
||||||
|
|
||||||
void toggleLED(void *parameter)
|
|
||||||
{
|
|
||||||
for (;;)
|
|
||||||
{ // infinite loop
|
|
||||||
if((power == false) || (!(hour >= 7 && hour < 22)))
|
|
||||||
{
|
|
||||||
// OFF
|
|
||||||
digitalWrite(led1, LOW);
|
|
||||||
vTaskDelay(1000 / portTICK_PERIOD_MS);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Turn the LED on
|
|
||||||
digitalWrite(led1, HIGH);
|
|
||||||
|
|
||||||
if (wifiStatus)
|
|
||||||
{
|
|
||||||
// Pause the task for 500ms
|
|
||||||
vTaskDelay(500 / portTICK_PERIOD_MS);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
vTaskDelay(100 / portTICK_PERIOD_MS);
|
|
||||||
|
|
||||||
// Turn the LED off
|
|
||||||
digitalWrite(led1, LOW);
|
|
||||||
|
|
||||||
if (wifiStatus)
|
|
||||||
{
|
|
||||||
// Pause the task again for 500ms
|
|
||||||
vTaskDelay(500 / portTICK_PERIOD_MS);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
vTaskDelay(100 / portTICK_PERIOD_MS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#define DEG2RAD 0.0174532925
|
|
||||||
void drawRotatedBitmap(int16_t x, int16_t y, const uint8_t *bitmap, int16_t w, int16_t h, bool color, uint16_t angle)
|
|
||||||
{
|
|
||||||
int16_t newx, newy;
|
|
||||||
uint8_t data = 0;
|
|
||||||
|
|
||||||
float cosa = cos(angle * DEG2RAD), sina = sin(angle * DEG2RAD);
|
|
||||||
|
|
||||||
x = x - ((w * cosa / 2) - (h * sina / 2));
|
|
||||||
y = y - ((h * cosa / 2) + (w * sina / 2));
|
|
||||||
|
|
||||||
for (int16_t j = 0; j < h; j++)
|
|
||||||
{
|
|
||||||
for (int16_t i = 0; i < w; i++)
|
|
||||||
{
|
|
||||||
if ((j * w + i) & 7)
|
|
||||||
data <<= 1;
|
|
||||||
else
|
|
||||||
data = pgm_read_byte(bitmap++);
|
|
||||||
|
|
||||||
newx = 0.5 + x + ((i * cosa) - (j * sina));
|
|
||||||
newy = 0.5 + y + ((j * cosa) + (i * sina));
|
|
||||||
|
|
||||||
if (data & 0x80)
|
|
||||||
display.drawPixel(newx, newy, color);
|
|
||||||
//else display.drawPixel(newx, newy, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void turnPowerOn(bool overide = false)
|
|
||||||
{
|
|
||||||
if(power == true || overide == true)
|
|
||||||
relayOn();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void loop()
|
void loop()
|
||||||
{
|
{
|
||||||
ArduinoOTA.handle();
|
otaLoop();
|
||||||
mqtt.loop();
|
homeAssistantLoop();
|
||||||
|
|
||||||
// While the Nano is being flashed, nothing else may talk to it
|
// While the Nano is being flashed, nothing else may talk to it
|
||||||
if (nanoFlashBridge.handle())
|
if (flashBridge.handle())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
updatePower();
|
struct tm now = {};
|
||||||
|
bool time_valid = getLocalTime(&now, 0);
|
||||||
|
controller.update(millis(), time_valid, now.tm_hour, now.tm_min);
|
||||||
|
|
||||||
wifiStatus = WiFi.waitForConnectResult() == WL_CONNECTED;
|
digitalWrite(LED_PIN, statusLedLevel(millis(), wifiConnected(), nano.isOn()));
|
||||||
|
|
||||||
if(next_display < millis())
|
|
||||||
{
|
|
||||||
if (hassioDisplayStatus)
|
|
||||||
// if (false)
|
|
||||||
{
|
|
||||||
char t[20];
|
|
||||||
char timeStringBuff[50]; //50 chars should be enough
|
|
||||||
|
|
||||||
struct tm timeinfo;
|
|
||||||
if (!getLocalTime(&timeinfo))
|
|
||||||
{
|
|
||||||
// printFromStart("Failed to get time");
|
|
||||||
// return;
|
|
||||||
|
|
||||||
// sprintf(t, "%02d:%02d:%02d\0", millis() / 1000 / 60 / 60, (millis() / 1000 / 60) % 60, (millis() / 1000 % 60));
|
|
||||||
// sprintf(t, "%02d%02d%02d\0", millis() / 1000 / 60 / 60, (millis() / 1000 / 60) % 60, (millis() / 1000 % 60));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// const char *format = "%H:%M:%S";
|
|
||||||
const char *format = "%H:%M";
|
|
||||||
|
|
||||||
// strftime(timeStringBuff, sizeof(timeStringBuff), "%A, %B %d %Y %H:%M:%S", &timeinfo);
|
|
||||||
strftime(timeStringBuff, sizeof(timeStringBuff), format, &timeinfo);
|
|
||||||
String asString(timeStringBuff);
|
|
||||||
|
|
||||||
hour = timeinfo.tm_hour;
|
|
||||||
minute = timeinfo.tm_min;
|
|
||||||
second = timeinfo.tm_sec;
|
|
||||||
}
|
|
||||||
|
|
||||||
// display.setFont(&FreeSerif12pt7b);
|
|
||||||
|
|
||||||
// if ((millis() / 1000 % 60) <= 30)
|
|
||||||
// display.setFont(&FreeSerif12pt7b);
|
|
||||||
// else
|
|
||||||
// display.setFont(&FreeSerifItalic12pt7b);
|
|
||||||
|
|
||||||
if(hour <= 7 || (hour >= 23 && minute > 30))
|
|
||||||
{
|
|
||||||
if (relay_on)
|
|
||||||
{
|
|
||||||
display.fillScreen(0);
|
|
||||||
display.display();
|
|
||||||
}
|
|
||||||
|
|
||||||
turnPowerOff();
|
|
||||||
}
|
|
||||||
// show time between 7 and 23 with updates every minute, and after that; every 15 minutes.
|
|
||||||
else if((hour >= 7 && hour < 22) || (minute % 15 == 0))
|
|
||||||
// else if((hour >= 7 && hour < 19) || (minute % 5 == 0))
|
|
||||||
// else if(true)
|
|
||||||
{
|
|
||||||
turnPowerOn();
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
// 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.setCursor(xoffset + 43, (19/2) - 5);
|
|
||||||
display.print(timeStringBuff);
|
|
||||||
|
|
||||||
display.display();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
turnPowerOff();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
|
|
||||||
// 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");
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
turnPowerOff();
|
|
||||||
}
|
|
||||||
|
|
||||||
next_display += 1000;
|
|
||||||
// next_display += 500;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
#include "nano_flash_bridge.h"
|
#include "nano_link/nano_flash_bridge.h"
|
||||||
|
|
||||||
NanoFlashBridge::NanoFlashBridge(uint16_t port, HardwareSerial &serial, void (*onStart)(uint32_t baud), void (*onEnd)())
|
NanoFlashBridge::NanoFlashBridge(uint16_t port, HardwareSerial &serial, void (*onStart)(uint32_t baud), void (*onEnd)())
|
||||||
: _server(port), _serial(serial), _onStart(onStart), _onEnd(onEnd)
|
: _server(port), _serial(serial), _onStart(onStart), _onEnd(onEnd)
|
||||||
File renamed without changes.
@@ -0,0 +1,116 @@
|
|||||||
|
#include "nano_link/nano_link.h"
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
// Relay input is active low
|
||||||
|
static const uint8_t RELAY_ON = LOW;
|
||||||
|
static const uint8_t RELAY_OFF = HIGH;
|
||||||
|
|
||||||
|
NanoLink::NanoLink(HardwareSerial &serial, int rx_pin, int tx_pin, int relay_pin, int reset_pin)
|
||||||
|
: _serial(serial), _rx_pin(rx_pin), _tx_pin(tx_pin), _relay_pin(relay_pin), _reset_pin(reset_pin)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void NanoLink::begin()
|
||||||
|
{
|
||||||
|
digitalWrite(_relay_pin, RELAY_OFF);
|
||||||
|
pinMode(_relay_pin, OUTPUT);
|
||||||
|
pinMode(_tx_pin, INPUT);
|
||||||
|
if (_reset_pin >= 0)
|
||||||
|
{
|
||||||
|
digitalWrite(_reset_pin, LOW);
|
||||||
|
pinMode(_reset_pin, OUTPUT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream &NanoLink::stream()
|
||||||
|
{
|
||||||
|
return _serial;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NanoLink::isOn()
|
||||||
|
{
|
||||||
|
return _on;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NanoLink::on()
|
||||||
|
{
|
||||||
|
if (_on)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_beginSerial(NANO_BAUD);
|
||||||
|
digitalWrite(_relay_pin, RELAY_ON);
|
||||||
|
_waitForReady();
|
||||||
|
_on = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NanoLink::off()
|
||||||
|
{
|
||||||
|
digitalWrite(_relay_pin, RELAY_OFF);
|
||||||
|
_on = false;
|
||||||
|
|
||||||
|
// An idle UART TX line is high and would power the Nano through its RX pin
|
||||||
|
_serial.flush();
|
||||||
|
_serial.end();
|
||||||
|
pinMode(_tx_pin, INPUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The old bootloader also starts after a power cycle; Optiboot only after a reset pulse.
|
||||||
|
void NanoLink::startBootloader(uint32_t baud)
|
||||||
|
{
|
||||||
|
off();
|
||||||
|
delay(NANO_POWER_OFF_TIME);
|
||||||
|
|
||||||
|
_beginSerial(baud);
|
||||||
|
digitalWrite(_relay_pin, RELAY_ON);
|
||||||
|
|
||||||
|
if (_reset_pin >= 0)
|
||||||
|
{
|
||||||
|
delay(NANO_RESET_DELAY);
|
||||||
|
digitalWrite(_reset_pin, HIGH);
|
||||||
|
delay(NANO_RESET_PULSE);
|
||||||
|
digitalWrite(_reset_pin, LOW);
|
||||||
|
}
|
||||||
|
|
||||||
|
// drop anything the Nano sent before its bootloader started
|
||||||
|
_drain();
|
||||||
|
// Not "on" for the display: the bootloader is running, not the display firmware
|
||||||
|
}
|
||||||
|
|
||||||
|
void NanoLink::_beginSerial(uint32_t baud)
|
||||||
|
{
|
||||||
|
_serial.begin(baud, SERIAL_8N1, _rx_pin, _tx_pin);
|
||||||
|
// drop old data (e.g. ERR replies) so only a new READY counts
|
||||||
|
_drain();
|
||||||
|
}
|
||||||
|
|
||||||
|
void NanoLink::_drain()
|
||||||
|
{
|
||||||
|
while (_serial.available())
|
||||||
|
_serial.read();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait until the Nano reports READY after booting, or until NANO_BOOT_TIMEOUT
|
||||||
|
void NanoLink::_waitForReady()
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
#include "logic/display_controller.h"
|
||||||
|
|
||||||
|
// The relay that powers the panels and the Nano, and the serial link to the Nano.
|
||||||
|
class NanoLink : public DisplayPower
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
NanoLink(HardwareSerial &serial, int rx_pin, int tx_pin, int relay_pin, int reset_pin);
|
||||||
|
|
||||||
|
// Set up the pins; the display starts switched off
|
||||||
|
void begin();
|
||||||
|
|
||||||
|
// The serial port to the Nano (for the BROSE9323 driver)
|
||||||
|
Stream &stream();
|
||||||
|
|
||||||
|
bool isOn() override;
|
||||||
|
// Switch on, then wait for the Nano's READY (or NANO_BOOT_TIMEOUT)
|
||||||
|
void on() override;
|
||||||
|
// Switch off and release the TX pin
|
||||||
|
void off() override;
|
||||||
|
|
||||||
|
// For flashing: restart the Nano so its bootloader runs, and talk to it at `baud`
|
||||||
|
void startBootloader(uint32_t baud);
|
||||||
|
|
||||||
|
private:
|
||||||
|
HardwareSerial &_serial;
|
||||||
|
const int _rx_pin, _tx_pin, _relay_pin, _reset_pin;
|
||||||
|
bool _on = false;
|
||||||
|
|
||||||
|
void _beginSerial(uint32_t baud);
|
||||||
|
void _drain();
|
||||||
|
void _waitForReady();
|
||||||
|
};
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
#include "net/home_assistant.h"
|
||||||
|
|
||||||
|
#include <ArduinoHA.h>
|
||||||
|
#include <WiFi.h>
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
#include "credentials.h"
|
||||||
|
|
||||||
|
static WiFiClient client;
|
||||||
|
static HADevice device(HA_DEVICE_ID, sizeof(HA_DEVICE_ID));
|
||||||
|
static HAMqtt mqtt(client, device);
|
||||||
|
|
||||||
|
// Unique ids: do not change, Home Assistant's entities are linked to them
|
||||||
|
static HASwitch clockSwitch("FlipDotDisplayClock");
|
||||||
|
static HASwitch powerSwitch("FlipDotDisplayPower");
|
||||||
|
|
||||||
|
static DisplayController *displayController = nullptr;
|
||||||
|
|
||||||
|
// The switches are not optimistic: the new state must be reported back to Home Assistant
|
||||||
|
static void onClockCommand(bool state, HASwitch *sender)
|
||||||
|
{
|
||||||
|
displayController->setClockEnabled(state);
|
||||||
|
sender->setState(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void onPowerCommand(bool state, HASwitch *sender)
|
||||||
|
{
|
||||||
|
displayController->setPowerEnabled(state);
|
||||||
|
sender->setState(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
void homeAssistantBegin(DisplayController &controller)
|
||||||
|
{
|
||||||
|
displayController = &controller;
|
||||||
|
|
||||||
|
device.setName(HA_DEVICE_NAME);
|
||||||
|
device.setSoftwareVersion(HA_SOFTWARE_VERSION);
|
||||||
|
|
||||||
|
clockSwitch.setName("FlipDot Display Clock");
|
||||||
|
clockSwitch.setIcon("mdi:lightbulb");
|
||||||
|
clockSwitch.setCurrentState(controller.clockEnabled());
|
||||||
|
clockSwitch.onCommand(onClockCommand);
|
||||||
|
|
||||||
|
powerSwitch.setName("FlipDot Display Power");
|
||||||
|
powerSwitch.setIcon("mdi:lightbulb");
|
||||||
|
powerSwitch.setCurrentState(controller.powerEnabled());
|
||||||
|
powerSwitch.onCommand(onPowerCommand);
|
||||||
|
|
||||||
|
mqtt.begin(BROKER_ADDR, MQTT_USER, MQTT_PASSWORD);
|
||||||
|
}
|
||||||
|
|
||||||
|
void homeAssistantLoop()
|
||||||
|
{
|
||||||
|
mqtt.loop();
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "logic/display_controller.h"
|
||||||
|
|
||||||
|
// Home Assistant integration over MQTT (with discovery). Two switches:
|
||||||
|
// FlipDot Display Clock on = clock, off = Nintendo logo
|
||||||
|
// FlipDot Display Power off = clear the display and switch it off
|
||||||
|
void homeAssistantBegin(DisplayController &controller);
|
||||||
|
void homeAssistantLoop();
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
#include "net/wifi_ota.h"
|
||||||
|
|
||||||
|
#include <ArduinoOTA.h>
|
||||||
|
#include <WiFi.h>
|
||||||
|
#include <rom/rtc.h>
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
#include "credentials.h"
|
||||||
|
|
||||||
|
void wifiBegin()
|
||||||
|
{
|
||||||
|
WiFi.setHostname(HOSTNAME);
|
||||||
|
WiFi.mode(WIFI_STA);
|
||||||
|
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
|
||||||
|
Serial.println("Connecting to WiFi");
|
||||||
|
if (WiFi.waitForConnectResult() != WL_CONNECTED)
|
||||||
|
{
|
||||||
|
Serial.println("WiFi connection failed, restarting");
|
||||||
|
delay(5000);
|
||||||
|
ESP.restart();
|
||||||
|
}
|
||||||
|
// Later disconnects are handled by the WiFi driver's auto reconnect
|
||||||
|
Serial.print("Connected, IP ");
|
||||||
|
Serial.println(WiFi.localIP());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool wifiConnected()
|
||||||
|
{
|
||||||
|
return WiFi.status() == WL_CONNECTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
void otaBegin()
|
||||||
|
{
|
||||||
|
ArduinoOTA.setHostname(HOSTNAME);
|
||||||
|
ArduinoOTA.onStart([]() { Serial.println("OTA update started"); });
|
||||||
|
ArduinoOTA.onEnd([]() { Serial.println("OTA update done"); });
|
||||||
|
ArduinoOTA.onError([](ota_error_t error) { Serial.printf("OTA error %u\n", error); });
|
||||||
|
ArduinoOTA.begin();
|
||||||
|
}
|
||||||
|
|
||||||
|
void otaLoop()
|
||||||
|
{
|
||||||
|
ArduinoOTA.handle();
|
||||||
|
}
|
||||||
|
|
||||||
|
// A normal start is a power on of CPU 0 or a start of CPU 1 by CPU 0
|
||||||
|
static bool unexpectedReset()
|
||||||
|
{
|
||||||
|
RESET_REASON cpu0 = rtc_get_reset_reason(0);
|
||||||
|
RESET_REASON cpu1 = rtc_get_reset_reason(1);
|
||||||
|
Serial.printf("Reset reason: CPU0 %d, CPU1 %d\n", cpu0, cpu1);
|
||||||
|
return cpu0 != POWERON_RESET && cpu1 != EXT_CPU_RESET;
|
||||||
|
}
|
||||||
|
|
||||||
|
void otaPanicWindow(int led_pin)
|
||||||
|
{
|
||||||
|
if (!unexpectedReset())
|
||||||
|
return;
|
||||||
|
|
||||||
|
Serial.println("Unexpected reset: waiting for an OTA update");
|
||||||
|
uint32_t start = millis();
|
||||||
|
while (millis() - start < OTA_PANIC_WINDOW)
|
||||||
|
{
|
||||||
|
ArduinoOTA.handle();
|
||||||
|
// fast blink
|
||||||
|
digitalWrite(led_pin, (millis() / 50) % 2);
|
||||||
|
delay(10);
|
||||||
|
}
|
||||||
|
digitalWrite(led_pin, LOW);
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// Connect to WiFi. Restarts the ESP32 when the first connection fails.
|
||||||
|
void wifiBegin();
|
||||||
|
|
||||||
|
bool wifiConnected();
|
||||||
|
|
||||||
|
// Over-the-air updates of the ESP32 firmware (pio run -e esp32 -t upload)
|
||||||
|
void otaBegin();
|
||||||
|
void otaLoop();
|
||||||
|
|
||||||
|
// After a crash or unexpected reset, run only OTA for OTA_PANIC_WINDOW, so a firmware that
|
||||||
|
// crashes at boot can still be replaced. Does nothing after a normal power on.
|
||||||
|
void otaPanicWindow(int led_pin);
|
||||||
+1
-1
@@ -19,7 +19,7 @@ The display driver itself is in [lib/BROSE9323](../../lib/BROSE9323/README.md).
|
|||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
`pio test -e native` runs the unit tests in [test/test_nano](../../test/test_nano/test_main.cpp)
|
`pio test -e test_nano` runs the unit tests in [test/test_nano](../../test/test_nano/test_main.cpp)
|
||||||
on the PC. They feed serial data to the firmware and check what it asks the display to do:
|
on the PC. They feed serial data to the firmware and check what it asks the display to do:
|
||||||
every command, bitmaps, bad input, flip time storage and the full-redraw timing. The
|
every command, bitmaps, bad input, flip time storage and the full-redraw timing. The
|
||||||
Arduino core, EEPROM and the display driver are replaced by fakes in
|
Arduino core, EEPROM and the display driver are replaced by fakes in
|
||||||
|
|||||||
@@ -0,0 +1,371 @@
|
|||||||
|
// Tests for the ESP32 logic (src/esp32/logic), run on the PC: pio test -e test_esp32
|
||||||
|
//
|
||||||
|
// The display controller is tested with a fake power switch and fake frames that record
|
||||||
|
// what the controller asks for.
|
||||||
|
|
||||||
|
#include <unity.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
#include "logic/display_controller.h"
|
||||||
|
#include "logic/power_timer.h"
|
||||||
|
#include "logic/schedule.h"
|
||||||
|
#include "logic/status_led.h"
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Fakes
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class FakePower : public DisplayPower
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
bool is_on = false;
|
||||||
|
int on_count = 0;
|
||||||
|
int off_count = 0;
|
||||||
|
|
||||||
|
bool isOn() override { return is_on; }
|
||||||
|
void on() override
|
||||||
|
{
|
||||||
|
is_on = true;
|
||||||
|
on_count++;
|
||||||
|
}
|
||||||
|
void off() override
|
||||||
|
{
|
||||||
|
is_on = false;
|
||||||
|
off_count++;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class FakeFrames : public Frames
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
std::vector<std::string> shown;
|
||||||
|
|
||||||
|
void clock(int hour, int minute) override
|
||||||
|
{
|
||||||
|
char s[16];
|
||||||
|
snprintf(s, sizeof(s), "clock %02d:%02d", hour, minute);
|
||||||
|
shown.push_back(s);
|
||||||
|
}
|
||||||
|
void logo() override { shown.push_back("logo"); }
|
||||||
|
void blank() override { shown.push_back("blank"); }
|
||||||
|
|
||||||
|
std::string last() const { return shown.empty() ? "" : shown.back(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
static FakePower power;
|
||||||
|
static FakeFrames frames;
|
||||||
|
static DisplayController *controller;
|
||||||
|
static uint32_t now;
|
||||||
|
|
||||||
|
void setUp()
|
||||||
|
{
|
||||||
|
power = FakePower();
|
||||||
|
frames = FakeFrames();
|
||||||
|
delete controller;
|
||||||
|
controller = new DisplayController(power, frames);
|
||||||
|
now = 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
void tearDown() {}
|
||||||
|
|
||||||
|
// Run the controller at a time of day; `ms` passes in steps like the real loop
|
||||||
|
static void run(int hour, int minute, uint32_t ms = 0)
|
||||||
|
{
|
||||||
|
uint32_t end = now + ms;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
controller->update(now, true, hour, minute);
|
||||||
|
now += 100;
|
||||||
|
} while ((int32_t)(end - now) >= 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Schedule
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void test_schedule_night_until_8()
|
||||||
|
{
|
||||||
|
TEST_ASSERT_TRUE(scheduleFor(0, 0) == ScheduleMode::Night);
|
||||||
|
TEST_ASSERT_TRUE(scheduleFor(7, 0) == ScheduleMode::Night);
|
||||||
|
TEST_ASSERT_TRUE(scheduleFor(7, 59) == ScheduleMode::Night);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_schedule_clock_from_8_to_22()
|
||||||
|
{
|
||||||
|
TEST_ASSERT_TRUE(scheduleFor(8, 0) == ScheduleMode::Clock);
|
||||||
|
TEST_ASSERT_TRUE(scheduleFor(12, 34) == ScheduleMode::Clock);
|
||||||
|
TEST_ASSERT_TRUE(scheduleFor(21, 59) == ScheduleMode::Clock);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_schedule_quarter_hours_until_23_30()
|
||||||
|
{
|
||||||
|
TEST_ASSERT_TRUE(scheduleFor(22, 0) == ScheduleMode::Clock);
|
||||||
|
TEST_ASSERT_TRUE(scheduleFor(22, 1) == ScheduleMode::Standby);
|
||||||
|
TEST_ASSERT_TRUE(scheduleFor(22, 14) == ScheduleMode::Standby);
|
||||||
|
TEST_ASSERT_TRUE(scheduleFor(22, 15) == ScheduleMode::Clock);
|
||||||
|
TEST_ASSERT_TRUE(scheduleFor(22, 45) == ScheduleMode::Clock);
|
||||||
|
TEST_ASSERT_TRUE(scheduleFor(23, 0) == ScheduleMode::Clock);
|
||||||
|
TEST_ASSERT_TRUE(scheduleFor(23, 29) == ScheduleMode::Standby);
|
||||||
|
TEST_ASSERT_TRUE(scheduleFor(23, 30) == ScheduleMode::Clock);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_schedule_night_from_23_31()
|
||||||
|
{
|
||||||
|
TEST_ASSERT_TRUE(scheduleFor(23, 31) == ScheduleMode::Night);
|
||||||
|
TEST_ASSERT_TRUE(scheduleFor(23, 45) == ScheduleMode::Night);
|
||||||
|
TEST_ASSERT_TRUE(scheduleFor(23, 59) == ScheduleMode::Night);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_evening_from_17()
|
||||||
|
{
|
||||||
|
TEST_ASSERT_FALSE(isEvening(16));
|
||||||
|
TEST_ASSERT_TRUE(isEvening(17));
|
||||||
|
TEST_ASSERT_TRUE(isEvening(23));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Power timer
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void test_timer_expires_once_after_delay()
|
||||||
|
{
|
||||||
|
PowerTimer t;
|
||||||
|
t.request(1000, 500);
|
||||||
|
TEST_ASSERT_FALSE(t.expired(1499));
|
||||||
|
TEST_ASSERT_TRUE(t.expired(1500));
|
||||||
|
TEST_ASSERT_FALSE(t.expired(1600));
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_timer_keeps_earlier_deadline()
|
||||||
|
{
|
||||||
|
PowerTimer t;
|
||||||
|
t.request(0, 1000);
|
||||||
|
t.request(500, 1000); // later deadline: ignored
|
||||||
|
TEST_ASSERT_TRUE(t.expired(1000));
|
||||||
|
|
||||||
|
t.request(0, 30000);
|
||||||
|
t.request(100, 1000); // earlier deadline: used
|
||||||
|
TEST_ASSERT_TRUE(t.expired(1100));
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_timer_cancel()
|
||||||
|
{
|
||||||
|
PowerTimer t;
|
||||||
|
t.request(0, 100);
|
||||||
|
t.cancel();
|
||||||
|
TEST_ASSERT_FALSE(t.pending());
|
||||||
|
TEST_ASSERT_FALSE(t.expired(1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_timer_across_millis_overflow()
|
||||||
|
{
|
||||||
|
PowerTimer t;
|
||||||
|
t.request(0xFFFFFF00u, 0x200); // deadline wraps past 0
|
||||||
|
TEST_ASSERT_FALSE(t.expired(0xFFFFFFFFu));
|
||||||
|
TEST_ASSERT_FALSE(t.expired(0x00000050u));
|
||||||
|
TEST_ASSERT_TRUE(t.expired(0x00000100u));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Status LED
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void test_led_off_while_display_off()
|
||||||
|
{
|
||||||
|
for (uint32_t t = 0; t < 2000; t += 50)
|
||||||
|
TEST_ASSERT_FALSE(statusLedLevel(t, true, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_led_blinks_slow_with_wifi_and_fast_without()
|
||||||
|
{
|
||||||
|
TEST_ASSERT_TRUE(statusLedLevel(0, true, true));
|
||||||
|
TEST_ASSERT_TRUE(statusLedLevel(499, true, true));
|
||||||
|
TEST_ASSERT_FALSE(statusLedLevel(500, true, true));
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE(statusLedLevel(0, false, true));
|
||||||
|
TEST_ASSERT_FALSE(statusLedLevel(100, false, true));
|
||||||
|
TEST_ASSERT_TRUE(statusLedLevel(200, false, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Display controller
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void test_nothing_happens_until_time_is_synced()
|
||||||
|
{
|
||||||
|
power.is_on = true; // showing "Connecting..."
|
||||||
|
controller->update(now, false, 0, 0);
|
||||||
|
TEST_ASSERT_EQUAL(0, power.off_count);
|
||||||
|
TEST_ASSERT_EQUAL(0, (int)frames.shown.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_daytime_powers_on_and_shows_clock()
|
||||||
|
{
|
||||||
|
run(12, 34);
|
||||||
|
TEST_ASSERT_TRUE(power.is_on);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("clock 12:34", frames.last().c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_clock_is_only_sent_when_the_minute_changes()
|
||||||
|
{
|
||||||
|
run(12, 34, 5000);
|
||||||
|
TEST_ASSERT_EQUAL(1, (int)frames.shown.size());
|
||||||
|
run(12, 35);
|
||||||
|
TEST_ASSERT_EQUAL(2, (int)frames.shown.size());
|
||||||
|
TEST_ASSERT_EQUAL_STRING("clock 12:35", frames.last().c_str());
|
||||||
|
TEST_ASSERT_EQUAL(1, power.on_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_night_clears_then_powers_off_after_delay()
|
||||||
|
{
|
||||||
|
run(23, 30);
|
||||||
|
run(23, 31, POWER_OFF_DELAY - 200);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("blank", frames.last().c_str());
|
||||||
|
TEST_ASSERT_TRUE(power.is_on);
|
||||||
|
run(23, 31, 300);
|
||||||
|
TEST_ASSERT_FALSE(power.is_on);
|
||||||
|
TEST_ASSERT_EQUAL(1, power.off_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_night_does_not_power_on()
|
||||||
|
{
|
||||||
|
run(3, 0, 60000);
|
||||||
|
TEST_ASSERT_EQUAL(0, power.on_count);
|
||||||
|
TEST_ASSERT_EQUAL(0, (int)frames.shown.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_standby_keeps_the_last_time_and_powers_off()
|
||||||
|
{
|
||||||
|
run(22, 0);
|
||||||
|
run(22, 1, POWER_OFF_DELAY + 200);
|
||||||
|
TEST_ASSERT_FALSE(power.is_on);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("clock 22:00", frames.last().c_str()); // no blank
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_quarter_hour_powers_on_again_and_redraws()
|
||||||
|
{
|
||||||
|
run(22, 1, POWER_OFF_DELAY + 200);
|
||||||
|
run(22, 14);
|
||||||
|
TEST_ASSERT_FALSE(power.is_on);
|
||||||
|
run(22, 15);
|
||||||
|
TEST_ASSERT_TRUE(power.is_on);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("clock 22:15", frames.last().c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_clock_cancels_pending_power_off()
|
||||||
|
{
|
||||||
|
run(22, 0);
|
||||||
|
run(22, 14, 10000); // power off countdown running
|
||||||
|
TEST_ASSERT_TRUE(power.is_on);
|
||||||
|
run(22, 15, POWER_OFF_DELAY);
|
||||||
|
TEST_ASSERT_TRUE(power.is_on);
|
||||||
|
TEST_ASSERT_EQUAL(0, power.off_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_power_switch_off_clears_and_powers_off_quickly()
|
||||||
|
{
|
||||||
|
run(12, 0);
|
||||||
|
run(22, 14, 1000); // 30 s countdown already running
|
||||||
|
controller->setPowerEnabled(false);
|
||||||
|
run(22, 14, CLEAR_BEFORE_OFF_TIME + 200);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("blank", frames.last().c_str());
|
||||||
|
TEST_ASSERT_FALSE(power.is_on);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_power_switch_off_keeps_display_off_during_the_day()
|
||||||
|
{
|
||||||
|
controller->setPowerEnabled(false);
|
||||||
|
run(12, 0, 60000);
|
||||||
|
TEST_ASSERT_EQUAL(0, power.on_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_power_switch_on_again_shows_clock()
|
||||||
|
{
|
||||||
|
run(12, 0);
|
||||||
|
controller->setPowerEnabled(false);
|
||||||
|
run(12, 0, CLEAR_BEFORE_OFF_TIME + 200);
|
||||||
|
controller->setPowerEnabled(true);
|
||||||
|
run(12, 1);
|
||||||
|
TEST_ASSERT_TRUE(power.is_on);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("clock 12:01", frames.last().c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_clock_switch_off_shows_logo_then_powers_off()
|
||||||
|
{
|
||||||
|
run(12, 0);
|
||||||
|
controller->setClockEnabled(false);
|
||||||
|
run(12, 0, POWER_OFF_DELAY + 200);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("logo", frames.last().c_str());
|
||||||
|
TEST_ASSERT_EQUAL(1, (int)std::count(frames.shown.begin(), frames.shown.end(), std::string("logo")));
|
||||||
|
TEST_ASSERT_FALSE(power.is_on);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_clock_switch_off_never_powers_on()
|
||||||
|
{
|
||||||
|
controller->setClockEnabled(false);
|
||||||
|
run(12, 0, 60000);
|
||||||
|
TEST_ASSERT_EQUAL(0, power.on_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_clock_switch_on_again_shows_clock()
|
||||||
|
{
|
||||||
|
run(12, 0);
|
||||||
|
controller->setClockEnabled(false);
|
||||||
|
run(12, 0);
|
||||||
|
controller->setClockEnabled(true);
|
||||||
|
run(12, 0);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("clock 12:00", frames.last().c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_redraws_after_power_was_switched_off_elsewhere()
|
||||||
|
{
|
||||||
|
// e.g. after flashing the Nano: the Nano restarts without a frame
|
||||||
|
run(12, 0);
|
||||||
|
power.off();
|
||||||
|
run(12, 0);
|
||||||
|
TEST_ASSERT_TRUE(power.is_on);
|
||||||
|
TEST_ASSERT_EQUAL(2, (int)frames.shown.size());
|
||||||
|
TEST_ASSERT_EQUAL_STRING("clock 12:00", frames.last().c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
UNITY_BEGIN();
|
||||||
|
|
||||||
|
RUN_TEST(test_schedule_night_until_8);
|
||||||
|
RUN_TEST(test_schedule_clock_from_8_to_22);
|
||||||
|
RUN_TEST(test_schedule_quarter_hours_until_23_30);
|
||||||
|
RUN_TEST(test_schedule_night_from_23_31);
|
||||||
|
RUN_TEST(test_evening_from_17);
|
||||||
|
|
||||||
|
RUN_TEST(test_timer_expires_once_after_delay);
|
||||||
|
RUN_TEST(test_timer_keeps_earlier_deadline);
|
||||||
|
RUN_TEST(test_timer_cancel);
|
||||||
|
RUN_TEST(test_timer_across_millis_overflow);
|
||||||
|
|
||||||
|
RUN_TEST(test_led_off_while_display_off);
|
||||||
|
RUN_TEST(test_led_blinks_slow_with_wifi_and_fast_without);
|
||||||
|
|
||||||
|
RUN_TEST(test_nothing_happens_until_time_is_synced);
|
||||||
|
RUN_TEST(test_daytime_powers_on_and_shows_clock);
|
||||||
|
RUN_TEST(test_clock_is_only_sent_when_the_minute_changes);
|
||||||
|
RUN_TEST(test_night_clears_then_powers_off_after_delay);
|
||||||
|
RUN_TEST(test_night_does_not_power_on);
|
||||||
|
RUN_TEST(test_standby_keeps_the_last_time_and_powers_off);
|
||||||
|
RUN_TEST(test_quarter_hour_powers_on_again_and_redraws);
|
||||||
|
RUN_TEST(test_clock_cancels_pending_power_off);
|
||||||
|
RUN_TEST(test_power_switch_off_clears_and_powers_off_quickly);
|
||||||
|
RUN_TEST(test_power_switch_off_keeps_display_off_during_the_day);
|
||||||
|
RUN_TEST(test_power_switch_on_again_shows_clock);
|
||||||
|
RUN_TEST(test_clock_switch_off_shows_logo_then_powers_off);
|
||||||
|
RUN_TEST(test_clock_switch_off_never_powers_on);
|
||||||
|
RUN_TEST(test_clock_switch_on_again_shows_clock);
|
||||||
|
RUN_TEST(test_redraws_after_power_was_switched_off_elsewhere);
|
||||||
|
|
||||||
|
return UNITY_END();
|
||||||
|
}
|
||||||
@@ -232,7 +232,7 @@ def upload(hex_path, host, port, bauds):
|
|||||||
raise NoBootloader(
|
raise NoBootloader(
|
||||||
f"no answer from the Nano bootloader at {', '.join(map(str, bauds))} baud. "
|
f"no answer from the Nano bootloader at {', '.join(map(str, bauds))} baud. "
|
||||||
"Check the Nano TX -> ESP32 GPIO 16 wire. A Nano with Optiboot also needs "
|
"Check the Nano TX -> ESP32 GPIO 16 wire. A Nano with Optiboot also needs "
|
||||||
"the reset wire (nano_reset in src/esp32/libs.h).")
|
"the reset wire (NANO_RESET_PIN in src/esp32/config.h).")
|
||||||
flash(sock, image, baud)
|
flash(sock, image, baud)
|
||||||
print("Done. The ESP32 restarts the Nano with the new firmware.")
|
print("Done. The ESP32 restarts the Nano with the new firmware.")
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in new issue
Block a user