Clock

Source: src/clock.c · 258 lines · 16 functions.

Screen captures

Views of this part running on the Mini Arcade.

Clock screen on the Mini Arcade
Clock showing time, date, and countdown information.

Module entry points

These functions are exposed to the rest of the project. Open one to follow its decisions and the helpers it calls.

How to read these diagrams

Each function has its own expandable diagram using the actual expressions and statements from this file. Decisions split into True and False; loop bodies are indented. Returns end the function immediately; break and continue follow their C meanings. Arrows between statements show their order when execution reaches them.

These are structural source diagrams before preprocessing, not runtime traces. Conditional compilation and macros remain source expressions. Ternary choices remain inside their statement. Calls are linked separately; callback execution and task scheduling are not inferred.

Functions in this file

configure_clock_timezone() · source line 36
static void configure_clock_timezone(void)
  1. Executestatic bool configured;
  2. Decisionconfigured
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  3. Executesetenv("TZ", "CET-1CEST,M3.5.0/02,M10.5.0/03", 1);
  4. Executetzset();
  5. Executeconfigured = true;
Read original function source
static void configure_clock_timezone(void)
{
    static bool configured;
    if (configured) return;
    setenv("TZ", "CET-1CEST,M3.5.0/02,M10.5.0/03", 1);
    tzset();
    configured = true;
}
choose_section_layout() · source line 45
static void choose_section_layout(void)
  1. Executeuint8_t previous_layout[CLOCK_ZONE_COUNT];
  2. Executememcpy(previous_layout, section_zone, sizeof(previous_layout));
  3. DO / WHILE loopmemcmp(previous_layout, section_zone, sizeof(section_zone)) == 0

    Run body, then test condition; repeat while true.

      1. FOR loopuint8_t index = 0U; index < CLOCK_ZONE_COUNT; ++index

        Repeat body while the loop condition permits; then continue below.

        1. Executesection_zone[index] = index;
      2. FOR loopuint8_t index = CLOCK_ZONE_COUNT - 1U; index > 0U; --index

        Repeat body while the loop condition permits; then continue below.

          1. Executeconst uint8_t swap = (uint8_t)(esp_random() % (index + 1U));
          2. Executeconst uint8_t temporary = section_zone[index];
          3. Executesection_zone[index] = section_zone[swap];
          4. Executesection_zone[swap] = temporary;
Read original function source
static void choose_section_layout(void)
{
    uint8_t previous_layout[CLOCK_ZONE_COUNT];
    memcpy(previous_layout, section_zone, sizeof(previous_layout));
    do {
        for (uint8_t index = 0U; index < CLOCK_ZONE_COUNT; ++index) section_zone[index] = index;
        for (uint8_t index = CLOCK_ZONE_COUNT - 1U; index > 0U; --index) {
            const uint8_t swap = (uint8_t)(esp_random() % (index + 1U));
            const uint8_t temporary = section_zone[index];
            section_zone[index] = section_zone[swap];
            section_zone[swap] = temporary;
        }
    } while (memcmp(previous_layout, section_zone, sizeof(section_zone)) == 0);
}
section_y() · source line 60
static int section_y(clock_section_t section)
  1. Exit this functionreturn zone_y[section_zone[section]];
Read original function source
static int section_y(clock_section_t section)
{
    return zone_y[section_zone[section]];
}
draw_unsynchronized_status() · source line 65
static void draw_unsynchronized_status(uint16_t *frame, int y)

Calls into this documentation:

  1. Executeconst wifi_connection_state_t connection = wifiServiceConnectionState();
  2. Executeconst time_sync_status_t sync_status = timeSyncStatus();
  3. Executeconst char *status_text = "CLOCK NOT SET";
  4. Executeuint16_t status_colour = COLOR_YELLOW;
  5. Decisionconnection == WIFI_CONNECTION_CONNECTING
    True
    1. Executestatus_text = "WIFI CONNECTING...";
    False
    1. Decisionconnection == WIFI_CONNECTION_FAILED
      True
        1. Executestatus_text = "WIFI CONNECTION FAILED";
        2. Executestatus_colour = COLOR_RED;
      False
      1. Decisionsync_status == TIME_SYNC_SYNCING
        True
        1. Executestatus_text = "TIME SYNCING...";
        False
        1. Decisionsync_status == TIME_SYNC_FAILED
          True
            1. Executestatus_text = "TIME SYNC FAILED";
            2. Executestatus_colour = COLOR_RED;
          False

          Continue after this decision.

  6. Executedraw_text_centred(frame, 160, y + 18, status_text, 1, status_colour);
