Public Access
Show status messages on the display
- Clock firmware: 'Connecting to WiFi...' and 'Getting the time...' at boot, 'Crashed!' in the OTA-only modes, and 'No WiFi' with the time when WiFi is down for over a minute (new DisplayController screen) - Recovery app: shows its reason, the setup WiFi name, or its IP address, then powers the display off (flipdots keep the image) - StatusScreen and drawTextLines shared by both apps - Driver: ESP begin() no longer draws 'ESP Connected' - Tests: No WiFi screen, short reason texts (70 total) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4749f9e596
commit
c9e0800e36
19 files changed
+332
-48
No files matched your search
+8
-1
@@ -25,7 +25,9 @@ debug output. Wiring is described in the [main README](../../README.md#wiring-be
|
||||
| `system/boot_target.*` | Switching between the clock firmware and the recovery app |
|
||||
| `system/system_store.*` | Settings in NVS: WiFi, recovery reason, firmware slot and version, power-on count |
|
||||
| **`display/`** | **What is drawn** |
|
||||
| `display/frames.*` | The screens: clock, logo, blank, text (`FlipdotFrames`) |
|
||||
| `display/frames.*` | The screens of the clock: clock, "No WiFi", logo, blank (`FlipdotFrames`) |
|
||||
| `display/status_screen.*` | Two-line status messages at boot, after a crash and in the recovery app (`StatusScreen`) |
|
||||
| `display/text.*` | Centered text lines on the 84 x 19 display |
|
||||
| `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`) |
|
||||
@@ -74,6 +76,11 @@ Rules for changing the code:
|
||||
|
||||
## Features
|
||||
|
||||
- **Status on the display:** `Connecting to WiFi...` and `Getting the time...` at boot,
|
||||
`Crashed!` messages in the OTA-only modes, and `No WiFi` with the time when WiFi is down
|
||||
for more than a minute. See the table in the
|
||||
[main README](../../README.md#recovery-app).
|
||||
|
||||
- **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.
|
||||
|
||||
@@ -36,6 +36,11 @@ 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;
|
||||
// Status messages (boot, crash, recovery): time for the Nano to flip all dots before the
|
||||
// power is cut again. The message stays visible without power.
|
||||
const uint32_t STATUS_FLIP_TIME = 2000;
|
||||
// WiFi down this long while the clock runs: show "No WiFi" with the time instead
|
||||
const uint32_t NO_WIFI_SHOW_TIME = 60000;
|
||||
// Power off this long after the display is no longer needed (night, standby, logo)
|
||||
const uint32_t POWER_OFF_DELAY = 30000;
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <stdio.h>
|
||||
|
||||
#include "display/bitmaps.h"
|
||||
#include "display/text.h"
|
||||
#include "logic/schedule.h"
|
||||
|
||||
// Horizontal offset that centers the 81 pixel wide clock on the 84 pixel display
|
||||
@@ -31,6 +32,14 @@ void FlipdotFrames::clock(int hour, int minute)
|
||||
_display.display();
|
||||
}
|
||||
|
||||
void FlipdotFrames::noWifi(int hour, int minute)
|
||||
{
|
||||
char time[6];
|
||||
snprintf(time, sizeof(time), "%02d:%02d", hour, minute);
|
||||
drawTextLines(_display, "No WiFi", time);
|
||||
_display.display();
|
||||
}
|
||||
|
||||
void FlipdotFrames::logo()
|
||||
{
|
||||
_display.fillScreen(0);
|
||||
@@ -43,13 +52,3 @@ 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();
|
||||
}
|
||||
@@ -11,11 +11,11 @@ public:
|
||||
|
||||
// Pikachu with a thought bubble showing HH:MM (yawning in the evening)
|
||||
void clock(int hour, int minute) override;
|
||||
// "No WiFi" with the time below it
|
||||
void noWifi(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;
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
#include "display/status_screen.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
#include "config.h"
|
||||
#include "display/text.h"
|
||||
|
||||
StatusScreen::StatusScreen(BROSE9323 &display, NanoLink &nano) : _display(display), _nano(nano)
|
||||
{
|
||||
}
|
||||
|
||||
void StatusScreen::show(const char *line1, const char *line2, bool keep_on)
|
||||
{
|
||||
Serial.printf("Display: %s / %s\n", line1, line2 ? line2 : "");
|
||||
_nano.on();
|
||||
if (!_started)
|
||||
{
|
||||
_display.begin(&_nano.stream());
|
||||
_display.setTiming(FLIP_TIME);
|
||||
_started = true;
|
||||
}
|
||||
drawTextLines(_display, line1, line2);
|
||||
_display.display();
|
||||
|
||||
if (!keep_on)
|
||||
{
|
||||
// The first frame after power on flips every dot, give the Nano time for that
|
||||
delay(STATUS_FLIP_TIME);
|
||||
_nano.off();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include "BROSE9323.h"
|
||||
#include "nano_link/nano_link.h"
|
||||
|
||||
// Short messages for situations outside the normal clock: booting, crashes, recovery app.
|
||||
// Used by both the clock firmware and the recovery app.
|
||||
class StatusScreen
|
||||
{
|
||||
public:
|
||||
StatusScreen(BROSE9323 &display, NanoLink &nano);
|
||||
|
||||
// Power the display on, show up to two lines (TEXT_LINE_CHARS each). Unless keep_on,
|
||||
// power off again once the dots have flipped: flipdots keep their image without power.
|
||||
void show(const char *line1, const char *line2, bool keep_on = false);
|
||||
|
||||
private:
|
||||
BROSE9323 &_display;
|
||||
NanoLink &_nano;
|
||||
bool _started = false;
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
#include "display/text.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
static const int CHAR_WIDTH = 6;
|
||||
|
||||
static void drawCentered(BROSE9323 &display, const char *text, int y)
|
||||
{
|
||||
int len = strlen(text);
|
||||
if (len > TEXT_LINE_CHARS)
|
||||
len = TEXT_LINE_CHARS;
|
||||
char line[TEXT_LINE_CHARS + 1];
|
||||
memcpy(line, text, len);
|
||||
line[len] = '\0';
|
||||
|
||||
display.setCursor((display.width() - len * CHAR_WIDTH) / 2, y);
|
||||
display.print(line);
|
||||
}
|
||||
|
||||
void drawTextLines(BROSE9323 &display, const char *line1, const char *line2)
|
||||
{
|
||||
display.fillScreen(0);
|
||||
display.setTextSize(1);
|
||||
display.setTextWrap(false);
|
||||
display.setTextColor(1);
|
||||
if (line2 == nullptr || line2[0] == '\0')
|
||||
{
|
||||
drawCentered(display, line1, 6); // one line: vertically centered
|
||||
return;
|
||||
}
|
||||
drawCentered(display, line1, 1);
|
||||
drawCentered(display, line2, 10);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "BROSE9323.h"
|
||||
|
||||
// Characters per line with the built-in 6x8 font on the 84 pixel wide display
|
||||
const int TEXT_LINE_CHARS = 14;
|
||||
|
||||
// Clear the screen and draw up to two centered lines of text (line2 may be empty).
|
||||
// Does not call display().
|
||||
void drawTextLines(BROSE9323 &display, const char *line1, const char *line2);
|
||||
@@ -54,6 +54,24 @@ const char *recoveryReasonText(RecoveryReason reason)
|
||||
}
|
||||
}
|
||||
|
||||
const char *recoveryReasonShort(RecoveryReason reason)
|
||||
{
|
||||
switch (reason)
|
||||
{
|
||||
case RecoveryReason::Wifi:
|
||||
return "no WiFi";
|
||||
case RecoveryReason::Crash:
|
||||
return "crashed";
|
||||
case RecoveryReason::Manual:
|
||||
return "requested";
|
||||
case RecoveryReason::PowerCycle:
|
||||
return "power cycled";
|
||||
case RecoveryReason::None:
|
||||
default:
|
||||
return "no firmware";
|
||||
}
|
||||
}
|
||||
|
||||
bool powerCyclesRequestRecovery(uint8_t power_on_count)
|
||||
{
|
||||
return power_on_count >= POWER_CYCLES_FOR_RECOVERY;
|
||||
|
||||
@@ -59,6 +59,8 @@ enum class RecoveryReason : uint8_t
|
||||
};
|
||||
|
||||
const char *recoveryReasonText(RecoveryReason reason);
|
||||
// At most 14 characters, for the flipdot display
|
||||
const char *recoveryReasonShort(RecoveryReason reason);
|
||||
|
||||
// Power-on count: each power on within POWER_CYCLE_WINDOW of the previous one counts.
|
||||
// True when the next start should be the recovery app.
|
||||
|
||||
@@ -27,8 +27,17 @@ bool DisplayController::powerEnabled() const
|
||||
return _power_enabled;
|
||||
}
|
||||
|
||||
void DisplayController::update(uint32_t now, bool time_valid, int hour, int minute)
|
||||
void DisplayController::update(uint32_t now, bool time_valid, int hour, int minute, bool wifi_connected)
|
||||
{
|
||||
if (wifi_connected)
|
||||
_wifi_lost = false;
|
||||
else if (!_wifi_lost)
|
||||
{
|
||||
_wifi_lost = true;
|
||||
_wifi_lost_since = now;
|
||||
}
|
||||
bool show_no_wifi = _wifi_lost && now - _wifi_lost_since >= NO_WIFI_SHOW_TIME;
|
||||
|
||||
// Switched off elsewhere (power timer, Nano flashing): the Nano lost its frame
|
||||
if (!_power.isOn())
|
||||
_screen = Screen::Unknown;
|
||||
@@ -81,7 +90,7 @@ void DisplayController::update(uint32_t now, bool time_valid, int hour, int minu
|
||||
_power.on();
|
||||
_screen = Screen::Unknown;
|
||||
}
|
||||
_showClock(hour, minute);
|
||||
_showClock(hour, minute, show_no_wifi);
|
||||
break;
|
||||
case ScheduleMode::Standby:
|
||||
// Flipdots keep their image without power, so the last time stays visible
|
||||
@@ -91,12 +100,16 @@ void DisplayController::update(uint32_t now, bool time_valid, int hour, int minu
|
||||
}
|
||||
}
|
||||
|
||||
void DisplayController::_showClock(int hour, int minute)
|
||||
void DisplayController::_showClock(int hour, int minute, bool no_wifi)
|
||||
{
|
||||
if (_screen == Screen::Clock && _shown_hour == hour && _shown_minute == minute)
|
||||
Screen screen = no_wifi ? Screen::NoWifi : Screen::Clock;
|
||||
if (_screen == screen && _shown_hour == hour && _shown_minute == minute)
|
||||
return;
|
||||
_frames.clock(hour, minute);
|
||||
_screen = Screen::Clock;
|
||||
if (no_wifi)
|
||||
_frames.noWifi(hour, minute);
|
||||
else
|
||||
_frames.clock(hour, minute);
|
||||
_screen = screen;
|
||||
_shown_hour = hour;
|
||||
_shown_minute = minute;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ class Frames
|
||||
{
|
||||
public:
|
||||
virtual void clock(int hour, int minute) = 0;
|
||||
// Shown instead of the clock while WiFi is down (the time keeps running without it)
|
||||
virtual void noWifi(int hour, int minute) = 0;
|
||||
virtual void logo() = 0;
|
||||
virtual void blank() = 0;
|
||||
};
|
||||
@@ -40,13 +42,15 @@ public:
|
||||
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);
|
||||
// While WiFi is down for NO_WIFI_SHOW_TIME, "No WiFi" is shown instead of the clock.
|
||||
void update(uint32_t now, bool time_valid, int hour, int minute, bool wifi_connected);
|
||||
|
||||
private:
|
||||
enum class Screen
|
||||
{
|
||||
Unknown, // nothing sent since the last power on
|
||||
Clock,
|
||||
NoWifi,
|
||||
Logo,
|
||||
Blank,
|
||||
};
|
||||
@@ -62,7 +66,10 @@ private:
|
||||
int _shown_hour = -1;
|
||||
int _shown_minute = -1;
|
||||
|
||||
void _showClock(int hour, int minute);
|
||||
bool _wifi_lost = false;
|
||||
uint32_t _wifi_lost_since = 0;
|
||||
|
||||
void _showClock(int hour, int minute, bool no_wifi);
|
||||
void _showLogo();
|
||||
void _showBlank();
|
||||
};
|
||||
+13
-8
@@ -7,6 +7,7 @@
|
||||
#include "BROSE9323.h"
|
||||
#include "config.h"
|
||||
#include "display/frames.h"
|
||||
#include "display/status_screen.h"
|
||||
#include "logic/display_controller.h"
|
||||
#include "logic/status_led.h"
|
||||
#include "nano_link/nano_flash_bridge.h"
|
||||
@@ -19,6 +20,7 @@
|
||||
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);
|
||||
StatusScreen status(display, nano);
|
||||
DisplayController controller(nano, frames);
|
||||
|
||||
// Flashing the Nano over WiFi: restart it into its bootloader, switch it off afterwards.
|
||||
@@ -32,12 +34,14 @@ void setup()
|
||||
{
|
||||
Serial.begin(115200);
|
||||
pinMode(LED_PIN, OUTPUT);
|
||||
nano.begin(); // display off until the main code runs
|
||||
nano.begin(); // display off until something is shown
|
||||
|
||||
// Decided first, before anything that could crash
|
||||
BootMode boot_mode = firmwareGuardBegin();
|
||||
|
||||
wifiBegin();
|
||||
if (boot_mode == BootMode::Normal)
|
||||
status.show("Connecting", "to WiFi...", true);
|
||||
wifiBegin(); // starts the recovery app when it fails
|
||||
otaBegin();
|
||||
|
||||
if (boot_mode == BootMode::SafeMode)
|
||||
@@ -46,19 +50,20 @@ void setup()
|
||||
// then try the main code once more
|
||||
firmwareGuardSafeModeEnded();
|
||||
rebootToRecovery(RecoveryReason::Crash);
|
||||
status.show("Crashed 3x", "OTA only");
|
||||
otaOnlyMode(SAFE_MODE_TIME, LED_PIN);
|
||||
ESP.restart();
|
||||
}
|
||||
if (boot_mode == BootMode::PanicWindow)
|
||||
{
|
||||
status.show("Crashed!", "OTA open 15s");
|
||||
otaOnlyMode(OTA_PANIC_WINDOW, LED_PIN);
|
||||
}
|
||||
|
||||
flashBridge.begin();
|
||||
|
||||
nano.on();
|
||||
display.begin(&nano.stream());
|
||||
display.setTiming(FLIP_TIME);
|
||||
// Shown until the time is synced
|
||||
frames.message("Connecting...");
|
||||
// Shown until the time is synced over NTP
|
||||
status.show("Getting", "the time...", true);
|
||||
|
||||
configTzTime(TIMEZONE, NTP_SERVER);
|
||||
homeAssistantBegin(controller);
|
||||
@@ -82,7 +87,7 @@ void loop()
|
||||
|
||||
struct tm now = {};
|
||||
bool time_valid = getLocalTime(&now, 0);
|
||||
controller.update(millis(), time_valid, now.tm_hour, now.tm_min);
|
||||
controller.update(millis(), time_valid, now.tm_hour, now.tm_min, wifiConnected());
|
||||
|
||||
digitalWrite(LED_PIN, statusLedLevel(millis(), wifiConnected(), nano.isOn()));
|
||||
}
|
||||
Reference in new issue
Block a user