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_log.h"
13#include "driver/ledc.h"
14#include "nvs_flash.h"
15#include "nvs.h"
16#include "freertos/FreeRTOS.h"
17#include "freertos/task.h"
18#include "freertos/semphr.h"
19#include <goodisplay/gdey029T94.h>
20#include <Fonts/FreeMonoBold12pt7b.h>
21#include <Fonts/FreeMonoBold9pt7b.h>
22#include <cstring>
23#include <cstdarg>
24
25static const char* TAG = "EpaperDisplay";
26
28static constexpr const char* SPLASH_TITLE = "CDC Badge";
29static constexpr const char* SPLASH_VERSION = "v" APP_VERSION;
30
31namespace cdc::hal {
32
34static constexpr ledc_timer_t LEDC_TIMER = LEDC_TIMER_0;
35static constexpr ledc_mode_t LEDC_MODE = LEDC_LOW_SPEED_MODE;
36static constexpr ledc_channel_t LEDC_CHANNEL = LEDC_CHANNEL_0;
37static constexpr ledc_timer_bit_t LEDC_DUTY_RES = LEDC_TIMER_10_BIT;
38static constexpr uint32_t LEDC_FREQUENCY = 10000;
39
41static constexpr const char* NVS_NAMESPACE = "display";
42static constexpr const char* NVS_KEY_BACKLIGHT = "backlight";
43
45static constexpr uint16_t WIDTH = 296;
46static constexpr uint16_t HEIGHT = 128;
47static constexpr uint16_t BACKLIGHT_DEFAULT = 512;
48static constexpr uint16_t BACKLIGHT_MAX = 1023;
49
51static EpdSpi* s_epd_spi = nullptr;
52static Gdey029T94* s_epd_display = nullptr;
53
55static bool s_initialized = false;
57static bool s_backlightOn = true;
58
60static SemaphoreHandle_t s_renderMutex = nullptr;
61static TaskHandle_t s_renderTask = nullptr;
62static volatile bool s_renderPending = false;
64
65// Serialises the actual SSD1680 SPI transfer. The render task and any
66// synchronous flushSync() caller (e.g. a plugin host edit running on the
67// plg_tick task) may both drive the panel; the driver is not reentrant.
68static SemaphoreHandle_t s_panelMutex = nullptr;
69
70// The SSD1680 accumulates ghosting across consecutive partial updates and
71// eventually stops applying new partials cleanly. After this many partials a
72// refresh is promoted to a FULL update to reset the panel. Guarded by
73// s_panelMutex.
74static uint16_t s_partialsSinceFull = 0;
75static constexpr uint16_t kMaxPartialsBeforeFull = 60;
76
77// Decide the effective refresh mode, promoting to FULL periodically to clear
78// ghosting. Caller must hold s_panelMutex.
80 // Light partials (e.g. the lock-screen clock) never promote and do not
81 // advance the ghost counter; they stay PARTIAL until a real FULL clears them.
82 if (mode == RefreshMode::PARTIAL_LIGHT) {
83 return false;
84 }
87 return true;
88 }
90 return false;
91}
92
93// Coalescing rank for queued async refreshes: when several flush() calls
94// collapse into one render, the stronger mode wins. FULL > PARTIAL > PARTIAL_LIGHT.
95static int refreshStrength(RefreshMode mode) {
96 switch (mode) {
97 case RefreshMode::FULL: return 2;
98 case RefreshMode::PARTIAL: return 1;
99 case RefreshMode::PARTIAL_LIGHT: return 0;
100 }
101 return 1;
102}
103
108static void applyBacklight(uint16_t level) {
109 ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, level);
110 ledc_update_duty(LEDC_MODE, LEDC_CHANNEL);
111}
112
116static void loadBacklight() {
117 nvs_handle_t nvs;
118 if (nvs_open(NVS_NAMESPACE, NVS_READONLY, &nvs) == ESP_OK) {
119 uint16_t saved = BACKLIGHT_DEFAULT;
120 if (nvs_get_u16(nvs, NVS_KEY_BACKLIGHT, &saved) == ESP_OK) {
121 s_backlightLevel = (saved > BACKLIGHT_MAX) ? BACKLIGHT_MAX : saved;
122 }
123 nvs_close(nvs);
124 }
125}
126
131static void persistBacklight(uint16_t level) {
132 nvs_handle_t nvs;
133 if (nvs_open(NVS_NAMESPACE, NVS_READWRITE, &nvs) == ESP_OK) {
134 nvs_set_u16(nvs, NVS_KEY_BACKLIGHT, level);
135 nvs_commit(nvs);
136 nvs_close(nvs);
137 LOG_D(TAG, "Backlight saved to NVS: %u", level);
138 }
139}
140
145static void renderTask(void* arg) {
146 while (true) {
147 ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
148
149 RefreshMode mode;
150 {
152 mode = s_renderMode;
153 s_renderPending = false;
154 }
155
156 if (s_epd_display) {
158 if (resolveFullRefresh(mode)) {
159 s_epd_display->update();
160 } else {
161 // updateWindow takes physical coordinates (128 x 296). HAL
162 // WIDTH/HEIGHT are logical post-rotation values; swap them
163 // and pass using_rotation=false.
164 s_epd_display->updateWindow(0, 0, HEIGHT, WIDTH, false);
165 }
166 }
167 }
168}
169
173class EpaperDisplay : public IDisplay {
174public:
175 bool init() override;
176 bool start() override;
177 void stop() override;
178 core::ServiceState getState() const override { return state_; }
179 const char* getName() const override { return "display"; }
180
181 void clear() override;
182 void flush(RefreshMode mode) override;
183 void flushSync(RefreshMode mode) override;
184 bool isBusy() const override { return s_renderPending; }
185 uint16_t getWidth() const override { return WIDTH; }
186 uint16_t getHeight() const override { return HEIGHT; }
187 void setBacklight(uint16_t level) override;
188 void saveBacklight() override;
189 uint16_t getBacklight() const override { return s_backlightLevel; }
190 bool isBacklightOn() const override { return s_backlightOn && s_backlightLevel > 0; }
191 void backlightOn() override;
192 void backlightOff() override;
193 void* getNativeHandle() override { return s_epd_display; }
194 void showSplash(const char* subtitle = nullptr) override;
195
196 // GFX Drawing Methods
197 void drawPixel(int16_t x, int16_t y, uint16_t color) override;
198 void drawLine(int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint16_t color) override;
199 void drawRect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color) override;
200 void fillRect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color) override;
201 void setCursor(int16_t x, int16_t y) override;
202 void setTextColor(uint16_t color) override;
203 void setTextSize(uint8_t size) override;
204 void setFont(const void* font) override;
205 void print(const char* text) override;
206 void printf(const char* fmt, ...) override;
207
208private:
210};
211
217 if (s_initialized) {
218 return true;
219 }
220
221 LOG_I(TAG, "Initializing E-Paper display...");
222
223 // Load backlight from NVS
225
226 // Configure backlight PWM
227 ledc_timer_config_t ledcTimer = {
228 .speed_mode = LEDC_MODE,
229 .duty_resolution = LEDC_DUTY_RES,
230 .timer_num = LEDC_TIMER,
231 .freq_hz = LEDC_FREQUENCY,
232 .clk_cfg = LEDC_USE_XTAL_CLK,
233 .deconfigure = false
234 };
235 ledc_timer_config(&ledcTimer);
236
237 ledc_channel_config_t ledcChannel = {
238 .gpio_num = EPD_LED_PIN,
239 .speed_mode = LEDC_MODE,
240 .channel = LEDC_CHANNEL,
241 .intr_type = LEDC_INTR_DISABLE,
242 .timer_sel = LEDC_TIMER,
243 .duty = s_backlightLevel,
244 .hpoint = 0,
245 .sleep_mode = LEDC_SLEEP_MODE_KEEP_ALIVE,
246 .flags = {.output_invert = 0}
247 };
248 ledc_channel_config(&ledcChannel);
249
250 LOG_I(TAG, "Backlight configured");
251
252 // LAZY create display objects
253 if (!s_epd_spi) {
254 s_epd_spi = new EpdSpi();
255 }
256 if (!s_epd_display) {
257 s_epd_display = new Gdey029T94(*s_epd_spi);
258 }
259
260 // Initialize display
261 s_epd_display->init(false);
262 s_epd_display->setRotation(1);
263 s_epd_display->setMonoMode(true);
264 s_epd_display->cp437(true);
265 s_epd_display->fillScreen(EPD_WHITE);
266
267 LOG_I(TAG, "Display hardware initialized");
268
269 // Create render task
270 s_renderMutex = xSemaphoreCreateMutex();
271 s_panelMutex = xSemaphoreCreateMutex();
272 if (!s_renderMutex || !s_panelMutex) {
273 LOG_E(TAG, "Failed to create render mutex");
275 return false;
276 }
277
278 BaseType_t ret = xTaskCreate(renderTask, "epd_render", 4096, nullptr, 5, &s_renderTask);
279 if (ret != pdPASS) {
280 LOG_E(TAG, "Failed to create render task");
282 return false;
283 }
284
285 s_initialized = true;
287 LOG_I(TAG, "Display initialized (%ux%u), backlight=%u", WIDTH, HEIGHT, s_backlightLevel);
288 return true;
289}
290
296 if (state_ == core::ServiceState::INITIALIZED) {
298 s_backlightOn = true;
300 return true;
301 }
302 return state_ == core::ServiceState::STARTED;
303}
304
309 if (state_ == core::ServiceState::STARTED) {
310 s_backlightOn = false;
313 }
314}
315
320 if (s_epd_display) {
321 s_epd_display->fillScreen(EPD_WHITE);
322 }
323}
324
330 // If no render task, fall back to sync
331 if (!s_renderTask) {
332 flushSync(mode);
333 return;
334 }
335
336 {
338 if (s_renderPending) {
339 // Coalesce with the already-queued refresh: keep the stronger mode.
341 } else {
342 s_renderPending = true;
343 s_renderMode = mode;
344 }
345 }
346
347 xTaskNotifyGive(s_renderTask);
348}
349
355 if (!s_epd_display) return;
357 if (resolveFullRefresh(mode)) {
358 s_epd_display->update();
359 } else {
360 // updateWindow takes physical coordinates (128 x 296). HAL WIDTH/HEIGHT
361 // are logical post-rotation values; swap them and pass using_rotation=false.
362 s_epd_display->updateWindow(0, 0, HEIGHT, WIDTH, false);
363 }
364}
365
370void EpaperDisplay::setBacklight(uint16_t level) {
371 if (level > BACKLIGHT_MAX) level = BACKLIGHT_MAX;
372 s_backlightLevel = level;
373 // Always apply immediately for live preview (e.g., brightness slider)
374 // Also turn on backlight if level > 0
375 if (level > 0) {
376 s_backlightOn = true;
377 }
378 applyBacklight(level);
379}
380
387
392 s_backlightOn = true;
394 LOG_I(TAG, "Backlight ON (level=%u)", s_backlightLevel);
395}
396
401 s_backlightOn = false;
403 LOG_I(TAG, "Backlight OFF");
404}
405
410void EpaperDisplay::showSplash(const char* subtitle) {
411 if (!s_epd_display) return;
412
413 LOG_I(TAG, "Showing splash screen");
414
415 s_epd_display->fillScreen(EPD_BLACK);
416 s_epd_display->setTextColor(EPD_WHITE);
417
418 // App name - large, centered
419 s_epd_display->setFont(&FreeMonoBold12pt7b);
420 int16_t x1, y1;
421 uint16_t w, h;
422 s_epd_display->getTextBounds(SPLASH_TITLE, 0, 0, &x1, &y1, &w, &h);
423 int name_x = (s_epd_display->width() - w) / 2;
424 s_epd_display->setCursor(name_x, 55);
426
427 // Subtitle or version - smaller, centered below name
428 const char* sub = subtitle ? subtitle : SPLASH_VERSION;
429 s_epd_display->setFont(&FreeMonoBold9pt7b);
430 s_epd_display->getTextBounds(sub, 0, 0, &x1, &y1, &w, &h);
431 int ver_x = (s_epd_display->width() - w) / 2;
432 s_epd_display->setCursor(ver_x, 80);
433 s_epd_display->print(sub);
434
435 // Small text at bottom - built-in font (6x8)
436 s_epd_display->setFont(nullptr);
437 s_epd_display->setTextSize(1);
438
439 // Build date
440 char build_str[20];
441 snprintf(build_str, sizeof(build_str), "%.3s%2.2s %.5s", __DATE__, __DATE__ + 4, __TIME__);
442 s_epd_display->setCursor(2, 120);
443 s_epd_display->print(build_str);
444
445 // Right: tagline
446 const char* status_text = "Open Hardware Security";
447 int status_x = s_epd_display->width() - (strlen(status_text) * 6) - 2;
448 s_epd_display->setCursor(status_x, 120);
449 s_epd_display->print(status_text);
450
451 // Full refresh (blocking)
452 s_epd_display->update();
453 LOG_I(TAG, "Splash screen displayed");
454}
455
457
464void EpaperDisplay::drawPixel(int16_t x, int16_t y, uint16_t color) {
465 if (s_epd_display) s_epd_display->drawPixel(x, y, color);
466}
467
476void EpaperDisplay::drawLine(int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint16_t color) {
477 if (s_epd_display) s_epd_display->drawLine(x0, y0, x1, y1, color);
478}
479
488void EpaperDisplay::drawRect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color) {
489 if (s_epd_display) s_epd_display->drawRect(x, y, w, h, color);
490}
491
500void EpaperDisplay::fillRect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color) {
501 if (s_epd_display) s_epd_display->fillRect(x, y, w, h, color);
502}
503
509void EpaperDisplay::setCursor(int16_t x, int16_t y) {
510 if (s_epd_display) s_epd_display->setCursor(x, y);
511}
512
517void EpaperDisplay::setTextColor(uint16_t color) {
518 if (s_epd_display) s_epd_display->setTextColor(color);
519}
520
525void EpaperDisplay::setTextSize(uint8_t size) {
526 if (s_epd_display) s_epd_display->setTextSize(size);
527}
528
533void EpaperDisplay::setFont(const void* font) {
534 if (s_epd_display) s_epd_display->setFont(static_cast<const GFXfont*>(font));
535}
536
541void EpaperDisplay::print(const char* text) {
542 if (s_epd_display && text) s_epd_display->print(text);
543}
544
549void EpaperDisplay::printf(const char* fmt, ...) {
550 if (!s_epd_display || !fmt) return;
551 char buf[128];
552 va_list args;
553 va_start(args, fmt);
554 vsnprintf(buf, sizeof(buf), fmt, args);
555 va_end(args);
556 s_epd_display->print(buf);
557}
558
560static EpaperDisplay* s_display = nullptr;
561
567 if (!s_display) {
568 s_display = new EpaperDisplay();
569 }
570 return s_display;
571}
572
573void winkBacklight(uint8_t count, uint16_t period_ms) {
574 if (count == 0) count = 1;
575 if (count > 10) count = 10;
576 if (period_ms < 50) period_ms = 50;
577 if (period_ms > 1000) period_ms = 1000;
578
579 auto* display = getDisplayInstance();
580 if (!display) return;
581
582 const bool was_on = display->isBacklightOn();
583 const TickType_t ticks = pdMS_TO_TICKS(period_ms);
584 for (uint8_t i = 0; i < count; ++i) {
585 display->backlightOff();
586 vTaskDelay(ticks);
587 display->backlightOn();
588 vTaskDelay(ticks);
589 }
590 if (!was_on) display->backlightOff();
591}
592
593} // 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
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 APP_VERSION
#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 uint32_t LEDC_FREQUENCY
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 constexpr uint16_t kMaxPartialsBeforeFull
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 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 uint16_t s_partialsSinceFull
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 constexpr const char * NVS_KEY_BACKLIGHT
static void loadBacklight()
Loads persisted backlight level from NVS.
static SemaphoreHandle_t s_panelMutex
static constexpr ledc_timer_bit_t LEDC_DUTY_RES
static bool resolveFullRefresh(RefreshMode mode)