Read original function source
static void draw_unsynchronized_status(uint16_t *frame, int y)
{
    const wifi_connection_state_t connection = wifiServiceConnectionState();
    const time_sync_status_t sync_status = timeSyncStatus();
    const char *status_text = "CLOCK NOT SET";
    uint16_t status_colour = COLOR_YELLOW;

    if (connection == WIFI_CONNECTION_CONNECTING) status_text = "WIFI CONNECTING...";
    else if (connection == WIFI_CONNECTION_FAILED) {
        status_text = "WIFI CONNECTION FAILED";
        status_colour = COLOR_RED;
    } else if (sync_status == TIME_SYNC_SYNCING) status_text = "TIME SYNCING...";
    else if (sync_status == TIME_SYNC_FAILED) {
        status_text = "TIME SYNC FAILED";
        status_colour = COLOR_RED;
    }
    draw_text_centred(frame, 160, y + 18, status_text, 1, status_colour);
}
draw_time_section() · source line 84
static void draw_time_section(uint16_t *frame, int y, const char *text, uint16_t colour)

Calls into this documentation:

  1. Executefill_rect(frame, 0, y, LCD_WIDTH, 38, COLOR_BLACK);
  2. Executedraw_text_centred(frame, 160, y, text, 4, colour);
Read original function source
static void draw_time_section(uint16_t *frame, int y, const char *text, uint16_t colour)
{
    fill_rect(frame, 0, y, LCD_WIDTH, 38, COLOR_BLACK);
    draw_text_centred(frame, 160, y, text, 4, colour);
}
draw_date_section() · source line 90
static void draw_date_section(uint16_t *frame, int y)

Calls into this documentation:

  1. Executefill_rect(frame, 0, y, LCD_WIDTH, 42, COLOR_BLACK);
  2. Executedraw_text_centred(frame, 160, y + 2, day_text, 2, COLOR_ORANGE);
  3. Executedraw_text_centred(frame, 160, y + 24, date_text, 1, COLOR_ORANGE);
Read original function source
static void draw_date_section(uint16_t *frame, int y)
{
    fill_rect(frame, 0, y, LCD_WIDTH, 42, COLOR_BLACK);
    draw_text_centred(frame, 160, y + 2, day_text, 2, COLOR_ORANGE);
    draw_text_centred(frame, 160, y + 24, date_text, 1, COLOR_ORANGE);
}
draw_countdown_section() · source line 97
static void draw_countdown_section(uint16_t *frame, int y)

Calls into this documentation:

  1. Executefill_rect(frame, 0, y, LCD_WIDTH, 43, COLOR_BLACK);
  2. Executedraw_text(frame, 8, y + 2, "DOOM", 1, COLOR_CRIMSON);
  3. Executedraw_text_centred(frame, 210, y, monday_countdown, 2, COLOR_CRIMSON);
  4. Executedraw_text(frame, 8, y + 25, "FREEDOM", 1, COLOR_OLIVE);
  5. Executedraw_text_centred(frame, 210, y + 23, friday_countdown, 2, COLOR_OLIVE);
Read original function source
static void draw_countdown_section(uint16_t *frame, int y)
{
    fill_rect(frame, 0, y, LCD_WIDTH, 43, COLOR_BLACK);
    draw_text(frame, 8, y + 2, "DOOM", 1, COLOR_CRIMSON);
    draw_text_centred(frame, 210, y, monday_countdown, 2, COLOR_CRIMSON);
    draw_text(frame, 8, y + 25, "FREEDOM", 1, COLOR_OLIVE);
    draw_text_centred(frame, 210, y + 23, friday_countdown, 2, COLOR_OLIVE);
}
format_countdown() · source line 106
static void format_countdown(char *text, size_t text_size, time_t now_time,
                             const struct tm *now, int target_weekday,
                             int target_hour, int target_minute)
  1. Executestruct tm target = *now;
  2. Executeint days_until_target = (target_weekday - target.tm_wday + 7) % 7;
  3. Executetarget.tm_mday += days_until_target;
  4. Executetarget.tm_hour = target_hour;
  5. Executetarget.tm_min = target_minute;
  6. Executetarget.tm_sec = 0;
  7. Executetarget.tm_isdst = -1;
  8. Executetime_t target_time = mktime(&target);
  9. Decisiondifftime(target_time, now_time) <= 0.0
    True
      1. Executetarget.tm_mday += 7;
      2. Executetarget.tm_isdst = -1;
      3. Executetarget_time = mktime(&target);
    False

    Continue after this decision.

  10. Executelong remaining = (long)difftime(target_time, now_time);
  11. Executeconst long days = remaining / 86400L;
  12. Executeremaining %= 86400L;
  13. Executeconst long hours = remaining / 3600L;
  14. Executeremaining %= 3600L;
  15. Executeconst long minutes = remaining / 60L;
  16. Executeconst long seconds = remaining % 60L;
  17. Executesnprintf(text, text_size, "%ldd %02ld:%02ld:%02ld", days, hours, minutes, seconds);
