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:
mangavalkandClaude Opus 5.5 committed 2026-10-13 00:56:32 +02:00
1 parent cefb76c1ad
commit 683a47c055
29 files changed
+1289 -755

No files matched your search

+52 -23
View File
@@ -1,6 +1,6 @@
# 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
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 |
|------|----------|
| `main.cpp` | setup, OTA, clock / power logic, Home Assistant callbacks |
| `libs.h` | includes and global objects (display, MQTT, WiFi, pins, timezone) |
| `bitmaps.h` | 1-bit bitmaps (Pikachu, thought bubble, Nintendo logo, ...) |
| `nano_flash_bridge.h/.cpp` | WiFi to serial bridge for flashing the Nano over WiFi (TCP port 2323) |
| `secrets.example.h` | template for `secrets.h` (WiFi and MQTT credentials, **gitignored**) |
| `main.cpp` | Creates the objects, `setup()` and `loop()` |
| `config.h` | All settings: pins, timings, timezone, Home Assistant device id |
| `credentials.h` | Includes `secrets.h` (WiFi and MQTT credentials, **gitignored**); template: `secrets.example.h` |
| **`logic/`** | **Pure logic without Arduino code, tested on the PC** |
| `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
- **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 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
can still be replaced over the air.
- **NTP time** with timezone `CET-1CEST,M3.5.0,M10.5.0/3` (Europe/Amsterdam).
- **Home Assistant** through MQTT (`home-assistant-integration` library) with two switches:
- `FlipDotDisplayClock`: clock on, or the Nintendo logo when off
- `FlipDotDisplayPower`: turns the display off through the relay
handler for 15 seconds before it starts the main code, with the LED blinking fast. A
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). Until the
time is synced, the display shows "Connecting...".
- **Home Assistant** through MQTT (`home-assistant-integration` library) with two switches.
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
on.
- **On:** the ESP waits for the Nano's `READY` message, at most 3 s
(`NANO_BOOT_TIMEOUT`), before it sends frames. The relay stays on while it is needed;
the ESP only waits when it actually switches the relay on.
- **Off:** the ESP stops sending and releases its TX pin (`Serial.end()` and the pin set
to input). An idle UART line is high and would otherwise power the Nano through its RX
pin.
(`NANO_BOOT_TIMEOUT`), before it sends frames.
- **Off:** the ESP releases its TX pin (`Serial2.end()` and the pin set to input). An
idle UART line is high and would otherwise power the Nano through its RX pin.
- **Nano flash bridge:** `pio run -e nano_ota -t upload` flashes the Nano through the ESP32.
While a flash session runs, the clock is paused. See the
[main README](../../README.md#flashing-the-nano-over-wifi).
- **Status LED** (built-in) blinks fast when WiFi is down and slowly when connected. It is
off at night or when the display is powered off.
- **Status LED** (built-in): off while the display is off, slow blink with WiFi, fast blink
without WiFi.
## 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 |
| 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 |
| 23:31 – 08:00 | Screen cleared, power off |
| 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 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
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
`display.drawBitmap(x, y, bitmap, w, h, 1)`. The display is 84 × 19 pixels.
horizontal, 1 bit per pixel. Paste the array into `display/bitmaps.h` and draw it in
`display/frames.cpp` with `_display.drawBitmap(x, y, bitmap, w, h, 1)`. The display is
84 × 19 pixels.
+69
View File
@@ -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"
+8
View File
@@ -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.
+55
View File
@@ -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();
}
+22
View File
@@ -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;
};
-90
View File
@@ -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;
+118
View File
@@ -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;
}
+68
View File
@@ -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();
};
+33
View File
@@ -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;
}
+22
View File
@@ -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;
};
+21
View File
@@ -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;
}
+21
View File
@@ -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);
+13
View File
@@ -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;
}
+47 -604
View File
@@ -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;
int triggered_power_off = false;
int turn_hassio_power_off = false;
#include "BROSE9323.h"
#include "config.h"
#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
void waitForNano()
{
char line[16];
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);
}
}
NanoLink nano(Serial2, NANO_RX_PIN, NANO_TX_PIN, RELAY_PIN, NANO_RESET_PIN);
BROSE9323 display(DISPLAY_WIDTH, DISPLAY_HEIGHT, PANEL_WIDTH);
FlipdotFrames frames(display);
DisplayController controller(nano, frames);
void relayOn()
{
// cancel a pending power off countdown
triggered_power_off = false;
if (relay_on)
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;
}
// 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.
NanoFlashBridge flashBridge(
NANO_FLASH_PORT, Serial2,
[](uint32_t baud) { nano.startBootloader(baud); },
[]() { nano.off(); });
void setup()
{
otaPanicMode = true;
pinMode(led1, OUTPUT);
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
nano.begin();
Serial.begin(115200);
wifiBegin();
otaBegin();
flashBridge.begin();
otaPanicWindow(LED_PIN);
WiFi.setHostname("FlipDotDisplay");
wifiMulti.addAP(ssid, password);
nano.on();
display.begin(&nano.stream());
display.setTiming(FLIP_TIME);
// Shown until the time is synced
frames.message("Connecting...");
Serial.println("Booting");
WiFi.mode(WIFI_STA);
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();
configTzTime(TIMEZONE, NTP_SERVER);
homeAssistantBegin(controller);
}
void loop()
{
ArduinoOTA.handle();
mqtt.loop();
otaLoop();
homeAssistantLoop();
// While the Nano is being flashed, nothing else may talk to it
if (nanoFlashBridge.handle())
return;
// While the Nano is being flashed, nothing else may talk to it
if (flashBridge.handle())
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;
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;
}
}
digitalWrite(LED_PIN, statusLedLevel(millis(), wifiConnected(), nano.isOn()));
}
@@ -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)())
: _server(port), _serial(serial), _onStart(onStart), _onEnd(onEnd)
File renamed without changes.
+116
View File
@@ -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);
}
}
+36
View File
@@ -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();
};
+55
View File
@@ -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();
}
+9
View File
@@ -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();
+70
View File
@@ -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);
}
+14
View File
@@ -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
View File
@@ -19,7 +19,7 @@ The display driver itself is in [lib/BROSE9323](../../lib/BROSE9323/README.md).
## 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:
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