feat: X3 clock display with DS3231 RTC and NTP sync (#1612)

This commit is contained in:
Justinian
2026-05-18 21:06:56 -04:00
committed by GitHub
parent 151bf1dae4
commit cfd3a381ed
14 changed files with 836 additions and 57 deletions
@@ -0,0 +1,108 @@
#include "ClockSyncActivity.h"
#include <GfxRenderer.h>
#include <HalClock.h>
#include <I18n.h>
#include <Logging.h>
#include <WiFi.h>
#include <cstdio>
#include "CrossPointSettings.h"
#include "MappedInputManager.h"
#include "components/UITheme.h"
#include "fontIds.h"
void ClockSyncActivity::onEnter() {
Activity::onEnter();
state = SYNCING;
syncedTime[0] = '\0';
requestUpdate();
}
void ClockSyncActivity::onExit() { Activity::onExit(); }
void ClockSyncActivity::runSync() {
if (WiFi.status() != WL_CONNECTED) {
LOG_INF("CLK", "Manual sync requested but WiFi is not connected");
state = NO_WIFI;
requestUpdate();
return;
}
const bool ok = halClock.syncFromNTP();
if (!ok) {
state = FAILED;
requestUpdate();
return;
}
// Mark as synced so the auto-sync hook stops firing on future WiFi connects.
SETTINGS.clockHasBeenSynced = 1;
SETTINGS.saveToFile();
// Read the freshly synced time back for the user-facing confirmation.
char buf[9];
if (halClock.formatTime(buf, sizeof(buf), SETTINGS.clockUtcOffsetQ, SETTINGS.clockFormat == 1)) {
snprintf(syncedTime, sizeof(syncedTime), "%s", buf);
}
state = SUCCESS;
requestUpdate();
}
void ClockSyncActivity::loop() {
if (state == SYNCING) {
// First-tick: render the "Syncing..." screen, then perform the (blocking) sync.
// requestUpdateAndWait below forces the render before we block on WiFi.
requestUpdateAndWait();
runSync();
return;
}
if (mappedInput.wasPressed(MappedInputManager::Button::Back) ||
mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
finish();
}
}
void ClockSyncActivity::render(RenderLock&&) {
const auto& metrics = UITheme::getInstance().getMetrics();
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
renderer.clearScreen();
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_CLOCK_SYNC));
const int midY = pageHeight / 2;
switch (state) {
case SYNCING:
renderer.drawCenteredText(UI_12_FONT_ID, midY, tr(STR_CLOCK_SYNCING));
break;
case SUCCESS: {
renderer.drawCenteredText(UI_12_FONT_ID, midY - 20, tr(STR_CLOCK_SYNC_OK), true, EpdFontFamily::BOLD);
if (syncedTime[0] != '\0') {
char line[32];
snprintf(line, sizeof(line), "%s %s", tr(STR_CURRENT_TIME), syncedTime);
renderer.drawCenteredText(UI_10_FONT_ID, midY + 10, line);
}
break;
}
case NO_WIFI:
renderer.drawCenteredText(UI_12_FONT_ID, midY - 20, tr(STR_CLOCK_SYNC_NO_WIFI), true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, midY + 10, tr(STR_CLOCK_SYNC_NO_WIFI_HINT));
break;
case FAILED:
renderer.drawCenteredText(UI_12_FONT_ID, midY - 20, tr(STR_CLOCK_SYNC_FAIL), true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, midY + 10, tr(STR_CHECK_SERIAL_OUTPUT));
break;
}
if (state != SYNCING) {
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_OK_BUTTON), "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
}
renderer.displayBuffer();
}