Read original function source
static void format_countdown(char *text, size_t text_size, time_t now_time,
                             const struct tm *now, int target_weekday,
                             int target_hour, int target_minute)
{
    struct tm target = *now;
    int days_until_target = (target_weekday - target.tm_wday + 7) % 7;
    target.tm_mday += days_until_target;
    target.tm_hour = target_hour;
    target.tm_min = target_minute;
    target.tm_sec = 0;
    target.tm_isdst = -1;

    time_t target_time = mktime(&target);
    if (difftime(target_time, now_time) <= 0.0) {
        target.tm_mday += 7;
        target.tm_isdst = -1;
        target_time = mktime(&target);
    }

    long remaining = (long)difftime(target_time, now_time);
    const long days = remaining / 86400L;
    remaining %= 86400L;
    const long hours = remaining / 3600L;
    remaining %= 3600L;
    const long minutes = remaining / 60L;
    const long seconds = remaining % 60L;
    snprintf(text, text_size, "%ldd %02ld:%02ld:%02ld", days, hours, minutes, seconds);
}
format_doomsday_countdown() · source line 135
static void format_doomsday_countdown(char *text, size_t text_size, time_t now_time)
  1. Executestruct tm target = { .tm_year = 2027 - 1900, .tm_mon = 1, .tm_mday = 5, .tm_isdst = -1, };
  2. Executelong remaining = (long)difftime(mktime(&target), now_time);
  3. Decisionremaining < 0L
    True
    1. Executeremaining = 0L;
    False

    Continue after this decision.

  4. Executeconst long days = remaining / 86400L;
  5. Executeremaining %= 86400L;
  6. Executeconst long hours = remaining / 3600L;
  7. Executeremaining %= 3600L;
  8. Executeconst long minutes = remaining / 60L;
  9. Executeconst long seconds = remaining % 60L;
  10. Executesnprintf(text, text_size, "%ldd %02ld:%02ld:%02ld", days, hours, minutes, seconds);
Read original function source
static void format_doomsday_countdown(char *text, size_t text_size, time_t now_time)
{
    struct tm target = {
        .tm_year = 2027 - 1900,
        .tm_mon = 1,
        .tm_mday = 5,
        .tm_isdst = -1,
    };
    long remaining = (long)difftime(mktime(&target), now_time);
    if (remaining < 0L) remaining = 0L;

    const long days = remaining / 86400L;
    remaining %= 86400L;
    const long hours = remaining / 3600L;
    remaining %= 3600L;
    const long minutes = remaining / 60L;
    const long seconds = remaining % 60L;
    snprintf(text, text_size, "%ldd %02ld:%02ld:%02ld", days, hours, minutes, seconds);
}
update_time_strings() · source line 155
static void update_time_strings(void)

Calls into this documentation:

  1. Executeconst time_t now_time = time(NULL);
  2. Executestruct tm local_time = {0};
  3. Decisionnow_time == (time_t)-1 || localtime_r(&now_time, &local_time) == NULL || local_time.tm_year + 1900 < CLOCK_VALID_YEAR
    True
      1. Decisiontime_available
        True
        1. Executefull_redraw = true;
        False

        Continue after this decision.

      2. Executetime_available = false;
      3. Exit this functionreturn;
    False

    Continue after this decision.

  4. Decision!time_available || last_day_of_year != local_time.tm_yday
    True
    1. Executefull_redraw = true;
    False

    Continue after this decision.

  5. Executetime_available = true;
  6. Executestrftime(time_text, sizeof(time_text), "%H:%M:%S", &local_time);
  7. Executestrftime(day_text, sizeof(day_text), "%A", &local_time);
  8. Executestrftime(date_text, sizeof(date_text), "%d %B %Y", &local_time);
  9. Executeformat_doomsday_countdown(monday_countdown, sizeof(monday_countdown), now_time);
  10. Executeformat_countdown(friday_countdown, sizeof(friday_countdown), now_time, &local_time, 5, 15, 0);
  11. Executelast_day_of_year = local_time.tm_yday;
