CDC Badge OS
Firmware for the CDC Badge v1.0 hardware security key
Loading...
Searching...
No Matches
EpaperDisplay.cpp
Go to the documentation of this file.
1
8
9#include "cdc_hal/IDisplay.h"
10#include "cdc_hal/hw_config.h"
11#include "cdc_core/Raii.h"
12#include "cdc_core/EventBus.h"
14#include "cdc_log.h"
15#include "driver/ledc.h"
16#include "nvs_flash.h"
17#include "nvs.h"
18#include "freertos/FreeRTOS.h"
19#include "freertos/task.h"
20#include "freertos/semphr.h"
21#include <goodisplay/gdey029T94.h>
22#include <Fonts/FreeMonoBold12pt7b.h>
23#include <Fonts/FreeMonoBold9pt7b.h>
24#include <cstring>
25#include <cstdarg>
26
27static const char* TAG = "EpaperDisplay";
28
30static constexpr const char* SPLASH_TITLE = "CDC Badge";
31static constexpr const char* SPLASH_VERSION = "v" APP_VERSION;
32
33namespace cdc::hal {
34
36static constexpr ledc_timer_t LEDC_TIMER = LEDC_TIMER_0;
37static constexpr ledc_mode_t LEDC_MODE = LEDC_LOW_SPEED_MODE;
38static constexpr ledc_channel_t LEDC_CHANNEL = LEDC_CHANNEL_0;
39static constexpr ledc_timer_bit_t LEDC_DUTY_RES = LEDC_TIMER_10_BIT;
40static constexpr uint32_t LEDC_FREQUENCY = 10000;
41
43static constexpr const char* NVS_NAMESPACE = "display";
44static constexpr const char* NVS_KEY_BACKLIGHT = "backlight";
45
47static constexpr uint16_t WIDTH = 296;
48static constexpr uint16_t HEIGHT = 128;
49static constexpr uint16_t BACKLIGHT_DEFAULT = 512;
50static constexpr uint16_t BACKLIGHT_MAX = 1023;
51
53static EpdSpi* s_epd_spi = nullptr;
54static Gdey029T94* s_epd_display = nullptr;
55
57static bool s_initialized = false;
59static bool s_backlightOn = true;
60
61// Serialises writes to the backlight state (s_backlightOn / s_backlightLevel)
62// and the LEDC duty across the UI, fido2 and ctaphid tasks.
63static SemaphoreHandle_t s_backlightMutex = nullptr;
64
66static SemaphoreHandle_t s_renderMutex = nullptr;
67static TaskHandle_t s_renderTask = nullptr;
68static volatile bool s_renderPending = false;
70
71// Serialises the actual SSD1680 SPI transfer. The render task and any
72// synchronous flushSync() caller (e.g. a plugin host edit running on the
73// plg_tick task) may both drive the panel; the driver is not reentrant.
74static SemaphoreHandle_t s_panelMutex = nullptr;
75
76// The SSD1680 accumulates ghosting across consecutive partial updates and
77// eventually stops applying new partials cleanly. Refreshes escalate in two
78// stages to bound ghosting with minimal visible flashing: after
79// FEATURE_EPD_MAX_PARTIALS_BEFORE_FAST partials the next refresh is promoted
80// to FAST (single flash), and after FEATURE_EPD_MAX_FASTS_BEFORE_FULL fast
81// refreshes the next one is promoted to FULL (multi-flash OTP waveform).
82// Both counters are guarded by s_panelMutex.
83static_assert(FEATURE_EPD_MAX_PARTIALS_BEFORE_FAST > 0, "partial threshold must be positive");
84static_assert(FEATURE_EPD_MAX_FASTS_BEFORE_FULL > 0, "fast threshold must be positive");
85static uint16_t s_partialsSinceFast = 0;
86static uint16_t s_fastsSinceFull = 0;
89
90// Decide the effective refresh mode, escalating PARTIAL -> FAST -> FULL to
91// clear ghosting. Caller must hold s_panelMutex.
93 // Light partials (e.g. the lock-screen clock) never promote and do not
94 // advance the ghost counters; they stay partial until a FAST/FULL clears them.
95 if (mode == RefreshMode::PARTIAL_LIGHT) {
96 return mode;
97 }
98 if (mode == RefreshMode::PARTIAL) {
101 return mode;
102 }
103 mode = RefreshMode::FAST; // falls through to the FAST accounting below
104 }
105 if (mode == RefreshMode::FAST) {
108 // A FAST refresh does not fully DC-balance the film, so it does
109 // not reset the full-refresh counter.
111#if FEATURE_EPD_FAST_REFRESH
112 return mode;
113#else
114 return RefreshMode::FULL; // hardware fast waveform disabled
115#endif
116 }
117 mode = RefreshMode::FULL;
118 }
121 return RefreshMode::FULL;
122}
123
124// Coalescing rank for queued async refreshes: when several flush() calls
125// collapse into one render, the stronger mode wins.
126// FULL > FAST > PARTIAL > PARTIAL_LIGHT.
127static int refreshStrength(RefreshMode mode) {
128 switch (mode) {
129 case RefreshMode::FULL: return 3;
130 case RefreshMode::FAST: return 2;
131 case RefreshMode::PARTIAL: return 1;
132 case RefreshMode::PARTIAL_LIGHT: return 0;
133 }
134 return 1;
135}
136
137// Drive the panel with the already-resolved refresh mode. Caller must hold
138// s_panelMutex.
139static void driveRefresh(RefreshMode resolved) {
140 switch (resolved) {
142 s_epd_display->update();
143 break;
145 s_epd_display->updateFast();
146 break;
147 default:
148 // updateWindow takes physical coordinates (128 x 296). HAL
149 // WIDTH/HEIGHT are logical post-rotation values; swap them and
150 // pass using_rotation=false.
151 s_epd_display->updateWindow(0, 0, HEIGHT, WIDTH, false);
152 break;
153 }
154}
155
160static void applyBacklight(uint16_t level) {
161 ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, level);
162 ledc_update_duty(LEDC_MODE, LEDC_CHANNEL);
163}
164
168static void loadBacklight() {
169 nvs_handle_t nvs;
170 if (nvs_open(NVS_NAMESPACE, NVS_READONLY, &nvs) == ESP_OK) {
171 uint16_t saved = BACKLIGHT_DEFAULT;
172 if (nvs_get_u16(nvs, NVS_KEY_BACKLIGHT, &saved) == ESP_OK) {
173 s_backlightLevel = (saved > BACKLIGHT_MAX) ? BACKLIGHT_MAX : saved;
174 }
175 nvs_close(nvs);
176 }
177}
178
183static void persistBacklight(uint16_t level) {
184 nvs_handle_t nvs;
185 if (nvs_open(NVS_NAMESPACE, NVS_READWRITE, &nvs) == ESP_OK) {
186 nvs_set_u16(nvs, NVS_KEY_BACKLIGHT, level);
187 nvs_commit(nvs);
188 nvs_close(nvs);
189 LOG_D(TAG, "Backlight saved to NVS: %u", level);
190 }
191}
192
197static void renderTask(void* arg) {
198 while (true) {
199 ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
200
201 RefreshMode mode;
202 {
204 mode = s_renderMode;
205 s_renderPending = false;
206 }
207
208 if (s_epd_display) {
210 const RefreshMode resolved = resolveRefresh(mode);
211 // A FAST/FULL waveform leaves the panel unreadable for its whole
212 // duration; bracket it with a DISPLAY_REFRESH begin/end so plugins
213 // can pause. PARTIAL refreshes are transparent and stay silent.
214 const bool heavy =
215 (resolved == RefreshMode::FULL || resolved == RefreshMode::FAST);
216 if (heavy) {
219 }
220 driveRefresh(resolved);
221 if (heavy) {
224 }
225 }
226 }
227}
228
232class EpaperDisplay : public IDisplay {
233public:
234 bool init() override;
235 bool start() override;
236 void stop() override;
237 core::ServiceState getState() const override { return state_; }
238 const char* getName() const override { return "display"; }
239
240 void clear() override;
241 void flush(RefreshMode mode) override;
242 void flushSync(RefreshMode mode) override;
243 bool isBusy() const override { return s_renderPending; }
244 uint16_t getWidth() const override { return WIDTH; }
245 uint16_t getHeight() const override { return HEIGHT; }
246 void setBacklight(uint16_t level) override;
247 void saveBacklight() override;
248 uint16_t getBacklight() const override { return s_backlightLevel; }
249 bool isBacklightOn() const override { return s_backlightOn && s_backlightLevel > 0; }
250 void backlightOn() override;
251 void backlightOff() override;
252 void* getNativeHandle() override { return s_epd_display; }
253 void showSplash(const char* subtitle = nullptr) override;
254
255 // GFX Drawing Methods
256 void drawPixel(int16_t x, int16_t y, uint16_t color) override;
257 void drawLine(int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint16_t color) override;
258 void drawRect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color) override;
259 void fillRect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color) override;
260 void setCursor(int16_t x, int16_t y) override;
261 void setTextColor(uint16_t color) override;
262 void setTextSize(uint8_t size) override;
263 void setFont(const void* font) override;
264 void print(const char* text) override;
265 void printf(const char* fmt, ...) override;
266
267private:
269};
270
276 if (s_initialized) {
277 return true;
278 }
279
280 LOG_I(TAG, "Initializing E-Paper display...");
281
282 // Load backlight from NVS
284
285 // Configure backlight PWM
286 ledc_timer_config_t ledcTimer = {
287 .speed_mode = LEDC_MODE,
288 .duty_resolution = LEDC_DUTY_RES,
289 .timer_num = LEDC_TIMER,
290 .freq_hz = LEDC_FREQUENCY,
291 .clk_cfg = LEDC_USE_XTAL_CLK,
292 .deconfigure = false
293 };
294 ledc_timer_config(&ledcTimer);
295
296 ledc_channel_config_t ledcChannel = {
297 .gpio_num = EPD_LED_PIN,
298 .speed_mode = LEDC_MODE,
299 .channel = LEDC_CHANNEL,
300 .intr_type = LEDC_INTR_DISABLE,
301 .timer_sel = LEDC_TIMER,
302 .duty = s_backlightLevel,
303 .hpoint = 0,
304 .sleep_mode = LEDC_SLEEP_MODE_KEEP_ALIVE,
305 .flags = {.output_invert = 0}
306 };
307 ledc_channel_config(&ledcChannel);
308
309 LOG_I(TAG, "Backlight configured");
310
311 // LAZY create display objects
312 if (!s_epd_spi) {
313 s_epd_spi = new EpdSpi();
314 }
315 if (!s_epd_display) {
316 s_epd_display = new Gdey029T94(*s_epd_spi);
317 }
318
319 // Initialize display
320 s_epd_display->init(false);
321 s_epd_display->setRotation(1);
322 s_epd_display->setMonoMode(true);
323 s_epd_display->cp437(true);
324 s_epd_display->fillScreen(EPD_WHITE);
325
326 LOG_I(TAG, "Display hardware initialized");
327
328 // Create render task
329 s_renderMutex = xSemaphoreCreateMutex();
330 s_panelMutex = xSemaphoreCreateMutex();
331 s_backlightMutex = xSemaphoreCreateMutex();
333 LOG_E(TAG, "Failed to create render mutex");
335 return false;
336 }
337
338 BaseType_t ret = xTaskCreate(renderTask, "epd_render", 4096, nullptr, 5, &s_renderTask);
339 if (ret != pdPASS) {
340 LOG_E(TAG, "Failed to create render task");
342 return false;
343 }
344
345 s_initialized = true;
347 LOG_I(TAG, "Display initialized (%ux%u), backlight=%u", WIDTH, HEIGHT, s_backlightLevel);
348 return true;
349}
350
356 if (state_ == core::ServiceState::INITIALIZED) {
358 s_backlightOn = true;
360 return true;
361 }
362 return state_ == core::ServiceState::STARTED;
363}
364
369 if (state_ == core::ServiceState::STARTED) {
370 s_backlightOn = false;
373 }
374}
375
380 if (s_epd_display) {
381 s_epd_display->fillScreen(EPD_WHITE);
382 }
383}
384
390 // If no render task, fall back to sync
391 if (!s_renderTask) {
392 flushSync(mode);
393 return;
394 }
395
396 {
398 if (s_renderPending) {
399 // Coalesce with the already-queued refresh: keep the stronger mode.
401 } else {
402 s_renderPending = true;
403 s_renderMode = mode;
404 }
405 }
406
407 xTaskNotifyGive(s_renderTask);
408}
409
419
424void EpaperDisplay::setBacklight(uint16_t level) {
425 if (level > BACKLIGHT_MAX) level = BACKLIGHT_MAX;
427 s_backlightLevel = level;
428 // Always apply immediately for live preview (e.g., brightness slider)
429 // Also turn on backlight if level > 0
430 if (level > 0) {
431 s_backlightOn = true;
432 }
433 applyBacklight(level);
434}
435
442
452
458 s_backlightOn = false;
460 LOG_I(TAG, "Backlight OFF");
461}
462
467void EpaperDisplay::showSplash(const char* subtitle) {
468 if (!s_epd_display) return;
469
470 LOG_I(TAG, "Showing splash screen");
471
472 s_epd_display->fillScreen(EPD_BLACK);
473 s_epd_display->setTextColor(EPD_WHITE);
474
475 // App name - large, centered
476 s_epd_display->setFont(&FreeMonoBold12pt7b);
477 int16_t x1, y1;
478 uint16_t w, h;
479 s_epd_display->getTextBounds(SPLASH_TITLE, 0, 0, &x1, &y1, &w, &h);
480 int name_x = (s_epd_display->width() - w) / 2;
481 s_epd_display->setCursor(name_x, 55);
483
484 // Subtitle or version - smaller, centered below name
485 const char* sub = subtitle ? subtitle : SPLASH_VERSION;
486 s_epd_display->setFont(&FreeMonoBold9pt7b);
487 s_epd_display->getTextBounds(sub, 0, 0, &x1, &y1, &w, &h);
488 int ver_x = (s_epd_display->width() - w) / 2;
489 s_epd_display->setCursor(ver_x, 80);
490 s_epd_display->print(sub);
491
492 // Small text at bottom - built-in font (6x8)
493 s_epd_display->setFont(nullptr);
494 s_epd_display->setTextSize(1);
495
496 // Build date
497 char build_str[20];
498 snprintf(build_str, sizeof(build_str), "%.3s%2.2s %.5s", __DATE__, __DATE__ + 4, __TIME__);
499 s_epd_display->setCursor(2, 120);
500 s_epd_display->print(build_str);
501
502 // Right: tagline
503 const char* status_text = "Open Hardware Security";
504 int status_x = s_epd_display->width() - (strlen(status_text) * 6) - 2;
505 s_epd_display->setCursor(status_x, 120);
506 s_epd_display->print(status_text);
507
508 // Full refresh (blocking)
509 s_epd_display->update();
510 LOG_I(TAG, "Splash screen displayed");
511}
512
514
521void EpaperDisplay::drawPixel(int16_t x, int16_t y, uint16_t color) {
522 if (s_epd_display) s_epd_display->drawPixel(x, y, color);
523}
524
533void EpaperDisplay::drawLine(int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint16_t color) {
534 if (s_epd_display) s_epd_display->drawLine(x0, y0, x1, y1, color);
535}
536
545void EpaperDisplay::drawRect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color) {
546 if (s_epd_display) s_epd_display->drawRect(x, y, w, h, color);
547}
548
557void EpaperDisplay::fillRect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color) {
558 if (s_epd_display) s_epd_display->fillRect(x, y, w, h, color);
559}
560
566void EpaperDisplay::setCursor(int16_t x, int16_t y) {
567 if (s_epd_display) s_epd_display->setCursor(x, y);
568}
569
574void EpaperDisplay::setTextColor(uint16_t color) {
575 if (s_epd_display) s_epd_display->setTextColor(color);
576}
577
582void EpaperDisplay::setTextSize(uint8_t size) {
583 if (s_epd_display) s_epd_display->setTextSize(size);
584}
585
590void EpaperDisplay::setFont(const void* font) {
591 if (s_epd_display) s_epd_display->setFont(static_cast<const GFXfont*>(font));
592}
593
598void EpaperDisplay::print(const char* text) {
599 if (s_epd_display && text) s_epd_display->print(text);
600}
601
606void EpaperDisplay::printf(const char* fmt, ...) {
607 if (!s_epd_display || !fmt) return;
608 char buf[128];
609 va_list args;
610 va_start(args, fmt);
611 vsnprintf(buf, sizeof(buf), fmt, args);
612 va_end(args);
613 s_epd_display->print(buf);
614}
615
617static EpaperDisplay* s_display = nullptr;
618
624 if (!s_display) {
625 s_display = new EpaperDisplay();
626 }
627 return s_display;
628}
629
630void winkBacklight(uint8_t count, uint16_t period_ms) {
631 if (count == 0) count = 1;
632 if (count > 10) count = 10;
633 if (period_ms < 50) period_ms = 50;
634 if (period_ms > 1000) period_ms = 1000;
635
636 auto* display = getDisplayInstance();
637 if (!display) return;
638
639 // Pulse the LEDC duty only; never touch the logical state (s_backlightOn),
640 // so a concurrent owner (lock screen / FIDO2 prompt) stays authoritative.
641 const TickType_t ticks = pdMS_TO_TICKS(period_ms);
642 for (uint8_t i = 0; i < count; ++i) {
644 vTaskDelay(ticks);
646 vTaskDelay(ticks);
647 }
648
649 // Re-sync the physical duty to the current logical state.
652}
653
654} // namespace cdc::hal
static const char * TAG
static constexpr const char * SPLASH_VERSION
static constexpr const char * SPLASH_TITLE
Splash-screen text defaults.
Shared RAII wrappers for firmware resources.
CDC Log: logging over TinyUSB CDC and UART.
#define LOG_D(tag, fmt,...)
Definition cdc_log.h:148
#define LOG_I(tag, fmt,...)
Definition cdc_log.h:147
#define LOG_E(tag, fmt,...)
Definition cdc_log.h:145
static EventBus & instance()
Returns singleton event-bus instance.
Definition EventBus.cpp:19
bool publish(const Event &event, bool fromISR=false)
Publishes an event to the queue.
Definition EventBus.cpp:88
RAII wrapper for a FreeRTOS semaphore / mutex.
Definition Raii.h:181
const char * getName() const override
void stop() override
Stops display service and disables backlight.
void drawLine(int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint16_t color) override
Draws a line on framebuffer.
uint16_t getWidth() const override
void backlightOn() override
Enables backlight using current configured level.
core::ServiceState getState() const override
bool isBacklightOn() const override
void setTextSize(uint8_t size) override
Sets active text scale.
void saveBacklight() override
Persists current backlight level.
uint16_t getHeight() const override
void showSplash(const char *subtitle=nullptr) override
Renders and displays boot splash screen.
void setFont(const void *font) override
Sets active font pointer.
void clear() override
Clears framebuffer to white.
void drawPixel(int16_t x, int16_t y, uint16_t color) override
Adafruit-GFX method implementations.
void fillRect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color) override
Draws filled rectangle on framebuffer.
void print(const char *text) override
Prints text at current cursor position.
void flushSync(RefreshMode mode) override
Performs synchronous display refresh.
uint16_t getBacklight() const override
void setBacklight(uint16_t level) override
Sets current backlight level and applies immediately.
void backlightOff() override
Disables backlight output.
bool start() override
Starts display service and enables backlight.
void * getNativeHandle() override
void setCursor(int16_t x, int16_t y) override
Sets text cursor position.
bool isBusy() const override
void flush(RefreshMode mode) override
Requests asynchronous display refresh.
void drawRect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color) override
Draws rectangle outline on framebuffer.
void setTextColor(uint16_t color) override
Sets active text color.
void printf(const char *fmt,...) override
Formatted print helper for display text output.
bool init() override
Initializes display hardware, backlight, and render task.
#define FEATURE_EPD_MAX_FASTS_BEFORE_FULL
#define FEATURE_EPD_MAX_PARTIALS_BEFORE_FAST
#define EPD_LED_PIN
Definition hw_config.h:28
static bool s_backlightOn
static volatile RefreshMode s_renderMode
static int refreshStrength(RefreshMode mode)
static constexpr ledc_mode_t LEDC_MODE
IDisplay * getDisplayInstance()
Returns lazily created singleton display instance.
static constexpr uint16_t kMaxPartialsBeforeFast
static constexpr uint32_t LEDC_FREQUENCY
static uint16_t s_partialsSinceFast
static constexpr ledc_timer_t LEDC_TIMER
LEDC backlight PWM configuration constants.
static constexpr uint16_t WIDTH
Display timing and geometry constants.
static constexpr uint16_t HEIGHT
static EpaperDisplay * s_display
Lazily created singleton display instance.
static void driveRefresh(RefreshMode resolved)
static constexpr uint16_t BACKLIGHT_MAX
static void renderTask(void *arg)
Render worker task processing async flush requests.
static constexpr uint16_t BACKLIGHT_DEFAULT
static SemaphoreHandle_t s_renderMutex
Render-task runtime state.
static constexpr uint16_t kMaxFastsBeforeFull
static bool s_initialized
Mutable display state cache.
static void applyBacklight(uint16_t level)
Applies backlight PWM duty level.
void winkBacklight(uint8_t count=2, uint16_t period_ms=150)
Blink the backlight as a visual "look at me" signal.
static constexpr const char * NVS_NAMESPACE
NVS namespace and keys for display settings.
static EpdSpi * s_epd_spi
Lazily initialized display objects to avoid global constructors.
static volatile bool s_renderPending
static constexpr ledc_channel_t LEDC_CHANNEL
static uint16_t s_backlightLevel
static Gdey029T94 * s_epd_display
static void persistBacklight(uint16_t level)
Persists backlight level to NVS.
static TaskHandle_t s_renderTask
static RefreshMode resolveRefresh(RefreshMode mode)
static constexpr const char * NVS_KEY_BACKLIGHT
static void loadBacklight()
Loads persisted backlight level from NVS.
static SemaphoreHandle_t s_panelMutex
static SemaphoreHandle_t s_backlightMutex
static constexpr ledc_timer_bit_t LEDC_DUTY_RES
static uint16_t s_fastsSinceFull
#define APP_VERSION