Skip to main content

Display Rendering

The display task renders the current screen once per minute (or immediately when state changes) using a dirty-flag pattern to avoid unnecessary refreshes.


Screen Enum

typedef enum {
SCREEN_CLOCK, // Normal clock face: time, date, next alarm
SCREEN_ALARM_FIRING, // "ALARM" in large text, snooze/dismiss prompts
SCREEN_MENU, // Top-level menu
SCREEN_ALARM_SET, // Alarm configuration UI
SCREEN_SNOOZED, // "Snoozed — rings at HH:MM"
} screen_t;

Display Task

static screen_t current_screen = SCREEN_CLOCK;
static bool screen_dirty = true;

static void display_task(void *arg) {
display_cmd_t cmd;
for (;;) {
// Wait for a command or periodic 60s tick
if (xQueueReceive(display_cmd_queue, &cmd, pdMS_TO_TICKS(60000))) {
current_screen = cmd.screen;
screen_dirty = true;
} else {
// 60s timeout — update clock face if on SCREEN_CLOCK
if (current_screen == SCREEN_CLOCK) screen_dirty = true;
}

if (screen_dirty) {
render_screen(current_screen);
screen_dirty = false;
}
}
}

Clock Face Layout

┌────────────────────────────────────┐
│ │
│ 09:45 │ ← 80pt bold, centred
│ │
│ Thursday 14 Apr │ ← 18pt, centred
│ │
│ │
│ ☼ Alarm: 07:00 │ ← 12pt, bottom-left
└────────────────────────────────────┘
static void render_clock(void) {
display.setFullWindow();
display.firstPage();
do {
display.fillScreen(GxEPD_WHITE);

// Time — large, centred
display.setFont(&FreeSansBold48pt7b);
display.setTextColor(GxEPD_BLACK);

time_t now = time(NULL);
struct tm *t = localtime(&now);

char time_str[6];
snprintf(time_str, sizeof(time_str), "%02d:%02d", t->tm_hour, t->tm_min);

int16_t x, y;
uint16_t w, h;
display.getTextBounds(time_str, 0, 0, &x, &y, &w, &h);
display.setCursor((400 - w) / 2, 140);
display.print(time_str);

// Date — smaller, centred
display.setFont(NULL);
display.setTextSize(2);
char date_str[20];
strftime(date_str, sizeof(date_str), "%A %d %b", t);
display.getTextBounds(date_str, 0, 0, &x, &y, &w, &h);
display.setCursor((400 - w) / 2, 185);
display.print(date_str);

// Next alarm — small, bottom left
// (fetch next alarm time from alarm_manager)
display.setTextSize(1);
display.setCursor(10, 280);
display.print("Alarm: ");
display.print(next_alarm_str);

} while (display.nextPage());
}

Partial Refresh for Clock Updates

Updating only the digits area is faster (~0.3 s) and reduces wear on the display. The digits area is approximately x=0, y=80, w=400, h=120.

static int full_refresh_counter = 0;

static void render_screen(screen_t screen) {
if (screen == SCREEN_CLOCK && full_refresh_counter < 10) {
display.setPartialWindow(0, 80, 400, 120);
full_refresh_counter++;
} else {
display.setFullWindow();
full_refresh_counter = 0;
}

display.firstPage();
do {
switch (screen) {
case SCREEN_CLOCK: render_clock(); break;
case SCREEN_ALARM_FIRING: render_alarm_firing(); break;
case SCREEN_MENU: render_menu(); break;
case SCREEN_ALARM_SET: render_alarm_set(); break;
case SCREEN_SNOOZED: render_snoozed(); break;
}
} while (display.nextPage());
}

Updating the Display from Main

The main task sends a command to the display task whenever the state changes:

display_cmd_t cmd = { .screen = SCREEN_ALARM_FIRING };
xQueueSend(display_cmd_queue, &cmd, pdMS_TO_TICKS(100));

The display task processes it asynchronously — the main task does not block waiting for the refresh to complete (~2 s for a full refresh).