Read original function source
static void update_time_strings(void)
{
    const time_t now_time = time(NULL);
    struct tm local_time = {0};
    if (now_time == (time_t)-1 || localtime_r(&now_time, &local_time) == NULL ||
        local_time.tm_year + 1900 < CLOCK_VALID_YEAR) {
        if (time_available) full_redraw = true;
        time_available = false;
        return;
    }

    if (!time_available || last_day_of_year != local_time.tm_yday) full_redraw = true;
    time_available = true;
    strftime(time_text, sizeof(time_text), "%H:%M:%S", &local_time);
    strftime(day_text, sizeof(day_text), "%A", &local_time);
    strftime(date_text, sizeof(date_text), "%d %B %Y", &local_time);
    format_doomsday_countdown(monday_countdown, sizeof(monday_countdown), now_time);
    format_countdown(friday_countdown, sizeof(friday_countdown), now_time,
                     &local_time, 5, 15, 0);
    last_day_of_year = local_time.tm_yday;
}
startClock() · source line 177
void startClock(uint32_t now_ms)

Calls into this documentation:

  1. Executeconfigure_clock_timezone();
  2. Executerunning = true;
  3. Executeframe_ready = true;
  4. Executefull_redraw = true;
  5. Executetime_available = false;
  6. Executelast_day_of_year = -1;
  7. Executelast_update = now_ms - CLOCK_UPDATE_MS;
  8. Executelast_layout_change = now_ms;
  9. Executechoose_section_layout();
Read original function source
void startClock(uint32_t now_ms)
{
    configure_clock_timezone();
    running = true;
    frame_ready = true;
    full_redraw = true;
    time_available = false;
    last_day_of_year = -1;
    last_update = now_ms - CLOCK_UPDATE_MS;
    last_layout_change = now_ms;
    choose_section_layout();
}
stopClock() · source line 190
void stopClock(void)
  1. Executerunning = false;
  2. Executeframe_ready = false;
Read original function source
void stopClock(void)
{
    running = false;
    frame_ready = false;
}
updateClock() · source line 196
void updateClock(uint32_t now_ms)

Calls into this documentation:

  1. Decision!running || now_ms - last_update < CLOCK_UPDATE_MS
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  2. Executelast_update = now_ms;
  3. Decisionnow_ms - last_layout_change >= CLOCK_LAYOUT_CHANGE_MS
    True
      1. Executechoose_section_layout();
      2. Executelast_layout_change = now_ms;
      3. Executefull_redraw = true;
    False

    Continue after this decision.

  4. Executeupdate_time_strings();
  5. Executeframe_ready = true;
Read original function source
void updateClock(uint32_t now_ms)
{
    if (!running || now_ms - last_update < CLOCK_UPDATE_MS) return;
    last_update = now_ms;
    if (now_ms - last_layout_change >= CLOCK_LAYOUT_CHANGE_MS) {
        choose_section_layout();
        last_layout_change = now_ms;
        full_redraw = true;
    }
    update_time_strings();
    frame_ready = true;
}
clockTakeFrame() · source line 209
bool clockTakeFrame(void)
  1. Executeconst bool ready = frame_ready;
  2. Executeframe_ready = false;
  3. Exit this functionreturn ready;
Read original function source
bool clockTakeFrame(void)
{
    const bool ready = frame_ready;
    frame_ready = false;
    return ready;
}
drawClock() · source line 216
void drawClock(uint16_t *frame)

Calls into this documentation:

  1. Decision!running || frame == NULL
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  2. Decisionfull_redraw
    True
      1. Executeclear(frame, COLOR_BLACK);
    False

    Continue after this decision.

  3. Decision!time_available
    True
      1. Executedraw_time_section(frame, section_y(CLOCK_SECTION_TIME), "--:--:--", COLOR_RED);
      2. Executefill_rect(frame, 0, section_y(CLOCK_SECTION_DATE), LCD_WIDTH, 42, COLOR_BLACK);
      3. Executedraw_unsynchronized_status(frame, section_y(CLOCK_SECTION_DATE));
      4. Executefull_redraw = false;
      5. Exit this functionreturn;
    False

    Continue after this decision.

  4. Executedraw_time_section(frame, section_y(CLOCK_SECTION_TIME), time_text, COLOR_RED);
  5. Executedraw_date_section(frame, section_y(CLOCK_SECTION_DATE));
  6. Executedraw_countdown_section(frame, section_y(CLOCK_SECTION_COUNTDOWNS));
  7. Executefull_redraw = false;
Read original function source
void drawClock(uint16_t *frame)
{
    if (!running || frame == NULL) return;

    if (full_redraw) {
        clear(frame, COLOR_BLACK);
    }

    if (!time_available) {
        draw_time_section(frame, section_y(CLOCK_SECTION_TIME), "--:--:--", COLOR_RED);
        fill_rect(frame, 0, section_y(CLOCK_SECTION_DATE), LCD_WIDTH, 42, COLOR_BLACK);
        draw_unsynchronized_status(frame, section_y(CLOCK_SECTION_DATE));
        full_redraw = false;
        return;
    }

    draw_time_section(frame, section_y(CLOCK_SECTION_TIME), time_text, COLOR_RED);
    draw_date_section(frame, section_y(CLOCK_SECTION_DATE));
    draw_countdown_section(frame, section_y(CLOCK_SECTION_COUNTDOWNS));
    full_redraw = false;
}
updateDoomCountdownOverlay() · source line 238
void updateDoomCountdownOverlay(void)

Calls into this documentation:

  1. Executeconfigure_clock_timezone();
  2. Executeconst time_t now_time = time(NULL);
  3. Executestruct tm local_time = {0};
  4. Decisionnow_time == (time_t)-1 || localtime_r(&now_time, &local_time) == NULL || local_time.tm_year + 1900 < CLOCK_VALID_YEAR
    True
      1. ExecuteglobalOverlayClearItem(0U);
      2. Exit this functionreturn;
    False

    Continue after this decision.

  5. Executechar countdown[20];
  6. Executechar overlay_text[32];
  7. Executeformat_doomsday_countdown(countdown, sizeof(countdown), now_time);
  8. Executesnprintf(overlay_text, sizeof(overlay_text), "DOOM %s", countdown);
  9. Execute(void)globalOverlaySetItem(0U, overlay_text, 2, 2, 1, COLOR_ORANGE);
Read original function source
void updateDoomCountdownOverlay(void)
{
    configure_clock_timezone();

    const time_t now_time = time(NULL);
    struct tm local_time = {0};
    if (now_time == (time_t)-1 || localtime_r(&now_time, &local_time) == NULL ||
        local_time.tm_year + 1900 < CLOCK_VALID_YEAR) {
        globalOverlayClearItem(0U);
        return;
    }

    char countdown[20];
    char overlay_text[32];
    format_doomsday_countdown(countdown, sizeof(countdown), now_time);
    snprintf(overlay_text, sizeof(overlay_text), "DOOM %s", countdown);
    // Pixel coordinates are deliberately independent of individual screens: move this
    // item anywhere on the LCD by changing its x/y values without changing a renderer.
    (void)globalOverlaySetItem(0U, overlay_text, 2, 2, 1, COLOR_ORANGE);
}
Complete original file, including constants and data
/* Coded By JD(2026) */
#error "Reference listing only — not provided as build-ready source."
#include "clock.h"

#include "esp_random.h"
#include "game_graphics.h"
#include "PET2001_font.h"
#include "global_overlay.h"
#include "time_sync.h"
#include "wifi.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#define CLOCK_UPDATE_MS 1000U
#define CLOCK_LAYOUT_CHANGE_MS (10U * 60U * 1000U)
#define CLOCK_VALID_YEAR 2024
#define CLOCK_ZONE_COUNT 3U

typedef enum { CLOCK_SECTION_TIME, CLOCK_SECTION_DATE, CLOCK_SECTION_COUNTDOWNS } clock_section_t;

static bool running;
static bool frame_ready;
static bool full_redraw;
static bool time_available;
static uint32_t last_update;
static uint32_t last_layout_change;
static int last_day_of_year;
static char time_text[16];
static char day_text[16];
static char date_text[32];
static char monday_countdown[20];
static char friday_countdown[20];
static uint8_t section_zone[CLOCK_ZONE_COUNT];
static const int zone_y[CLOCK_ZONE_COUNT] = {4, 58, 112};

static void configure_clock_timezone(void)
{
    static bool configured;
    if (configured) return;
    setenv("TZ", "CET-1CEST,M3.5.0/02,M10.5.0/03", 1);
    tzset();
    configured = true;
}

static void choose_section_layout(void)
{
    uint8_t previous_layout[CLOCK_ZONE_COUNT];
    memcpy(previous_layout, section_zone, sizeof(previous_layout));
    do {
        for (uint8_t index = 0U; index < CLOCK_ZONE_COUNT; ++index) section_zone[index] = index;
        for (uint8_t index = CLOCK_ZONE_COUNT - 1U; index > 0U; --index) {
            const uint8_t swap = (uint8_t)(esp_random() % (index + 1U));
            const uint8_t temporary = section_zone[index];
            section_zone[index] = section_zone[swap];
            section_zone[swap] = temporary;
        }
    } while (memcmp(previous_layout, section_zone, sizeof(section_zone)) == 0);
}

static int section_y(clock_section_t section)
{
    return zone_y[section_zone[section]];
}

static void draw_unsynchronized_status(uint16_t *frame, int y)
{
    const wifi_connection_state_t connection = wifiServiceConnectionState();
    const time_sync_status_t sync_status = timeSyncStatus();
    const char *status_text = "CLOCK NOT SET";
    uint16_t status_colour = COLOR_YELLOW;

    if (connection == WIFI_CONNECTION_CONNECTING) status_text = "WIFI CONNECTING...";
    else if (connection == WIFI_CONNECTION_FAILED) {
        status_text = "WIFI CONNECTION FAILED";
        status_colour = COLOR_RED;
    } else if (sync_status == TIME_SYNC_SYNCING) status_text = "TIME SYNCING...";
    else if (sync_status == TIME_SYNC_FAILED) {
        status_text = "TIME SYNC FAILED";
        status_colour = COLOR_RED;
    }
    drawPet2001TextCentredTransparent(frame, 160, y + 18, status_text, status_colour, 1);
}

static void draw_time_section(uint16_t *frame, int y, const char *text, uint16_t colour)
{
    fill_rect(frame, 0, y, LCD_WIDTH, 38, COLOR_BLACK);
    drawPet2001TextCentredTransparent(frame, 160, y, text, colour, 4);
}

static void draw_date_section(uint16_t *frame, int y)
{
    fill_rect(frame, 0, y, LCD_WIDTH, 42, COLOR_BLACK);
    drawPet2001TextCentredTransparent(frame, 160, y + 2, day_text, COLOR_ORANGE, 2);
    drawPet2001TextCentredTransparent(frame, 160, y + 24, date_text, COLOR_ORANGE, 1);
}

static void draw_countdown_section(uint16_t *frame, int y)
{
    fill_rect(frame, 0, y, LCD_WIDTH, 43, COLOR_BLACK);
    drawPet2001TextTransparent(frame, 8, y + 2, "DOOM", COLOR_CRIMSON, 1);
    drawPet2001TextCentredTransparent(frame, 208, y, monday_countdown, COLOR_CRIMSON, 2);
    drawPet2001TextTransparent(frame, 8, y + 25, "FREEDOM", COLOR_OLIVE, 1);
    drawPet2001TextCentredTransparent(frame, 208, y + 23, friday_countdown, COLOR_OLIVE, 2);
}

static void format_countdown(char *text, size_t text_size, time_t now_time,
                             const struct tm *now, int target_weekday,
                             int target_hour, int target_minute)
{
    struct tm target = *now;
    int days_until_target = (target_weekday - target.tm_wday + 7) % 7;
    target.tm_mday += days_until_target;
    target.tm_hour = target_hour;
    target.tm_min = target_minute;
    target.tm_sec = 0;
    target.tm_isdst = -1;

    time_t target_time = mktime(&target);
    if (difftime(target_time, now_time) <= 0.0) {
        target.tm_mday += 7;
        target.tm_isdst = -1;
        target_time = mktime(&target);
    }

    long remaining = (long)difftime(target_time, now_time);
    const long days = remaining / 86400L;
    remaining %= 86400L;
    const long hours = remaining / 3600L;
    remaining %= 3600L;
    const long minutes = remaining / 60L;
    const long seconds = remaining % 60L;
    snprintf(text, text_size, "%ldd %02ld:%02ld:%02ld", days, hours, minutes, seconds);
}

static void format_doomsday_countdown(char *text, size_t text_size, time_t now_time)
{
    struct tm target = {
        .tm_year = 2027 - 1900,
        .tm_mon = 1,
        .tm_mday = 5,
        .tm_isdst = -1,
    };
    long remaining = (long)difftime(mktime(&target), now_time);
    if (remaining < 0L) remaining = 0L;

    const long days = remaining / 86400L;
    remaining %= 86400L;
    const long hours = remaining / 3600L;
    remaining %= 3600L;
    const long minutes = remaining / 60L;
    const long seconds = remaining % 60L;
    snprintf(text, text_size, "%ldd %02ld:%02ld:%02ld", days, hours, minutes, seconds);
}

static void update_time_strings(void)
{
    const time_t now_time = time(NULL);
    struct tm local_time = {0};
    if (now_time == (time_t)-1 || localtime_r(&now_time, &local_time) == NULL ||
        local_time.tm_year + 1900 < CLOCK_VALID_YEAR) {
        if (time_available) full_redraw = true;
        time_available = false;
        return;
    }

    if (!time_available || last_day_of_year != local_time.tm_yday) full_redraw = true;
    time_available = true;
    strftime(time_text, sizeof(time_text), "%H:%M:%S", &local_time);
    strftime(day_text, sizeof(day_text), "%A", &local_time);
    strftime(date_text, sizeof(date_text), "%d %B %Y", &local_time);
    format_doomsday_countdown(monday_countdown, sizeof(monday_countdown), now_time);
    format_countdown(friday_countdown, sizeof(friday_countdown), now_time,
                     &local_time, 5, 15, 0);
    last_day_of_year = local_time.tm_yday;
}

void startClock(uint32_t now_ms)
{
    configure_clock_timezone();
    running = true;
    frame_ready = true;
    full_redraw = true;
    time_available = false;
    last_day_of_year = -1;
    last_update = now_ms - CLOCK_UPDATE_MS;
    last_layout_change = now_ms;
    choose_section_layout();
}

void stopClock(void)
{
    running = false;
    frame_ready = false;
}

void updateClock(uint32_t now_ms)
{
    if (!running || now_ms - last_update < CLOCK_UPDATE_MS) return;
    last_update = now_ms;
    if (now_ms - last_layout_change >= CLOCK_LAYOUT_CHANGE_MS) {
        choose_section_layout();
        last_layout_change = now_ms;
        full_redraw = true;
    }
    update_time_strings();
    frame_ready = true;
}

bool clockTakeFrame(void)
{
    const bool ready = frame_ready;
    frame_ready = false;
    return ready;
}

void drawClock(uint16_t *frame)
{
    if (!running || frame == NULL) return;

    if (full_redraw) {
        clear(frame, COLOR_BLACK);
    }

    if (!time_available) {
        draw_time_section(frame, section_y(CLOCK_SECTION_TIME), "--:--:--", COLOR_RED);
        fill_rect(frame, 0, section_y(CLOCK_SECTION_DATE), LCD_WIDTH, 42, COLOR_BLACK);
        draw_unsynchronized_status(frame, section_y(CLOCK_SECTION_DATE));
        full_redraw = false;
        return;
    }

    draw_time_section(frame, section_y(CLOCK_SECTION_TIME), time_text, COLOR_RED);
    draw_date_section(frame, section_y(CLOCK_SECTION_DATE));
    draw_countdown_section(frame, section_y(CLOCK_SECTION_COUNTDOWNS));
    full_redraw = false;
}

void updateDoomCountdownOverlay(void)
{
    configure_clock_timezone();

    const time_t now_time = time(NULL);
    struct tm local_time = {0};
    if (now_time == (time_t)-1 || localtime_r(&now_time, &local_time) == NULL ||
        local_time.tm_year + 1900 < CLOCK_VALID_YEAR) {
        globalOverlayClearItem(0U);
        return;
    }

    char countdown[20];
    char overlay_text[32];
    format_doomsday_countdown(countdown, sizeof(countdown), now_time);
    snprintf(overlay_text, sizeof(overlay_text), "DOOM %s", countdown);
    // Pixel coordinates are deliberately independent of individual screens: move this
    // item anywhere on the LCD by changing its x/y values without changing a renderer.
    (void)globalOverlaySetItem(0U, overlay_text, 2, 2, 1, COLOR_ORANGE);
}