Merge branch 'feat-additional-compensation' of https://github.com/jpirnay/crosspoint-reader into mybuild
This commit is contained in:
@@ -83,6 +83,43 @@ STR_PARA_ALIGNMENT: "Reader Paragraph Alignment"
|
|||||||
STR_HYPHENATION: "Hyphenation"
|
STR_HYPHENATION: "Hyphenation"
|
||||||
STR_TIME_TO_SLEEP: "Time to Sleep"
|
STR_TIME_TO_SLEEP: "Time to Sleep"
|
||||||
STR_SHOW_HIDDEN_FILES: "Show Hidden Files"
|
STR_SHOW_HIDDEN_FILES: "Show Hidden Files"
|
||||||
|
STR_USE_CLOCK: "Use Clock"
|
||||||
|
STR_CLOCK_SETTINGS: "Clock Settings"
|
||||||
|
STR_CLOCK_SETTINGS_WARNING: "Uses more battery; clock may drift"
|
||||||
|
STR_CLOCK: "Clock"
|
||||||
|
STR_CLOCK_FORMAT: "Clock Format"
|
||||||
|
STR_TIMEZONE: "Timezone"
|
||||||
|
STR_24H: "24h"
|
||||||
|
STR_12H: "12h"
|
||||||
|
STR_TZ_UTC: "UTC (GMT/BST)"
|
||||||
|
STR_TZ_CET: "Central Europe (CET/CEST)"
|
||||||
|
STR_TZ_EET: "Eastern Europe (EET/EEST)"
|
||||||
|
STR_TZ_EST: "US Eastern (EST/EDT)"
|
||||||
|
STR_TZ_CST: "US Central (CST/CDT)"
|
||||||
|
STR_TZ_MST: "US Mountain (MST/MDT)"
|
||||||
|
STR_TZ_PST: "US Pacific (PST/PDT)"
|
||||||
|
STR_TZ_AEST: "Australia Eastern (AEST/AEDT)"
|
||||||
|
STR_TZ_NZST: "New Zealand (NZST/NZDT)"
|
||||||
|
STR_TZ_MSK: "Russia (MSK)"
|
||||||
|
STR_TZ_UTC_MINUS3: "South America (UTC-3)"
|
||||||
|
STR_TZ_UTC_PLUS4: "Gulf (UTC+4)"
|
||||||
|
STR_TZ_IST: "India (UTC+5:30)"
|
||||||
|
STR_TZ_UTC_PLUS7: "SE Asia (UTC+7)"
|
||||||
|
STR_TZ_UTC_PLUS8: "China/SE Asia (UTC+8)"
|
||||||
|
STR_TZ_UTC_PLUS9: "Japan/Korea (UTC+9)"
|
||||||
|
STR_SYNC_TIME: "Sync Time"
|
||||||
|
STR_DETECT_TIMEZONE: "Detect Timezone"
|
||||||
|
STR_SYNCING_CLOCK: "Syncing clock..."
|
||||||
|
STR_DETECTING_TIMEZONE: "Detecting timezone..."
|
||||||
|
STR_TIME_SYNCED: "Time synced"
|
||||||
|
STR_TIME_SYNC_FAILED: "Time sync failed"
|
||||||
|
STR_CLOCK_DRIFT: "Drift: %s"
|
||||||
|
STR_LAST_NTP_SYNC: "Last sync: %s"
|
||||||
|
STR_TIMEZONE_DETECTED: "Timezone detected"
|
||||||
|
STR_TIMEZONE_DETECT_FAILED: "Timezone detect failed"
|
||||||
|
STR_DST_ACTIVE: "DST: active"
|
||||||
|
STR_DST_INACTIVE: "DST: inactive"
|
||||||
|
STR_DST_UNKNOWN: "DST: unknown"
|
||||||
STR_REFRESH_FREQ: "Refresh Frequency"
|
STR_REFRESH_FREQ: "Refresh Frequency"
|
||||||
STR_KOREADER_SYNC: "KOReader Sync"
|
STR_KOREADER_SYNC: "KOReader Sync"
|
||||||
STR_CHECK_UPDATES: "Check for updates"
|
STR_CHECK_UPDATES: "Check for updates"
|
||||||
|
|||||||
+10
-1
@@ -1,5 +1,7 @@
|
|||||||
#include "Logging.h"
|
#include "Logging.h"
|
||||||
|
|
||||||
|
#include <HalClock.h>
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#define MAX_ENTRY_LEN 256
|
#define MAX_ENTRY_LEN 256
|
||||||
@@ -41,7 +43,14 @@ void logPrintf(const char* level, const char* origin, const char* format, ...) {
|
|||||||
// add the timestamp
|
// add the timestamp
|
||||||
{
|
{
|
||||||
unsigned long ms = millis();
|
unsigned long ms = millis();
|
||||||
int len = snprintf(c, sizeof(buf), "[%lu] ", ms);
|
char wallClock[12];
|
||||||
|
HalClock::formatLogTime(wallClock, sizeof(wallClock));
|
||||||
|
int len;
|
||||||
|
if (wallClock[0] != '\0') {
|
||||||
|
len = snprintf(c, sizeof(buf), "[%lu %s] ", ms, wallClock);
|
||||||
|
} else {
|
||||||
|
len = snprintf(c, sizeof(buf), "[%lu] ", ms);
|
||||||
|
}
|
||||||
if (len < 0) {
|
if (len < 0) {
|
||||||
return; // encoding error, skip logging
|
return; // encoding error, skip logging
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,507 @@
|
|||||||
|
#include "HalClock.h"
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <Logging.h>
|
||||||
|
#include <Preferences.h>
|
||||||
|
#include <WiFi.h>
|
||||||
|
#include <esp_private/esp_clk.h>
|
||||||
|
#include <esp_sntp.h>
|
||||||
|
#include <sys/time.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
// ---- RTC-memory state (survives deep sleep, not cold boot) ----------------
|
||||||
|
|
||||||
|
static constexpr uint32_t CLOCK_RTC_MAGIC = 0xC10C4B1D;
|
||||||
|
static constexpr uint32_t CLOCK_RTC_FLAG_LP_VALID = 0x00000001u;
|
||||||
|
|
||||||
|
// Temperature drift model for ESP32 RTC-based timekeeping.
|
||||||
|
//
|
||||||
|
// The chip's low-power (slow) clock frequency depends on temperature.
|
||||||
|
// ESP32 variants can drift by about 2 minutes per day per °C from the
|
||||||
|
// initial captured operating temperature.
|
||||||
|
//
|
||||||
|
// - dt_drift ≈ 120 seconds/day/°C
|
||||||
|
// - relative frequency error per second per °C = 120 / 86400
|
||||||
|
//
|
||||||
|
// At restore() we apply a first-order correction over the sleep interval:
|
||||||
|
// corrected_interval = raw_interval × (1 + ΔT × drift_factor), where
|
||||||
|
// drift_factor = 120 / 86400.
|
||||||
|
//
|
||||||
|
// Experimental source: https://www.reddit.com/r/esp32/comments/11cikkp/the_clock_on_the_esp_is_wrong/
|
||||||
|
static constexpr float CLOCK_TEMP_DRIFT_SECONDS_PER_SECOND_PER_DEG = 120.0f / 86400.0f;
|
||||||
|
|
||||||
|
RTC_NOINIT_ATTR static uint32_t rtcClockMagic;
|
||||||
|
RTC_NOINIT_ATTR static uint32_t rtcClockFlags;
|
||||||
|
RTC_NOINIT_ATTR static time_t rtcEpoch; // last-known unix epoch
|
||||||
|
RTC_NOINIT_ATTR static uint64_t rtcLpTimeUs; // esp_clk_rtc_time() at capture
|
||||||
|
RTC_NOINIT_ATTR static uint32_t rtcSlowCal; // esp_clk_slowclk_cal_get() at capture
|
||||||
|
RTC_NOINIT_ATTR static float rtcTemperatureC; // captured chip temperature at save
|
||||||
|
|
||||||
|
static bool clockApproximate = true;
|
||||||
|
|
||||||
|
// Drift correction scale factor (learned from NTP sync results).
|
||||||
|
//
|
||||||
|
// Raw temp drift model uses 2 min/day/°C -> factor = 120/86400. This is a
|
||||||
|
// generic base model. The actual board may behave a bit differently. On each
|
||||||
|
// NTP sync we estimate how the local clock error compares to the model and
|
||||||
|
// update this scale factor slightly to converge toward real world behavior.
|
||||||
|
//
|
||||||
|
// rtcDriftScale = 1.0 means we trust 2 min/day/°C exactly. If the device is
|
||||||
|
// slower/faster than that, NTP drift calibration adjusts this factor.
|
||||||
|
static float rtcDriftScale = 1.0f;
|
||||||
|
|
||||||
|
static unsigned long lastPeriodicUpdateMs = 0;
|
||||||
|
static constexpr unsigned long PERIODIC_UPDATE_INTERVAL_MS = 10UL * 60UL * 1000UL;
|
||||||
|
|
||||||
|
struct TimeZoneEntry {
|
||||||
|
const char* tz;
|
||||||
|
};
|
||||||
|
|
||||||
|
static constexpr TimeZoneEntry TIMEZONES[] = {
|
||||||
|
{"GMT0BST,M3.5.0/1,M10.5.0/2"},
|
||||||
|
{"CET-1CEST,M3.5.0/2,M10.5.0/3"},
|
||||||
|
{"EET-2EEST,M3.5.0/3,M10.5.0/4"},
|
||||||
|
{"MSK-3"},
|
||||||
|
{"UTC-4"},
|
||||||
|
{"UTC-5:30"},
|
||||||
|
{"UTC-7"},
|
||||||
|
{"UTC-8"},
|
||||||
|
{"UTC-9"},
|
||||||
|
{"AEST-10AEDT,M10.1.0/2,M4.1.0/3"},
|
||||||
|
{"NZST-12NZDT,M9.5.0/2,M4.1.0/3"},
|
||||||
|
{"UTC+3"},
|
||||||
|
{"EST5EDT,M3.2.0/2,M11.1.0/2"},
|
||||||
|
{"CST6CDT,M3.2.0/2,M11.1.0/2"},
|
||||||
|
{"MST7MDT,M3.2.0/2,M11.1.0/2"},
|
||||||
|
{"PST8PDT,M3.2.0/2,M11.1.0/2"},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- NVS helpers ----------------------------------------------------------
|
||||||
|
|
||||||
|
// If the last NTP sync is older than this, treat a cold-boot restore as
|
||||||
|
// unsynced rather than showing a potentially very wrong time.
|
||||||
|
static constexpr int64_t STALE_THRESHOLD_S = 72 * 3600; // 72 hours
|
||||||
|
|
||||||
|
static constexpr char NVS_NAMESPACE[] = "halclock";
|
||||||
|
static constexpr char NVS_KEY[] = "epoch";
|
||||||
|
static constexpr char NVS_SYNC_KEY[] = "lastsync";
|
||||||
|
static constexpr char NVS_DRIFT_KEY[] = "driftcoef";
|
||||||
|
static constexpr char NVS_TEMP_KEY[] = "lasttemp";
|
||||||
|
|
||||||
|
static void nvsWrite(time_t epoch) {
|
||||||
|
Preferences prefs;
|
||||||
|
if (prefs.begin(NVS_NAMESPACE, false)) {
|
||||||
|
prefs.putLong64(NVS_KEY, (int64_t)epoch);
|
||||||
|
prefs.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void nvsWriteDriftScale(float driftScale) {
|
||||||
|
Preferences prefs;
|
||||||
|
if (prefs.begin(NVS_NAMESPACE, false)) {
|
||||||
|
prefs.putFloat(NVS_DRIFT_KEY, driftScale);
|
||||||
|
prefs.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static float nvsReadDriftScale() {
|
||||||
|
Preferences prefs;
|
||||||
|
float result = 1.0f;
|
||||||
|
if (prefs.begin(NVS_NAMESPACE, true)) {
|
||||||
|
result = prefs.getFloat(NVS_DRIFT_KEY, 1.0f);
|
||||||
|
prefs.end();
|
||||||
|
}
|
||||||
|
// Guard against NaN, Inf, or out-of-range values from corrupted NVS.
|
||||||
|
if (!std::isfinite(result) || result < 0.1f || result > 5.0f) {
|
||||||
|
result = 1.0f;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void nvsWriteLastSyncTemp(float tempC) {
|
||||||
|
Preferences prefs;
|
||||||
|
if (prefs.begin(NVS_NAMESPACE, false)) {
|
||||||
|
prefs.putFloat(NVS_TEMP_KEY, tempC);
|
||||||
|
prefs.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static float nvsReadLastSyncTemp() {
|
||||||
|
Preferences prefs;
|
||||||
|
float result = 0.0f;
|
||||||
|
if (prefs.begin(NVS_NAMESPACE, true)) {
|
||||||
|
result = prefs.getFloat(NVS_TEMP_KEY, 0.0f);
|
||||||
|
prefs.end();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void nvsWriteSyncTime(time_t syncEpoch) {
|
||||||
|
Preferences prefs;
|
||||||
|
if (prefs.begin(NVS_NAMESPACE, false)) {
|
||||||
|
prefs.putLong64(NVS_SYNC_KEY, (int64_t)syncEpoch);
|
||||||
|
prefs.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static time_t nvsRead() {
|
||||||
|
Preferences prefs;
|
||||||
|
time_t epoch = 0;
|
||||||
|
if (prefs.begin(NVS_NAMESPACE, true)) {
|
||||||
|
epoch = (time_t)prefs.getLong64(NVS_KEY, 0);
|
||||||
|
prefs.end();
|
||||||
|
}
|
||||||
|
return epoch;
|
||||||
|
}
|
||||||
|
|
||||||
|
static time_t nvsReadSyncTime() {
|
||||||
|
Preferences prefs;
|
||||||
|
time_t syncEpoch = 0;
|
||||||
|
if (prefs.begin(NVS_NAMESPACE, true)) {
|
||||||
|
syncEpoch = (time_t)prefs.getLong64(NVS_SYNC_KEY, 0);
|
||||||
|
prefs.end();
|
||||||
|
}
|
||||||
|
return syncEpoch;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- internal helpers -----------------------------------------------------
|
||||||
|
|
||||||
|
static float readChipTemperatureC() {
|
||||||
|
// ESP32 and ESP32-C3 use the internal ADC temperature sensor.
|
||||||
|
return (float)temperatureRead();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void setSystemClock(time_t epoch) {
|
||||||
|
struct timeval tv = {};
|
||||||
|
tv.tv_sec = epoch;
|
||||||
|
settimeofday(&tv, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool rtcValid() { return rtcClockMagic == CLOCK_RTC_MAGIC && rtcEpoch > 0; }
|
||||||
|
|
||||||
|
/// Compute temperature-corrected elapsed seconds from LP timer delta.
|
||||||
|
/// Uses the trapezoidal rule (average of start + end temperature) as a
|
||||||
|
/// first-order approximation of the temperature integral over the interval.
|
||||||
|
/// Returns the corrected elapsed seconds and updates lpNowOut/calNowOut
|
||||||
|
/// for the caller to re-baseline.
|
||||||
|
static double computeCorrectedElapsedSec(uint64_t lpNow, float tempNow) {
|
||||||
|
uint32_t calNow = esp_clk_slowclk_cal_get();
|
||||||
|
uint64_t elapsedUs;
|
||||||
|
if (rtcSlowCal != 0 && calNow != 0) {
|
||||||
|
// rtcLpTimeUs was computed with rtcSlowCal; convert it to the
|
||||||
|
// current calibration basis so the subtraction is consistent.
|
||||||
|
uint64_t lpThenCorrected = (uint64_t)((double)rtcLpTimeUs * calNow / rtcSlowCal);
|
||||||
|
elapsedUs = lpNow - lpThenCorrected;
|
||||||
|
} else {
|
||||||
|
elapsedUs = lpNow - rtcLpTimeUs;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the full temperature delta between the average over the interval
|
||||||
|
// and the calibration reference (which is the capture-time temperature).
|
||||||
|
// avgTemp approximates the mean temperature during the interval.
|
||||||
|
// The drift model says the RTC runs (1 + deltaT * driftRate) times
|
||||||
|
// faster/slower than nominal, so the true elapsed wall-clock time
|
||||||
|
// differs from the raw LP-derived time by that factor.
|
||||||
|
float avgTemp = (rtcTemperatureC + tempNow) * 0.5f;
|
||||||
|
float tempDelta = avgTemp - rtcTemperatureC; // = (tempNow - rtcTemperatureC) / 2
|
||||||
|
float tempFactor = 1.0f + tempDelta * CLOCK_TEMP_DRIFT_SECONDS_PER_SECOND_PER_DEG * rtcDriftScale;
|
||||||
|
if (tempFactor < 0.5f) {
|
||||||
|
tempFactor = 0.5f;
|
||||||
|
} else if (tempFactor > 1.5f) {
|
||||||
|
tempFactor = 1.5f;
|
||||||
|
}
|
||||||
|
|
||||||
|
double elapsedSec = (double)elapsedUs / 1000000.0;
|
||||||
|
double correctedSec = elapsedSec * (double)tempFactor;
|
||||||
|
|
||||||
|
LOG_DBG("CLK", "Drift calc: startT=%.1fC nowT=%.1fC dT=%.3f factor=%.6f raw=%.3fs corr=%.3fs", rtcTemperatureC,
|
||||||
|
tempNow, tempDelta, tempFactor, elapsedSec, correctedSec);
|
||||||
|
|
||||||
|
return correctedSec;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Capture current time + LP timer into RTC memory, and epoch into NVS.
|
||||||
|
static void capture(bool lpValid) {
|
||||||
|
rtcEpoch = time(nullptr);
|
||||||
|
rtcLpTimeUs = esp_clk_rtc_time();
|
||||||
|
rtcSlowCal = esp_clk_slowclk_cal_get();
|
||||||
|
rtcTemperatureC = readChipTemperatureC();
|
||||||
|
rtcClockMagic = CLOCK_RTC_MAGIC;
|
||||||
|
rtcClockFlags = lpValid ? CLOCK_RTC_FLAG_LP_VALID : 0;
|
||||||
|
nvsWrite(rtcEpoch);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- public API -----------------------------------------------------------
|
||||||
|
|
||||||
|
namespace HalClock {
|
||||||
|
|
||||||
|
void applyTimezone(uint8_t timeZoneSetting) {
|
||||||
|
const size_t index = timeZoneSetting < (sizeof(TIMEZONES) / sizeof(TIMEZONES[0])) ? timeZoneSetting : 0;
|
||||||
|
setenv("TZ", TIMEZONES[index].tz, 1);
|
||||||
|
tzset();
|
||||||
|
LOG_DBG("CLK", "Timezone applied: %s", TIMEZONES[index].tz);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool syncNtp() {
|
||||||
|
time_t preSyncTime = time(nullptr);
|
||||||
|
time_t prevSyncTime = nvsReadSyncTime();
|
||||||
|
float prevSyncTemp = nvsReadLastSyncTemp();
|
||||||
|
|
||||||
|
if (esp_sntp_enabled()) {
|
||||||
|
esp_sntp_stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_sntp_setoperatingmode(ESP_SNTP_OPMODE_POLL);
|
||||||
|
esp_sntp_setservername(0, "pool.ntp.org");
|
||||||
|
esp_sntp_init();
|
||||||
|
|
||||||
|
int retry = 0;
|
||||||
|
constexpr int maxRetries = 50; // 5 seconds
|
||||||
|
while (sntp_get_sync_status() != SNTP_SYNC_STATUS_COMPLETED && retry < maxRetries) {
|
||||||
|
vTaskDelay(100 / portTICK_PERIOD_MS);
|
||||||
|
retry++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (retry >= maxRetries) {
|
||||||
|
LOG_ERR("CLK", "NTP sync timeout");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
capture(false);
|
||||||
|
nvsWriteSyncTime(rtcEpoch);
|
||||||
|
|
||||||
|
float currentTemp = rtcTemperatureC;
|
||||||
|
if (currentTemp != 0.0f) {
|
||||||
|
nvsWriteLastSyncTemp(currentTemp);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prevSyncTime > 0 && preSyncTime > 0 && rtcEpoch > prevSyncTime) {
|
||||||
|
float interval = (float)(rtcEpoch - prevSyncTime);
|
||||||
|
// error = how far the local clock was off before NTP corrected it.
|
||||||
|
// Positive means local clock was behind (NTP jumped us forward).
|
||||||
|
// Negative means local clock was ahead (NTP pulled us back).
|
||||||
|
float error = (float)(preSyncTime - rtcEpoch);
|
||||||
|
if (interval >= 60.0f) {
|
||||||
|
// Convert to seconds-of-drift per day.
|
||||||
|
float observedDriftPerDay = error * 86400.0f / interval;
|
||||||
|
|
||||||
|
// Adaptive model calibration:
|
||||||
|
// - Observed drift is derived from the difference between local clock
|
||||||
|
// reading just before NTP and the true time reported by NTP, scaled
|
||||||
|
// to a per-day rate over the interval since the previous sync.
|
||||||
|
// - The baseline model expects 120 sec/day per °C.
|
||||||
|
// - Measure temp delta since last sync (from stored NVS temp).
|
||||||
|
// - If large enough, compute an empirical scale to apply to the model
|
||||||
|
// so future drift corrections are better aligned with actual hardware.
|
||||||
|
// - The scale is persisted to NVS via saveBeforeSleep().
|
||||||
|
float effectiveScale = rtcDriftScale;
|
||||||
|
float tempDelta = currentTemp - prevSyncTemp;
|
||||||
|
if (std::fabs(tempDelta) > 0.1f) {
|
||||||
|
float modelDriftPerDay = 120.0f * tempDelta;
|
||||||
|
if (std::fabs(modelDriftPerDay) > 0.01f) {
|
||||||
|
float measuredScale = observedDriftPerDay / modelDriftPerDay;
|
||||||
|
effectiveScale = 0.9f * rtcDriftScale + 0.1f * measuredScale;
|
||||||
|
effectiveScale = std::max(0.1f, std::min(5.0f, effectiveScale));
|
||||||
|
rtcDriftScale = effectiveScale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DBG("CLK", "NTP drift: interval=%.0fs error=%.3fs perDay=%.3f scale=%.3f deltaT=%.2f", interval, error,
|
||||||
|
observedDriftPerDay, rtcDriftScale, tempDelta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clockApproximate = false;
|
||||||
|
LOG_INF("CLK", "NTP synced, epoch %lld", (long long)rtcEpoch);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void saveBeforeSleep(bool keepLpAlive) {
|
||||||
|
if (!isSynced()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
capture(keepLpAlive);
|
||||||
|
// Persist learned drift scale and last temperature to NVS so they survive
|
||||||
|
// cold boot. We only write here (not periodically) to minimise flash wear.
|
||||||
|
nvsWriteDriftScale(rtcDriftScale);
|
||||||
|
nvsWriteLastSyncTemp(rtcTemperatureC);
|
||||||
|
LOG_DBG("CLK", "Saved epoch %lld before sleep (driftScale=%.3f)", (long long)rtcEpoch, rtcDriftScale);
|
||||||
|
}
|
||||||
|
|
||||||
|
void restore() {
|
||||||
|
rtcDriftScale = nvsReadDriftScale();
|
||||||
|
|
||||||
|
const bool lpValid = (rtcClockFlags & CLOCK_RTC_FLAG_LP_VALID) != 0;
|
||||||
|
if (rtcValid() && lpValid) {
|
||||||
|
// RTC memory survived — we woke from deep sleep.
|
||||||
|
//
|
||||||
|
// We restore the wall clock by computing elapsed real time from the
|
||||||
|
// LP timer delta and applying both frequency calibration and temperature
|
||||||
|
// drift correction.
|
||||||
|
//
|
||||||
|
// Steps:
|
||||||
|
// 1) Read current LP timer and slow-clock calibration.
|
||||||
|
// 2) Compute raw elapsed LP ticks, on the same calibration basis used
|
||||||
|
// when capture() was called.
|
||||||
|
// 3) Convert elapsed ticks to seconds.
|
||||||
|
// 4) Apply temperature drift correction based on measured RTC memory
|
||||||
|
// capture temperature and current chip temp.
|
||||||
|
// 5) Set system time to rtcEpoch + corrected elapsed seconds.
|
||||||
|
//
|
||||||
|
// This is an approximation: we use the average of start/end measured
|
||||||
|
// temperature as a simple integral proxy. More advanced models could
|
||||||
|
// sample temperature continuously, but this is a good tradeoff for low
|
||||||
|
// cost and better accuracy vs no temperature compensation.
|
||||||
|
uint64_t lpNow = esp_clk_rtc_time();
|
||||||
|
time_t estimated = rtcEpoch;
|
||||||
|
if (lpNow > rtcLpTimeUs) {
|
||||||
|
float tempNow = readChipTemperatureC();
|
||||||
|
double correctedSec = computeCorrectedElapsedSec(lpNow, tempNow);
|
||||||
|
estimated += (time_t)correctedSec;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSystemClock(estimated);
|
||||||
|
// Re-baseline LP timer and temperature for next interval.
|
||||||
|
rtcEpoch = estimated;
|
||||||
|
rtcLpTimeUs = esp_clk_rtc_time();
|
||||||
|
rtcSlowCal = esp_clk_slowclk_cal_get();
|
||||||
|
rtcTemperatureC = readChipTemperatureC();
|
||||||
|
clockApproximate = true;
|
||||||
|
LOG_INF("CLK", "Restored from RTC + LP timer, epoch %lld", (long long)estimated);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cold boot — try NVS. No elapsed correction possible.
|
||||||
|
time_t epoch = nvsRead();
|
||||||
|
if (epoch > 0) {
|
||||||
|
time_t lastSync = nvsReadSyncTime();
|
||||||
|
if (lastSync > 0 && (epoch - lastSync) > STALE_THRESHOLD_S) {
|
||||||
|
LOG_ERR("CLK", "NVS epoch %lld is stale (last NTP sync %lld, %lld h ago), discarding", (long long)epoch,
|
||||||
|
(long long)lastSync, (long long)((epoch - lastSync) / 3600));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSystemClock(epoch);
|
||||||
|
rtcEpoch = epoch;
|
||||||
|
rtcLpTimeUs = esp_clk_rtc_time();
|
||||||
|
rtcSlowCal = esp_clk_slowclk_cal_get();
|
||||||
|
rtcTemperatureC = nvsReadLastSyncTemp();
|
||||||
|
if (rtcTemperatureC == 0.0f) {
|
||||||
|
rtcTemperatureC = readChipTemperatureC();
|
||||||
|
}
|
||||||
|
rtcClockMagic = CLOCK_RTC_MAGIC;
|
||||||
|
rtcClockFlags = 0;
|
||||||
|
clockApproximate = true;
|
||||||
|
LOG_INF("CLK", "Restored from NVS, epoch %lld (no elapsed correction)", (long long)epoch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
time_t now() {
|
||||||
|
if (!isSynced()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return time(nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void updatePeriodic() {
|
||||||
|
if (!isSynced()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
unsigned long nowMs = millis();
|
||||||
|
if (nowMs - lastPeriodicUpdateMs < PERIODIC_UPDATE_INTERVAL_MS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastPeriodicUpdateMs = nowMs;
|
||||||
|
|
||||||
|
// Compute temperature-corrected elapsed time since last baseline and apply
|
||||||
|
// only the drift delta (correction - raw) to the system clock. The kernel
|
||||||
|
// clock already advanced by the raw amount, so we must not re-add it.
|
||||||
|
uint64_t lpNow = esp_clk_rtc_time();
|
||||||
|
if (lpNow <= rtcLpTimeUs) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
float tempNow = readChipTemperatureC();
|
||||||
|
double correctedSec = computeCorrectedElapsedSec(lpNow, tempNow);
|
||||||
|
|
||||||
|
// Raw elapsed seconds (what the kernel clock already counted).
|
||||||
|
uint64_t rawElapsedUs = lpNow - rtcLpTimeUs;
|
||||||
|
double rawSec = (double)rawElapsedUs / 1000000.0;
|
||||||
|
|
||||||
|
// The drift delta is the difference between what really elapsed
|
||||||
|
// (temperature-corrected) and what the kernel counted (raw).
|
||||||
|
double driftDeltaSec = correctedSec - rawSec;
|
||||||
|
|
||||||
|
// Re-baseline LP timer and temperature for the next interval.
|
||||||
|
rtcLpTimeUs = lpNow;
|
||||||
|
rtcSlowCal = esp_clk_slowclk_cal_get();
|
||||||
|
rtcTemperatureC = tempNow;
|
||||||
|
|
||||||
|
// Only nudge the system clock if the drift delta is meaningful (>50 ms).
|
||||||
|
// This avoids unnecessary settimeofday calls for negligible corrections.
|
||||||
|
if (std::fabs(driftDeltaSec) > 0.05) {
|
||||||
|
rtcEpoch = time(nullptr) + (time_t)driftDeltaSec;
|
||||||
|
setSystemClock(rtcEpoch);
|
||||||
|
LOG_DBG("CLK", "Periodic drift nudge: raw=%.3fs corr=%.3fs delta=%.3fs scale=%.3f", rawSec, correctedSec,
|
||||||
|
driftDeltaSec, rtcDriftScale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isSynced() {
|
||||||
|
return time(nullptr) > 1577836800; // > 2020-01-01
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isApproximate() { return clockApproximate; }
|
||||||
|
|
||||||
|
time_t lastSyncTime() { return nvsReadSyncTime(); }
|
||||||
|
|
||||||
|
void formatTime(char* buf, size_t bufSize, bool use24h) {
|
||||||
|
if (!isSynced()) {
|
||||||
|
snprintf(buf, bufSize, "--:--");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
time_t t = time(nullptr);
|
||||||
|
struct tm timeinfo;
|
||||||
|
localtime_r(&t, &timeinfo);
|
||||||
|
|
||||||
|
const char* prefix = isApproximate() ? "~" : "";
|
||||||
|
|
||||||
|
if (use24h) {
|
||||||
|
snprintf(buf, bufSize, "%s%02d:%02d", prefix, timeinfo.tm_hour, timeinfo.tm_min);
|
||||||
|
} else {
|
||||||
|
int hour = timeinfo.tm_hour % 12;
|
||||||
|
if (hour == 0) hour = 12;
|
||||||
|
const char* ampm = timeinfo.tm_hour < 12 ? "AM" : "PM";
|
||||||
|
snprintf(buf, bufSize, "%s%d:%02d%s", prefix, hour, timeinfo.tm_min, ampm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void formatLogTime(char* buf, size_t bufSize) {
|
||||||
|
if (!isSynced()) {
|
||||||
|
buf[0] = '\0';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
time_t t = time(nullptr);
|
||||||
|
struct tm timeinfo;
|
||||||
|
localtime_r(&t, &timeinfo);
|
||||||
|
snprintf(buf, bufSize, "%02d:%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
|
||||||
|
}
|
||||||
|
|
||||||
|
void wifiOff(bool skipNtpSync) {
|
||||||
|
if (!skipNtpSync && isApproximate() && WiFi.getMode() == WIFI_STA && WiFi.status() == WL_CONNECTED) {
|
||||||
|
syncNtp();
|
||||||
|
}
|
||||||
|
if (esp_sntp_enabled()) {
|
||||||
|
esp_sntp_stop();
|
||||||
|
}
|
||||||
|
WiFi.disconnect(false);
|
||||||
|
delay(100);
|
||||||
|
WiFi.mode(WIFI_OFF);
|
||||||
|
delay(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace HalClock
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <ctime>
|
||||||
|
|
||||||
|
/// Lightweight wall-clock facade.
|
||||||
|
///
|
||||||
|
/// The ESP32-C3 has no battery-backed RTC, so wall-clock time is lost on every
|
||||||
|
/// deep-sleep / power cycle. HalClock bridges this gap using three layers:
|
||||||
|
///
|
||||||
|
/// - **LP timer** (`esp_clk_rtc_time()`) — keeps running during deep sleep
|
||||||
|
/// when `keepClockAlive` is enabled (GPIO13 stays HIGH). Used to compute
|
||||||
|
/// elapsed time and correct the stored epoch on wake.
|
||||||
|
/// - **RTC memory** (`RTC_NOINIT_ATTR`) — survives deep sleep, lost on cold
|
||||||
|
/// boot. Stores the epoch + LP timer value captured before sleep.
|
||||||
|
/// - **NVS** (flash key-value store) — survives power cycles. Fallback when
|
||||||
|
/// RTC memory is unavailable (cold boot).
|
||||||
|
///
|
||||||
|
/// Usage:
|
||||||
|
/// 1. On boot, call `restore()` to seed the system clock from the best
|
||||||
|
/// available source (RTC memory + LP correction > NVS).
|
||||||
|
/// 2. After a successful NTP sync, call `syncNtp()`.
|
||||||
|
/// 3. Before entering deep sleep, call `saveBeforeSleep()`.
|
||||||
|
///
|
||||||
|
/// `now()` returns the best-effort epoch (0 if never synced).
|
||||||
|
namespace HalClock {
|
||||||
|
|
||||||
|
/// Perform an NTP sync (requires WiFi to be connected). Starts SNTP,
|
||||||
|
/// waits up to 5 seconds for completion, then captures the result.
|
||||||
|
/// Returns true if the sync succeeded.
|
||||||
|
bool syncNtp();
|
||||||
|
|
||||||
|
/// Apply timezone/DST rules via the POSIX TZ string for the given setting.
|
||||||
|
void applyTimezone(uint8_t timeZoneSetting);
|
||||||
|
|
||||||
|
/// Call just before deep sleep. Snapshots the current system time to RTC
|
||||||
|
/// memory and NVS so it can be restored on wake / cold boot. Pass true when
|
||||||
|
/// the LP timer is kept alive during sleep.
|
||||||
|
void saveBeforeSleep(bool keepLpAlive);
|
||||||
|
|
||||||
|
/// Call on boot to seed the system clock from the best available stored
|
||||||
|
/// value. When RTC memory is valid (deep-sleep wake) and the LP timer was
|
||||||
|
/// running, the restored time includes elapsed-time correction. Falls back
|
||||||
|
/// to NVS for cold boot (stale, but better than nothing).
|
||||||
|
void restore();
|
||||||
|
|
||||||
|
/// Returns the current best-effort wall-clock epoch, or 0 if the clock was
|
||||||
|
/// never set.
|
||||||
|
time_t now();
|
||||||
|
|
||||||
|
/// True if the clock has been set at least once (NTP or restore).
|
||||||
|
bool isSynced();
|
||||||
|
|
||||||
|
/// Periodic callback (called from main loop) to compensate temperature-induced
|
||||||
|
/// RTC drift while the device is awake. Runs at a 10-minute interval.
|
||||||
|
/// Computes the drift delta since the last baseline using the temperature
|
||||||
|
/// model and nudges the system clock by only that delta (the kernel clock
|
||||||
|
/// already advanced the raw amount). Drift state is persisted to NVS only
|
||||||
|
/// in saveBeforeSleep() to minimise flash wear.
|
||||||
|
void updatePeriodic();
|
||||||
|
|
||||||
|
/// True if the last restore was from a backup (not NTP) — i.e. the clock
|
||||||
|
/// may have drifted. Cleared on NTP sync.
|
||||||
|
bool isApproximate();
|
||||||
|
|
||||||
|
/// Returns the epoch of the last successful NTP sync (from NVS), or 0 if
|
||||||
|
/// no sync has ever been recorded.
|
||||||
|
time_t lastSyncTime();
|
||||||
|
|
||||||
|
/// Format the current time for display. Returns "--:--" if the clock was
|
||||||
|
/// never synced, prefixes with "~" if approximate.
|
||||||
|
/// When use24h is false, formats as "2:05pm" / "12:30am".
|
||||||
|
/// Output is written to `buf` (must be at least 16 bytes).
|
||||||
|
void formatTime(char* buf, size_t bufSize, bool use24h);
|
||||||
|
|
||||||
|
/// Format the current time for log timestamps. Returns "HH:MM:SS" if
|
||||||
|
/// synced, or an empty string if not.
|
||||||
|
void formatLogTime(char* buf, size_t bufSize);
|
||||||
|
|
||||||
|
/// Tear down WiFi cleanly. When skipNtpSync is false (default) and the
|
||||||
|
/// clock is approximate, performs an opportunistic NTP sync before
|
||||||
|
/// disconnecting — essentially free since we already have a connection.
|
||||||
|
void wifiOff(bool skipNtpSync = false);
|
||||||
|
|
||||||
|
} // namespace HalClock
|
||||||
@@ -52,26 +52,30 @@ void HalPowerManager::setPowerSaving(bool enabled) {
|
|||||||
// Otherwise, no change needed
|
// Otherwise, no change needed
|
||||||
}
|
}
|
||||||
|
|
||||||
void HalPowerManager::startDeepSleep(HalGPIO& gpio) const {
|
void HalPowerManager::startDeepSleep(HalGPIO& gpio, bool keepClockAlive) const {
|
||||||
// Ensure that the power button has been released to avoid immediately turning back on if you're holding it
|
// Ensure that the power button has been released to avoid immediately turning back on if you're holding it
|
||||||
while (gpio.isPressed(HalGPIO::BTN_POWER)) {
|
while (gpio.isPressed(HalGPIO::BTN_POWER)) {
|
||||||
delay(50);
|
delay(50);
|
||||||
gpio.update();
|
gpio.update();
|
||||||
}
|
}
|
||||||
// Pre-sleep routines from the original firmware
|
// GPIO13 is connected to the battery latch MOSFET.
|
||||||
// GPIO13 is connected to battery latch MOSFET, we need to make sure it's low during sleep
|
// When keepClockAlive is false (default): GPIO13 goes LOW, the MCU is
|
||||||
// Note that this means the MCU will be completely powered off during sleep, including RTC
|
// completely powered off during sleep (including the LP timer / RTC memory).
|
||||||
|
// When keepClockAlive is true: GPIO13 stays HIGH, the MCU remains powered
|
||||||
|
// at ~3-4 mA so the LP timer keeps running and RTC memory is preserved.
|
||||||
|
// This allows HalClock to accurately compute elapsed sleep time on wake.
|
||||||
constexpr gpio_num_t GPIO_SPIWP = GPIO_NUM_13;
|
constexpr gpio_num_t GPIO_SPIWP = GPIO_NUM_13;
|
||||||
gpio_set_direction(GPIO_SPIWP, GPIO_MODE_OUTPUT);
|
gpio_set_direction(GPIO_SPIWP, GPIO_MODE_OUTPUT);
|
||||||
gpio_set_level(GPIO_SPIWP, 0);
|
gpio_set_level(GPIO_SPIWP, keepClockAlive ? 1 : 0);
|
||||||
esp_sleep_config_gpio_isolate();
|
esp_sleep_config_gpio_isolate();
|
||||||
gpio_deep_sleep_hold_en();
|
gpio_deep_sleep_hold_en();
|
||||||
gpio_hold_en(GPIO_SPIWP);
|
gpio_hold_en(GPIO_SPIWP);
|
||||||
pinMode(InputManager::POWER_BUTTON_PIN, INPUT_PULLUP);
|
pinMode(InputManager::POWER_BUTTON_PIN, INPUT_PULLUP);
|
||||||
// Arm the wakeup trigger *after* the button is released
|
// Arm the wakeup trigger *after* the button is released
|
||||||
// Note: this is only useful for waking up on USB power. On battery, the MCU will be completely powered off, so the
|
// Note: when keepClockAlive is false, this is only useful for waking up on USB power. On battery, the MCU will be
|
||||||
// power button is hard-wired to briefly provide power to the MCU, waking it up regardless of the wakeup source
|
// completely powered off, so the power button is hard-wired to briefly provide power to the MCU, waking it up
|
||||||
// configuration
|
// regardless of the wakeup source configuration.
|
||||||
|
// When keepClockAlive is true, this is the actual wakeup mechanism since the MCU stays powered.
|
||||||
esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW);
|
esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW);
|
||||||
// Enter Deep Sleep
|
// Enter Deep Sleep
|
||||||
esp_deep_sleep_start();
|
esp_deep_sleep_start();
|
||||||
|
|||||||
@@ -29,9 +29,11 @@ class HalPowerManager {
|
|||||||
// Control CPU frequency for power saving
|
// Control CPU frequency for power saving
|
||||||
void setPowerSaving(bool enabled);
|
void setPowerSaving(bool enabled);
|
||||||
|
|
||||||
// Setup wake up GPIO and enter deep sleep
|
// Setup wake up GPIO and enter deep sleep.
|
||||||
// Should be called inside main loop() to handle the currentLockMode
|
// When keepClockAlive is true, GPIO13 stays HIGH so the LP timer keeps
|
||||||
void startDeepSleep(HalGPIO& gpio) const;
|
// running during sleep (~3-4 mA extra). This allows HalClock to compute
|
||||||
|
// elapsed sleep time and restore the wall clock accurately on wake.
|
||||||
|
void startDeepSleep(HalGPIO& gpio, bool keepClockAlive = false) const;
|
||||||
|
|
||||||
// Get battery percentage (range 0-100)
|
// Get battery percentage (range 0-100)
|
||||||
uint16_t getBatteryPercentage() const;
|
uint16_t getBatteryPercentage() const;
|
||||||
|
|||||||
@@ -138,6 +138,27 @@ class CrossPointSettings {
|
|||||||
// Image rendering in EPUB reader
|
// Image rendering in EPUB reader
|
||||||
enum IMAGE_RENDERING { IMAGES_DISPLAY = 0, IMAGES_PLACEHOLDER = 1, IMAGES_SUPPRESS = 2, IMAGE_RENDERING_COUNT };
|
enum IMAGE_RENDERING { IMAGES_DISPLAY = 0, IMAGES_PLACEHOLDER = 1, IMAGES_SUPPRESS = 2, IMAGE_RENDERING_COUNT };
|
||||||
|
|
||||||
|
// Timezone options (POSIX TZ rules for DST support)
|
||||||
|
enum TIMEZONE {
|
||||||
|
TZ_UTC = 0,
|
||||||
|
TZ_CET = 1,
|
||||||
|
TZ_EET = 2,
|
||||||
|
TZ_MSK = 3,
|
||||||
|
TZ_UTC_PLUS4 = 4,
|
||||||
|
TZ_IST = 5,
|
||||||
|
TZ_UTC_PLUS7 = 6,
|
||||||
|
TZ_UTC_PLUS8 = 7,
|
||||||
|
TZ_UTC_PLUS9 = 8,
|
||||||
|
TZ_AEST = 9,
|
||||||
|
TZ_NZST = 10,
|
||||||
|
TZ_UTC_MINUS3 = 11,
|
||||||
|
TZ_EST = 12,
|
||||||
|
TZ_CST = 13,
|
||||||
|
TZ_MST = 14,
|
||||||
|
TZ_PST = 15,
|
||||||
|
TIMEZONE_COUNT
|
||||||
|
};
|
||||||
|
|
||||||
// Sleep screen settings
|
// Sleep screen settings
|
||||||
uint8_t sleepScreen = DARK;
|
uint8_t sleepScreen = DARK;
|
||||||
// Sleep screen cover mode settings
|
// Sleep screen cover mode settings
|
||||||
@@ -204,6 +225,15 @@ class CrossPointSettings {
|
|||||||
uint8_t imageRendering = IMAGES_DISPLAY;
|
uint8_t imageRendering = IMAGES_DISPLAY;
|
||||||
// Enable synthetic TOC fallback for malformed/sparse TOC books (1 = enabled, 0 = disabled)
|
// Enable synthetic TOC fallback for malformed/sparse TOC books (1 = enabled, 0 = disabled)
|
||||||
uint8_t syntheticTocFallback = 1;
|
uint8_t syntheticTocFallback = 1;
|
||||||
|
// Show clock in the reader status bar
|
||||||
|
uint8_t statusBarClock = 0;
|
||||||
|
// Clock format: 0 = 24h (14:00), 1 = 12h (2:00pm)
|
||||||
|
uint8_t clockFormat12h = 0;
|
||||||
|
// Timezone selection (applies POSIX TZ rules for DST)
|
||||||
|
uint8_t timeZone = TZ_UTC;
|
||||||
|
// Use clock and keep the LP timer running during deep sleep (GPIO13 HIGH)
|
||||||
|
// so time can be accurately restored on wake. Increases sleep current by ~3-4 mA.
|
||||||
|
uint8_t useClock = 0;
|
||||||
|
|
||||||
~CrossPointSettings() = default;
|
~CrossPointSettings() = default;
|
||||||
|
|
||||||
|
|||||||
@@ -84,6 +84,15 @@ inline const std::vector<SettingInfo>& getSettingsList() {
|
|||||||
"sleepTimeout", StrId::STR_CAT_SYSTEM),
|
"sleepTimeout", StrId::STR_CAT_SYSTEM),
|
||||||
SettingInfo::Toggle(StrId::STR_SHOW_HIDDEN_FILES, &CrossPointSettings::showHiddenFiles, "showHiddenFiles",
|
SettingInfo::Toggle(StrId::STR_SHOW_HIDDEN_FILES, &CrossPointSettings::showHiddenFiles, "showHiddenFiles",
|
||||||
StrId::STR_CAT_SYSTEM),
|
StrId::STR_CAT_SYSTEM),
|
||||||
|
SettingInfo::Enum(StrId::STR_CLOCK_FORMAT, &CrossPointSettings::clockFormat12h, {StrId::STR_24H, StrId::STR_12H},
|
||||||
|
"clockFormat12h", StrId::STR_CAT_SYSTEM),
|
||||||
|
SettingInfo::Enum(StrId::STR_TIMEZONE, &CrossPointSettings::timeZone,
|
||||||
|
{StrId::STR_TZ_UTC, StrId::STR_TZ_CET, StrId::STR_TZ_EET, StrId::STR_TZ_MSK,
|
||||||
|
StrId::STR_TZ_UTC_PLUS4, StrId::STR_TZ_IST, StrId::STR_TZ_UTC_PLUS7, StrId::STR_TZ_UTC_PLUS8,
|
||||||
|
StrId::STR_TZ_UTC_PLUS9, StrId::STR_TZ_AEST, StrId::STR_TZ_NZST, StrId::STR_TZ_UTC_MINUS3,
|
||||||
|
StrId::STR_TZ_EST, StrId::STR_TZ_CST, StrId::STR_TZ_MST, StrId::STR_TZ_PST},
|
||||||
|
"timeZone", StrId::STR_CAT_SYSTEM),
|
||||||
|
SettingInfo::Toggle(StrId::STR_USE_CLOCK, &CrossPointSettings::useClock, "useClock", StrId::STR_CAT_SYSTEM),
|
||||||
|
|
||||||
// --- KOReader Sync (web-only, uses KOReaderCredentialStore) ---
|
// --- KOReader Sync (web-only, uses KOReaderCredentialStore) ---
|
||||||
SettingInfo::DynamicString(
|
SettingInfo::DynamicString(
|
||||||
@@ -140,6 +149,8 @@ inline const std::vector<SettingInfo>& getSettingsList() {
|
|||||||
StrId::STR_CUSTOMISE_STATUS_BAR),
|
StrId::STR_CUSTOMISE_STATUS_BAR),
|
||||||
SettingInfo::Toggle(StrId::STR_BATTERY, &CrossPointSettings::statusBarBattery, "statusBarBattery",
|
SettingInfo::Toggle(StrId::STR_BATTERY, &CrossPointSettings::statusBarBattery, "statusBarBattery",
|
||||||
StrId::STR_CUSTOMISE_STATUS_BAR),
|
StrId::STR_CUSTOMISE_STATUS_BAR),
|
||||||
|
SettingInfo::Toggle(StrId::STR_CLOCK, &CrossPointSettings::statusBarClock, "statusBarClock",
|
||||||
|
StrId::STR_CUSTOMISE_STATUS_BAR),
|
||||||
};
|
};
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#include "ActivityManager.h"
|
#include "ActivityManager.h"
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <HalClock.h>
|
||||||
#include <HalPowerManager.h>
|
#include <HalPowerManager.h>
|
||||||
|
|
||||||
#include "boot_sleep/BootActivity.h"
|
#include "boot_sleep/BootActivity.h"
|
||||||
@@ -56,6 +58,18 @@ void ActivityManager::loop() {
|
|||||||
currentActivity->loop();
|
currentActivity->loop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (SETTINGS.useClock && HalClock::isSynced()) {
|
||||||
|
time_t now = HalClock::now();
|
||||||
|
if (now > 0) {
|
||||||
|
static time_t lastMinute = -1;
|
||||||
|
time_t minute = now / 60;
|
||||||
|
if (minute != lastMinute) {
|
||||||
|
lastMinute = minute;
|
||||||
|
requestUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
while (pendingAction != PendingAction::None) {
|
while (pendingAction != PendingAction::None) {
|
||||||
if (pendingAction == PendingAction::Pop) {
|
if (pendingAction == PendingAction::Pop) {
|
||||||
RenderLock lock;
|
RenderLock lock;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <Epub.h>
|
#include <Epub.h>
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
|
#include <HalClock.h>
|
||||||
#include <I18n.h>
|
#include <I18n.h>
|
||||||
#include <Logging.h>
|
#include <Logging.h>
|
||||||
#include <OpdsStream.h>
|
#include <OpdsStream.h>
|
||||||
@@ -39,8 +40,7 @@ void OpdsBookBrowserActivity::onEnter() {
|
|||||||
void OpdsBookBrowserActivity::onExit() {
|
void OpdsBookBrowserActivity::onExit() {
|
||||||
Activity::onExit();
|
Activity::onExit();
|
||||||
|
|
||||||
// Turn off WiFi when exiting
|
HalClock::wifiOff();
|
||||||
WiFi.mode(WIFI_OFF);
|
|
||||||
|
|
||||||
entries.clear();
|
entries.clear();
|
||||||
navigationHistory.clear();
|
navigationHistory.clear();
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include <DNSServer.h>
|
#include <DNSServer.h>
|
||||||
#include <ESPmDNS.h>
|
#include <ESPmDNS.h>
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
|
#include <HalClock.h>
|
||||||
#include <I18n.h>
|
#include <I18n.h>
|
||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
#include <esp_task_wdt.h>
|
#include <esp_task_wdt.h>
|
||||||
@@ -86,15 +87,12 @@ void CrossPointWebServerActivity::onExit() {
|
|||||||
if (isApMode) {
|
if (isApMode) {
|
||||||
LOG_DBG("WEBACT", "Stopping WiFi AP...");
|
LOG_DBG("WEBACT", "Stopping WiFi AP...");
|
||||||
WiFi.softAPdisconnect(true);
|
WiFi.softAPdisconnect(true);
|
||||||
|
delay(30);
|
||||||
|
WiFi.mode(WIFI_OFF);
|
||||||
|
delay(30);
|
||||||
} else {
|
} else {
|
||||||
LOG_DBG("WEBACT", "Disconnecting WiFi (graceful)...");
|
HalClock::wifiOff();
|
||||||
WiFi.disconnect(false); // false = don't erase credentials, send disconnect frame
|
|
||||||
}
|
}
|
||||||
delay(30); // Allow disconnect frame to be sent
|
|
||||||
|
|
||||||
LOG_DBG("WEBACT", "Setting WiFi mode OFF...");
|
|
||||||
WiFi.mode(WIFI_OFF);
|
|
||||||
delay(30); // Allow WiFi hardware to power down
|
|
||||||
|
|
||||||
LOG_DBG("WEBACT", "Free heap at onExit end: %d bytes", ESP.getFreeHeap());
|
LOG_DBG("WEBACT", "Free heap at onExit end: %d bytes", ESP.getFreeHeap());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
#include "KOReaderSyncActivity.h"
|
#include "KOReaderSyncActivity.h"
|
||||||
|
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
|
#include <HalClock.h>
|
||||||
#include <I18n.h>
|
#include <I18n.h>
|
||||||
#include <Logging.h>
|
#include <Logging.h>
|
||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
#include <esp_sntp.h>
|
|
||||||
|
|
||||||
#include "KOReaderCredentialStore.h"
|
#include "KOReaderCredentialStore.h"
|
||||||
#include "KOReaderDocumentId.h"
|
#include "KOReaderDocumentId.h"
|
||||||
@@ -13,43 +13,6 @@
|
|||||||
#include "components/UITheme.h"
|
#include "components/UITheme.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
|
||||||
namespace {
|
|
||||||
void syncTimeWithNTP() {
|
|
||||||
// Stop SNTP if already running (can't reconfigure while running)
|
|
||||||
if (esp_sntp_enabled()) {
|
|
||||||
esp_sntp_stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configure SNTP
|
|
||||||
esp_sntp_setoperatingmode(ESP_SNTP_OPMODE_POLL);
|
|
||||||
esp_sntp_setservername(0, "pool.ntp.org");
|
|
||||||
esp_sntp_init();
|
|
||||||
|
|
||||||
// Wait for time to sync (with timeout)
|
|
||||||
int retry = 0;
|
|
||||||
const int maxRetries = 50; // 5 seconds max
|
|
||||||
while (sntp_get_sync_status() != SNTP_SYNC_STATUS_COMPLETED && retry < maxRetries) {
|
|
||||||
vTaskDelay(100 / portTICK_PERIOD_MS);
|
|
||||||
retry++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (retry < maxRetries) {
|
|
||||||
LOG_DBG("KOSync", "NTP time synced");
|
|
||||||
} else {
|
|
||||||
LOG_DBG("KOSync", "NTP sync timeout, using fallback");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
void wifiOff() {
|
|
||||||
if (esp_sntp_enabled()) {
|
|
||||||
esp_sntp_stop();
|
|
||||||
}
|
|
||||||
WiFi.disconnect(false);
|
|
||||||
delay(100);
|
|
||||||
WiFi.mode(WIFI_OFF);
|
|
||||||
delay(100);
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
void KOReaderSyncActivity::onWifiSelectionComplete(const bool success) {
|
void KOReaderSyncActivity::onWifiSelectionComplete(const bool success) {
|
||||||
if (!success) {
|
if (!success) {
|
||||||
LOG_DBG("KOSync", "WiFi connection failed, exiting");
|
LOG_DBG("KOSync", "WiFi connection failed, exiting");
|
||||||
@@ -70,7 +33,7 @@ void KOReaderSyncActivity::onWifiSelectionComplete(const bool success) {
|
|||||||
requestUpdate(true);
|
requestUpdate(true);
|
||||||
|
|
||||||
// Sync time with NTP before making API requests
|
// Sync time with NTP before making API requests
|
||||||
syncTimeWithNTP();
|
HalClock::syncNtp();
|
||||||
|
|
||||||
{
|
{
|
||||||
RenderLock lock(*this);
|
RenderLock lock(*this);
|
||||||
@@ -187,7 +150,7 @@ void KOReaderSyncActivity::performUpload() {
|
|||||||
const auto result = KOReaderSyncClient::updateProgress(progress);
|
const auto result = KOReaderSyncClient::updateProgress(progress);
|
||||||
|
|
||||||
if (result != KOReaderSyncClient::OK) {
|
if (result != KOReaderSyncClient::OK) {
|
||||||
wifiOff();
|
HalClock::wifiOff(true);
|
||||||
{
|
{
|
||||||
RenderLock lock(*this);
|
RenderLock lock(*this);
|
||||||
state = SYNC_FAILED;
|
state = SYNC_FAILED;
|
||||||
@@ -197,7 +160,7 @@ void KOReaderSyncActivity::performUpload() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
wifiOff();
|
HalClock::wifiOff(true);
|
||||||
{
|
{
|
||||||
RenderLock lock(*this);
|
RenderLock lock(*this);
|
||||||
state = UPLOAD_COMPLETE;
|
state = UPLOAD_COMPLETE;
|
||||||
@@ -232,7 +195,7 @@ void KOReaderSyncActivity::onEnter() {
|
|||||||
void KOReaderSyncActivity::onExit() {
|
void KOReaderSyncActivity::onExit() {
|
||||||
Activity::onExit();
|
Activity::onExit();
|
||||||
|
|
||||||
wifiOff();
|
HalClock::wifiOff(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void KOReaderSyncActivity::closeCancelled() {
|
void KOReaderSyncActivity::closeCancelled() {
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
#include "ClockSettingsActivity.h"
|
||||||
|
|
||||||
|
#include <GfxRenderer.h>
|
||||||
|
#include <HalClock.h>
|
||||||
|
#include <I18n.h>
|
||||||
|
|
||||||
|
#include "CrossPointSettings.h"
|
||||||
|
#include "DetectTimezoneActivity.h"
|
||||||
|
#include "MappedInputManager.h"
|
||||||
|
#include "SyncTimeActivity.h"
|
||||||
|
#include "components/UITheme.h"
|
||||||
|
#include "fontIds.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr int MENU_ITEMS = 5;
|
||||||
|
const StrId menuNames[MENU_ITEMS] = {StrId::STR_USE_CLOCK, StrId::STR_CLOCK_FORMAT, StrId::STR_TIMEZONE,
|
||||||
|
StrId::STR_SYNC_TIME, StrId::STR_DETECT_TIMEZONE};
|
||||||
|
|
||||||
|
const StrId timeZoneNames[CrossPointSettings::TIMEZONE_COUNT] = {
|
||||||
|
StrId::STR_TZ_UTC, StrId::STR_TZ_CET, StrId::STR_TZ_EET, StrId::STR_TZ_MSK,
|
||||||
|
StrId::STR_TZ_UTC_PLUS4, StrId::STR_TZ_IST, StrId::STR_TZ_UTC_PLUS7, StrId::STR_TZ_UTC_PLUS8,
|
||||||
|
StrId::STR_TZ_UTC_PLUS9, StrId::STR_TZ_AEST, StrId::STR_TZ_NZST, StrId::STR_TZ_UTC_MINUS3,
|
||||||
|
StrId::STR_TZ_EST, StrId::STR_TZ_CST, StrId::STR_TZ_MST, StrId::STR_TZ_PST};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void ClockSettingsActivity::onEnter() {
|
||||||
|
Activity::onEnter();
|
||||||
|
selectedIndex = 0;
|
||||||
|
requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClockSettingsActivity::onExit() { Activity::onExit(); }
|
||||||
|
|
||||||
|
void ClockSettingsActivity::loop() {
|
||||||
|
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
||||||
|
handleSelection();
|
||||||
|
requestUpdate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
buttonNavigator.onNextRelease([this] {
|
||||||
|
selectedIndex = ButtonNavigator::nextIndex(selectedIndex, MENU_ITEMS);
|
||||||
|
requestUpdate();
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onPreviousRelease([this] {
|
||||||
|
selectedIndex = ButtonNavigator::previousIndex(selectedIndex, MENU_ITEMS);
|
||||||
|
requestUpdate();
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onNextContinuous([this] {
|
||||||
|
selectedIndex = ButtonNavigator::nextIndex(selectedIndex, MENU_ITEMS);
|
||||||
|
requestUpdate();
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonNavigator.onPreviousContinuous([this] {
|
||||||
|
selectedIndex = ButtonNavigator::previousIndex(selectedIndex, MENU_ITEMS);
|
||||||
|
requestUpdate();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClockSettingsActivity::handleSelection() {
|
||||||
|
if (selectedIndex == 0) {
|
||||||
|
SETTINGS.useClock = (SETTINGS.useClock + 1) % 2;
|
||||||
|
if (!SETTINGS.useClock) {
|
||||||
|
SETTINGS.statusBarClock = 0;
|
||||||
|
}
|
||||||
|
SETTINGS.saveToFile();
|
||||||
|
} else if (selectedIndex == 1) {
|
||||||
|
SETTINGS.clockFormat12h = (SETTINGS.clockFormat12h + 1) % 2;
|
||||||
|
SETTINGS.saveToFile();
|
||||||
|
} else if (selectedIndex == 2) {
|
||||||
|
SETTINGS.timeZone = (SETTINGS.timeZone + 1) % CrossPointSettings::TIMEZONE_COUNT;
|
||||||
|
HalClock::applyTimezone(SETTINGS.timeZone);
|
||||||
|
SETTINGS.saveToFile();
|
||||||
|
} else if (selectedIndex == 3) {
|
||||||
|
auto resultHandler = [](const ActivityResult&) { SETTINGS.saveToFile(); };
|
||||||
|
startActivityForResult(std::make_unique<SyncTimeActivity>(renderer, mappedInput), resultHandler);
|
||||||
|
} else if (selectedIndex == 4) {
|
||||||
|
auto resultHandler = [](const ActivityResult&) { SETTINGS.saveToFile(); };
|
||||||
|
startActivityForResult(std::make_unique<DetectTimezoneActivity>(renderer, mappedInput), resultHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClockSettingsActivity::render(RenderLock&&) {
|
||||||
|
renderer.clearScreen();
|
||||||
|
|
||||||
|
const auto& metrics = UITheme::getInstance().getMetrics();
|
||||||
|
const auto pageWidth = renderer.getScreenWidth();
|
||||||
|
const auto pageHeight = renderer.getScreenHeight();
|
||||||
|
|
||||||
|
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_CLOCK_SETTINGS));
|
||||||
|
GUI.drawSubHeader(renderer, Rect{0, metrics.topPadding + metrics.headerHeight, pageWidth, metrics.tabBarHeight},
|
||||||
|
tr(STR_CLOCK_SETTINGS_WARNING));
|
||||||
|
|
||||||
|
const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight + metrics.verticalSpacing;
|
||||||
|
const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing * 2;
|
||||||
|
|
||||||
|
GUI.drawList(
|
||||||
|
renderer, Rect{0, contentTop, pageWidth, contentHeight}, MENU_ITEMS, selectedIndex,
|
||||||
|
[](int index) { return std::string(I18N.get(menuNames[index])); }, nullptr, nullptr,
|
||||||
|
[](int index) {
|
||||||
|
if (index == 0) {
|
||||||
|
return std::string(SETTINGS.useClock ? tr(STR_STATE_ON) : tr(STR_STATE_OFF));
|
||||||
|
}
|
||||||
|
if (index == 1) {
|
||||||
|
return std::string(SETTINGS.clockFormat12h ? tr(STR_12H) : tr(STR_24H));
|
||||||
|
}
|
||||||
|
if (index == 2) {
|
||||||
|
const auto tzIndex = static_cast<size_t>(SETTINGS.timeZone);
|
||||||
|
if (tzIndex < (sizeof(timeZoneNames) / sizeof(timeZoneNames[0]))) {
|
||||||
|
return std::string(I18N.get(timeZoneNames[tzIndex]));
|
||||||
|
}
|
||||||
|
return std::string(tr(STR_TZ_UTC));
|
||||||
|
}
|
||||||
|
return std::string("");
|
||||||
|
},
|
||||||
|
true);
|
||||||
|
|
||||||
|
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
|
||||||
|
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
|
|
||||||
|
renderer.displayBuffer();
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <I18n.h>
|
||||||
|
|
||||||
|
#include "activities/Activity.h"
|
||||||
|
#include "util/ButtonNavigator.h"
|
||||||
|
|
||||||
|
class ClockSettingsActivity final : public Activity {
|
||||||
|
ButtonNavigator buttonNavigator;
|
||||||
|
int selectedIndex = 0;
|
||||||
|
|
||||||
|
void handleSelection();
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit ClockSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
|
||||||
|
: Activity("ClockSettings", renderer, mappedInput) {}
|
||||||
|
void onEnter() override;
|
||||||
|
void onExit() override;
|
||||||
|
void loop() override;
|
||||||
|
void render(RenderLock&&) override;
|
||||||
|
};
|
||||||
@@ -0,0 +1,308 @@
|
|||||||
|
#include "DetectTimezoneActivity.h"
|
||||||
|
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
#include <GfxRenderer.h>
|
||||||
|
#include <HalClock.h>
|
||||||
|
#include <I18n.h>
|
||||||
|
#include <Logging.h>
|
||||||
|
#include <WiFi.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "CrossPointSettings.h"
|
||||||
|
#include "MappedInputManager.h"
|
||||||
|
#include "activities/network/WifiSelectionActivity.h"
|
||||||
|
#include "components/UITheme.h"
|
||||||
|
#include "fontIds.h"
|
||||||
|
#include "network/HttpDownloader.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
bool mapIanaTimezone(const std::string& tz, uint8_t& outSetting) {
|
||||||
|
using TZ = CrossPointSettings::TIMEZONE;
|
||||||
|
if (tz == "UTC" || tz == "Etc/UTC") {
|
||||||
|
outSetting = TZ::TZ_UTC;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (tz == "Europe/London" || tz == "Europe/Guernsey" || tz == "Europe/Isle_of_Man" || tz == "Europe/Jersey") {
|
||||||
|
outSetting = TZ::TZ_UTC;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (tz == "Europe/Athens" || tz == "Europe/Bucharest" || tz == "Europe/Helsinki" || tz == "Europe/Kiev" ||
|
||||||
|
tz == "Europe/Vilnius" || tz == "Europe/Riga" || tz == "Europe/Tallinn") {
|
||||||
|
outSetting = TZ::TZ_EET;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (tz == "Europe/Moscow") {
|
||||||
|
outSetting = TZ::TZ_MSK;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (tz.rfind("Europe/", 0) == 0) {
|
||||||
|
outSetting = TZ::TZ_CET;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (tz == "America/New_York" || tz == "America/Toronto") {
|
||||||
|
outSetting = TZ::TZ_EST;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (tz == "America/Chicago") {
|
||||||
|
outSetting = TZ::TZ_CST;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (tz == "America/Denver") {
|
||||||
|
outSetting = TZ::TZ_MST;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (tz == "America/Los_Angeles" || tz == "America/Vancouver") {
|
||||||
|
outSetting = TZ::TZ_PST;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (tz == "America/Sao_Paulo" || tz == "America/Argentina/Buenos_Aires" || tz == "America/Montevideo") {
|
||||||
|
outSetting = TZ::TZ_UTC_MINUS3;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (tz == "Asia/Dubai" || tz == "Asia/Muscat") {
|
||||||
|
outSetting = TZ::TZ_UTC_PLUS4;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (tz == "Asia/Kolkata") {
|
||||||
|
outSetting = TZ::TZ_IST;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (tz == "Asia/Bangkok" || tz == "Asia/Ho_Chi_Minh" || tz == "Asia/Jakarta" || tz == "Asia/Phnom_Penh" ||
|
||||||
|
tz == "Asia/Vientiane") {
|
||||||
|
outSetting = TZ::TZ_UTC_PLUS7;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (tz == "Asia/Shanghai" || tz == "Asia/Hong_Kong" || tz == "Asia/Singapore" || tz == "Asia/Taipei" ||
|
||||||
|
tz == "Asia/Kuala_Lumpur" || tz == "Asia/Manila") {
|
||||||
|
outSetting = TZ::TZ_UTC_PLUS8;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (tz == "Asia/Tokyo" || tz == "Asia/Seoul") {
|
||||||
|
outSetting = TZ::TZ_UTC_PLUS9;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (tz == "Australia/Sydney" || tz == "Australia/Melbourne" || tz == "Australia/Hobart") {
|
||||||
|
outSetting = TZ::TZ_AEST;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (tz == "Pacific/Auckland") {
|
||||||
|
outSetting = TZ::TZ_NZST;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool fetchTimezonePayload(const char* url, std::string& payload) {
|
||||||
|
// Give DNS a moment after WiFi connect.
|
||||||
|
delay(500);
|
||||||
|
for (int attempt = 0; attempt < 2; ++attempt) {
|
||||||
|
if (HttpDownloader::fetchUrl(url, payload)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
delay(300);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool fetchPublicIp(std::string& ip) {
|
||||||
|
std::string payload;
|
||||||
|
if (!fetchTimezonePayload("https://api.ipify.org", payload)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Payload is plain text: IP address
|
||||||
|
ip = payload;
|
||||||
|
// Trim any whitespace
|
||||||
|
while (!ip.empty() && (ip.back() == '\n' || ip.back() == '\r' || ip.back() == ' ' || ip.back() == '\t')) {
|
||||||
|
ip.pop_back();
|
||||||
|
}
|
||||||
|
if (!ip.empty()) {
|
||||||
|
LOG_DBG("CLK", "Public IP: %s", ip.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool detectTimezoneSetting(uint8_t& outSetting, std::string& outIana, bool& outDstKnown, bool& outDstActive) {
|
||||||
|
std::string payload;
|
||||||
|
std::string publicIp;
|
||||||
|
if (fetchPublicIp(publicIp)) {
|
||||||
|
std::string timeUrl = std::string("https://timeapi.io/api/Time/current/ip?ipAddress=") + publicIp;
|
||||||
|
LOG_DBG("CLK", "Timezone detect via TimeAPI: %s", timeUrl.c_str());
|
||||||
|
if (fetchTimezonePayload(timeUrl.c_str(), payload)) {
|
||||||
|
// Continue to parse TimeAPI payload below.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.empty() && !fetchTimezonePayload("https://ip-api.com/json/?fields=timezone,dst", payload)) {
|
||||||
|
LOG_ERR("CLK", "Timezone detect failed: fetch error");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonDocument doc;
|
||||||
|
const auto err = deserializeJson(doc, payload);
|
||||||
|
if (err) {
|
||||||
|
LOG_ERR("CLK", "Timezone detect failed: %s", err.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* tz = doc["timezone"] | "";
|
||||||
|
if (!tz || tz[0] == '\0') {
|
||||||
|
tz = doc["timeZone"] | "";
|
||||||
|
}
|
||||||
|
if (!tz || tz[0] == '\0') {
|
||||||
|
tz = doc["time_zone"] | "";
|
||||||
|
}
|
||||||
|
if (!tz || tz[0] == '\0') {
|
||||||
|
LOG_ERR("CLK", "Timezone detect failed: missing timezone (payload: %s)", payload.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
outIana = tz;
|
||||||
|
if (!doc["dst_active"].isNull()) {
|
||||||
|
outDstKnown = true;
|
||||||
|
outDstActive = doc["dst_active"].as<bool>();
|
||||||
|
} else if (!doc["dstActive"].isNull()) {
|
||||||
|
outDstKnown = true;
|
||||||
|
outDstActive = doc["dstActive"].as<bool>();
|
||||||
|
} else if (!doc["dst"].isNull()) {
|
||||||
|
outDstKnown = true;
|
||||||
|
outDstActive = doc["dst"].as<bool>();
|
||||||
|
} else {
|
||||||
|
outDstKnown = false;
|
||||||
|
outDstActive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mapIanaTimezone(tz, outSetting)) {
|
||||||
|
LOG_ERR("CLK", "Timezone detect unsupported: %s", tz);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outDstKnown) {
|
||||||
|
LOG_DBG("CLK", "Timezone detected: %s (dst=%d)", tz, outDstActive ? 1 : 0);
|
||||||
|
} else {
|
||||||
|
LOG_DBG("CLK", "Timezone detected: %s (dst=unknown)", tz);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void DetectTimezoneActivity::onEnter() {
|
||||||
|
Activity::onEnter();
|
||||||
|
|
||||||
|
if (WiFi.status() == WL_CONNECTED) {
|
||||||
|
onWifiSelectionComplete(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
startActivityForResult(std::make_unique<WifiSelectionActivity>(renderer, mappedInput),
|
||||||
|
[this](const ActivityResult& result) {
|
||||||
|
if (result.isCancelled) {
|
||||||
|
onWifiSelectionCancelled();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onWifiSelectionComplete(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void DetectTimezoneActivity::onExit() {
|
||||||
|
Activity::onExit();
|
||||||
|
HalClock::wifiOff();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DetectTimezoneActivity::onWifiSelectionComplete(bool success) {
|
||||||
|
if (!success) {
|
||||||
|
state = FAILED;
|
||||||
|
requestUpdate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
RenderLock lock(*this);
|
||||||
|
state = DETECTING;
|
||||||
|
}
|
||||||
|
requestUpdateAndWait();
|
||||||
|
|
||||||
|
performDetect();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DetectTimezoneActivity::onWifiSelectionCancelled() { finish(); }
|
||||||
|
|
||||||
|
void DetectTimezoneActivity::performDetect() {
|
||||||
|
uint8_t detected = SETTINGS.timeZone;
|
||||||
|
detectedTimezone.clear();
|
||||||
|
dstKnown = false;
|
||||||
|
dstActive = false;
|
||||||
|
if (detectTimezoneSetting(detected, detectedTimezone, dstKnown, dstActive)) {
|
||||||
|
SETTINGS.timeZone = detected;
|
||||||
|
HalClock::applyTimezone(SETTINGS.timeZone);
|
||||||
|
SETTINGS.saveToFile();
|
||||||
|
state = SUCCESS;
|
||||||
|
} else {
|
||||||
|
state = FAILED;
|
||||||
|
}
|
||||||
|
|
||||||
|
HalClock::wifiOff();
|
||||||
|
requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DetectTimezoneActivity::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_DETECT_TIMEZONE));
|
||||||
|
|
||||||
|
if (state == CONNECTING) {
|
||||||
|
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, tr(STR_CONNECTING), true, EpdFontFamily::BOLD);
|
||||||
|
renderer.displayBuffer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state == DETECTING) {
|
||||||
|
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, tr(STR_DETECTING_TIMEZONE), true, EpdFontFamily::BOLD);
|
||||||
|
renderer.displayBuffer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state == SUCCESS) {
|
||||||
|
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 20, tr(STR_TIMEZONE_DETECTED), true, EpdFontFamily::BOLD);
|
||||||
|
|
||||||
|
if (!detectedTimezone.empty()) {
|
||||||
|
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 5, detectedTimezone.c_str());
|
||||||
|
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 25, dstStatusLabel());
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
|
||||||
|
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
|
renderer.displayBuffer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state == FAILED) {
|
||||||
|
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, tr(STR_TIMEZONE_DETECT_FAILED), true, EpdFontFamily::BOLD);
|
||||||
|
|
||||||
|
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
|
||||||
|
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
|
renderer.displayBuffer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* DetectTimezoneActivity::dstStatusLabel() const {
|
||||||
|
if (!dstKnown) {
|
||||||
|
return tr(STR_DST_UNKNOWN);
|
||||||
|
}
|
||||||
|
return dstActive ? tr(STR_DST_ACTIVE) : tr(STR_DST_INACTIVE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DetectTimezoneActivity::loop() {
|
||||||
|
if (state == SUCCESS || state == FAILED) {
|
||||||
|
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "activities/Activity.h"
|
||||||
|
|
||||||
|
class DetectTimezoneActivity final : public Activity {
|
||||||
|
public:
|
||||||
|
explicit DetectTimezoneActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
|
||||||
|
: Activity("DetectTimezone", renderer, mappedInput) {}
|
||||||
|
|
||||||
|
void onEnter() override;
|
||||||
|
void onExit() override;
|
||||||
|
void loop() override;
|
||||||
|
void render(RenderLock&&) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
enum State { CONNECTING, DETECTING, SUCCESS, FAILED };
|
||||||
|
State state = CONNECTING;
|
||||||
|
std::string detectedTimezone;
|
||||||
|
bool dstKnown = false;
|
||||||
|
bool dstActive = false;
|
||||||
|
|
||||||
|
void onWifiSelectionComplete(bool success);
|
||||||
|
void onWifiSelectionCancelled();
|
||||||
|
void performDetect();
|
||||||
|
const char* dstStatusLabel() const;
|
||||||
|
};
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "KOReaderAuthActivity.h"
|
#include "KOReaderAuthActivity.h"
|
||||||
|
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
|
#include <HalClock.h>
|
||||||
#include <I18n.h>
|
#include <I18n.h>
|
||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
|
|
||||||
@@ -93,11 +94,7 @@ void KOReaderAuthActivity::onEnter() {
|
|||||||
void KOReaderAuthActivity::onExit() {
|
void KOReaderAuthActivity::onExit() {
|
||||||
Activity::onExit();
|
Activity::onExit();
|
||||||
|
|
||||||
// Turn off wifi
|
HalClock::wifiOff();
|
||||||
WiFi.disconnect(false);
|
|
||||||
delay(100);
|
|
||||||
WiFi.mode(WIFI_OFF);
|
|
||||||
delay(100);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void KOReaderAuthActivity::render(RenderLock&&) {
|
void KOReaderAuthActivity::render(RenderLock&&) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "OtaUpdateActivity.h"
|
#include "OtaUpdateActivity.h"
|
||||||
|
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
|
#include <HalClock.h>
|
||||||
#include <I18n.h>
|
#include <I18n.h>
|
||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
|
|
||||||
@@ -66,11 +67,7 @@ void OtaUpdateActivity::onEnter() {
|
|||||||
void OtaUpdateActivity::onExit() {
|
void OtaUpdateActivity::onExit() {
|
||||||
Activity::onExit();
|
Activity::onExit();
|
||||||
|
|
||||||
// Turn off wifi
|
HalClock::wifiOff();
|
||||||
WiFi.disconnect(false); // false = don't erase credentials, send disconnect frame
|
|
||||||
delay(100); // Allow disconnect frame to be sent
|
|
||||||
WiFi.mode(WIFI_OFF);
|
|
||||||
delay(100); // Allow WiFi hardware to fully power down
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void OtaUpdateActivity::render(RenderLock&&) {
|
void OtaUpdateActivity::render(RenderLock&&) {
|
||||||
|
|||||||
@@ -1,18 +1,22 @@
|
|||||||
#include "SettingsActivity.h"
|
#include "SettingsActivity.h"
|
||||||
|
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
|
#include <HalClock.h>
|
||||||
#include <Logging.h>
|
#include <Logging.h>
|
||||||
|
|
||||||
#include "ButtonRemapActivity.h"
|
#include "ButtonRemapActivity.h"
|
||||||
#include "CalibreSettingsActivity.h"
|
#include "CalibreSettingsActivity.h"
|
||||||
#include "ClearCacheActivity.h"
|
#include "ClearCacheActivity.h"
|
||||||
|
#include "ClockSettingsActivity.h"
|
||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
|
#include "DetectTimezoneActivity.h"
|
||||||
#include "KOReaderSettingsActivity.h"
|
#include "KOReaderSettingsActivity.h"
|
||||||
#include "LanguageSelectActivity.h"
|
#include "LanguageSelectActivity.h"
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
#include "OtaUpdateActivity.h"
|
#include "OtaUpdateActivity.h"
|
||||||
#include "SettingsList.h"
|
#include "SettingsList.h"
|
||||||
#include "StatusBarSettingsActivity.h"
|
#include "StatusBarSettingsActivity.h"
|
||||||
|
#include "SyncTimeActivity.h"
|
||||||
#include "SystemInformationActivity.h"
|
#include "SystemInformationActivity.h"
|
||||||
#include "activities/network/WifiSelectionActivity.h"
|
#include "activities/network/WifiSelectionActivity.h"
|
||||||
#include "components/UITheme.h"
|
#include "components/UITheme.h"
|
||||||
@@ -32,6 +36,11 @@ void SettingsActivity::onEnter() {
|
|||||||
|
|
||||||
for (const auto& setting : getSettingsList()) {
|
for (const auto& setting : getSettingsList()) {
|
||||||
if (setting.category == StrId::STR_NONE_OPT) continue;
|
if (setting.category == StrId::STR_NONE_OPT) continue;
|
||||||
|
if (setting.category == StrId::STR_CAT_SYSTEM &&
|
||||||
|
(setting.nameId == StrId::STR_USE_CLOCK || setting.nameId == StrId::STR_CLOCK_FORMAT ||
|
||||||
|
setting.nameId == StrId::STR_TIMEZONE)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (setting.category == StrId::STR_CAT_DISPLAY) {
|
if (setting.category == StrId::STR_CAT_DISPLAY) {
|
||||||
displaySettings.push_back(setting);
|
displaySettings.push_back(setting);
|
||||||
} else if (setting.category == StrId::STR_CAT_READER) {
|
} else if (setting.category == StrId::STR_CAT_READER) {
|
||||||
@@ -47,6 +56,7 @@ void SettingsActivity::onEnter() {
|
|||||||
// Append device-only ACTION items
|
// Append device-only ACTION items
|
||||||
controlsSettings.insert(controlsSettings.begin(),
|
controlsSettings.insert(controlsSettings.begin(),
|
||||||
SettingInfo::Action(StrId::STR_REMAP_FRONT_BUTTONS, SettingAction::RemapFrontButtons));
|
SettingInfo::Action(StrId::STR_REMAP_FRONT_BUTTONS, SettingAction::RemapFrontButtons));
|
||||||
|
systemSettings.push_back(SettingInfo::Action(StrId::STR_CLOCK_SETTINGS, SettingAction::ClockSettings));
|
||||||
systemSettings.push_back(SettingInfo::Action(StrId::STR_WIFI_NETWORKS, SettingAction::Network));
|
systemSettings.push_back(SettingInfo::Action(StrId::STR_WIFI_NETWORKS, SettingAction::Network));
|
||||||
systemSettings.push_back(SettingInfo::Action(StrId::STR_KOREADER_SYNC, SettingAction::KOReaderSync));
|
systemSettings.push_back(SettingInfo::Action(StrId::STR_KOREADER_SYNC, SettingAction::KOReaderSync));
|
||||||
systemSettings.push_back(SettingInfo::Action(StrId::STR_OPDS_BROWSER, SettingAction::OPDSBrowser));
|
systemSettings.push_back(SettingInfo::Action(StrId::STR_OPDS_BROWSER, SettingAction::OPDSBrowser));
|
||||||
@@ -176,6 +186,9 @@ void SettingsActivity::toggleCurrentSetting() {
|
|||||||
case SettingAction::CustomiseStatusBar:
|
case SettingAction::CustomiseStatusBar:
|
||||||
startActivityForResult(std::make_unique<StatusBarSettingsActivity>(renderer, mappedInput), resultHandler);
|
startActivityForResult(std::make_unique<StatusBarSettingsActivity>(renderer, mappedInput), resultHandler);
|
||||||
break;
|
break;
|
||||||
|
case SettingAction::ClockSettings:
|
||||||
|
startActivityForResult(std::make_unique<ClockSettingsActivity>(renderer, mappedInput), resultHandler);
|
||||||
|
break;
|
||||||
case SettingAction::KOReaderSync:
|
case SettingAction::KOReaderSync:
|
||||||
startActivityForResult(std::make_unique<KOReaderSettingsActivity>(renderer, mappedInput), resultHandler);
|
startActivityForResult(std::make_unique<KOReaderSettingsActivity>(renderer, mappedInput), resultHandler);
|
||||||
break;
|
break;
|
||||||
@@ -197,6 +210,12 @@ void SettingsActivity::toggleCurrentSetting() {
|
|||||||
case SettingAction::SystemInfo:
|
case SettingAction::SystemInfo:
|
||||||
startActivityForResult(std::make_unique<SystemInformationActivity>(renderer, mappedInput), resultHandler);
|
startActivityForResult(std::make_unique<SystemInformationActivity>(renderer, mappedInput), resultHandler);
|
||||||
break;
|
break;
|
||||||
|
case SettingAction::SyncTime:
|
||||||
|
startActivityForResult(std::make_unique<SyncTimeActivity>(renderer, mappedInput), resultHandler);
|
||||||
|
break;
|
||||||
|
case SettingAction::DetectTimezone:
|
||||||
|
startActivityForResult(std::make_unique<DetectTimezoneActivity>(renderer, mappedInput), resultHandler);
|
||||||
|
break;
|
||||||
case SettingAction::None:
|
case SettingAction::None:
|
||||||
// Do nothing
|
// Do nothing
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ enum class SettingAction {
|
|||||||
None,
|
None,
|
||||||
RemapFrontButtons,
|
RemapFrontButtons,
|
||||||
CustomiseStatusBar,
|
CustomiseStatusBar,
|
||||||
|
ClockSettings,
|
||||||
KOReaderSync,
|
KOReaderSync,
|
||||||
OPDSBrowser,
|
OPDSBrowser,
|
||||||
Network,
|
Network,
|
||||||
@@ -22,6 +23,8 @@ enum class SettingAction {
|
|||||||
CheckForUpdates,
|
CheckForUpdates,
|
||||||
Language,
|
Language,
|
||||||
SystemInfo,
|
SystemInfo,
|
||||||
|
DetectTimezone,
|
||||||
|
SyncTime,
|
||||||
};
|
};
|
||||||
|
|
||||||
struct SettingInfo {
|
struct SettingInfo {
|
||||||
|
|||||||
@@ -11,13 +11,14 @@
|
|||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr int MENU_ITEMS = 6;
|
constexpr int MENU_ITEMS = 7;
|
||||||
const StrId menuNames[MENU_ITEMS] = {StrId::STR_CHAPTER_PAGE_COUNT,
|
const StrId menuNames[MENU_ITEMS] = {StrId::STR_CHAPTER_PAGE_COUNT,
|
||||||
StrId::STR_BOOK_PROGRESS_PERCENTAGE,
|
StrId::STR_BOOK_PROGRESS_PERCENTAGE,
|
||||||
StrId::STR_PROGRESS_BAR,
|
StrId::STR_PROGRESS_BAR,
|
||||||
StrId::STR_PROGRESS_BAR_THICKNESS,
|
StrId::STR_PROGRESS_BAR_THICKNESS,
|
||||||
StrId::STR_TITLE,
|
StrId::STR_TITLE,
|
||||||
StrId::STR_BATTERY};
|
StrId::STR_BATTERY,
|
||||||
|
StrId::STR_CLOCK};
|
||||||
constexpr int PROGRESS_BAR_ITEMS = 3;
|
constexpr int PROGRESS_BAR_ITEMS = 3;
|
||||||
const StrId progressBarNames[PROGRESS_BAR_ITEMS] = {StrId::STR_BOOK, StrId::STR_CHAPTER, StrId::STR_HIDE};
|
const StrId progressBarNames[PROGRESS_BAR_ITEMS] = {StrId::STR_BOOK, StrId::STR_CHAPTER, StrId::STR_HIDE};
|
||||||
|
|
||||||
@@ -36,7 +37,10 @@ const int verticalPreviewTextPadding = 40;
|
|||||||
void StatusBarSettingsActivity::onEnter() {
|
void StatusBarSettingsActivity::onEnter() {
|
||||||
Activity::onEnter();
|
Activity::onEnter();
|
||||||
|
|
||||||
selectedIndex = 0;
|
const int menuCount = SETTINGS.useClock ? MENU_ITEMS : MENU_ITEMS - 1;
|
||||||
|
if (selectedIndex >= menuCount) {
|
||||||
|
selectedIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Clamp statusBarProgressBar and statusBarTitle in case of corrupt/migrated data
|
// Clamp statusBarProgressBar and statusBarTitle in case of corrupt/migrated data
|
||||||
if (SETTINGS.statusBarProgressBar >= PROGRESS_BAR_ITEMS) {
|
if (SETTINGS.statusBarProgressBar >= PROGRESS_BAR_ITEMS) {
|
||||||
@@ -70,22 +74,26 @@ void StatusBarSettingsActivity::loop() {
|
|||||||
|
|
||||||
// Handle navigation
|
// Handle navigation
|
||||||
buttonNavigator.onNextRelease([this] {
|
buttonNavigator.onNextRelease([this] {
|
||||||
selectedIndex = ButtonNavigator::nextIndex(selectedIndex, MENU_ITEMS);
|
const int menuCount = SETTINGS.useClock ? MENU_ITEMS : MENU_ITEMS - 1;
|
||||||
|
selectedIndex = ButtonNavigator::nextIndex(selectedIndex, menuCount);
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
});
|
});
|
||||||
|
|
||||||
buttonNavigator.onPreviousRelease([this] {
|
buttonNavigator.onPreviousRelease([this] {
|
||||||
selectedIndex = ButtonNavigator::previousIndex(selectedIndex, MENU_ITEMS);
|
const int menuCount = SETTINGS.useClock ? MENU_ITEMS : MENU_ITEMS - 1;
|
||||||
|
selectedIndex = ButtonNavigator::previousIndex(selectedIndex, menuCount);
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
});
|
});
|
||||||
|
|
||||||
buttonNavigator.onNextContinuous([this] {
|
buttonNavigator.onNextContinuous([this] {
|
||||||
selectedIndex = ButtonNavigator::nextIndex(selectedIndex, MENU_ITEMS);
|
const int menuCount = SETTINGS.useClock ? MENU_ITEMS : MENU_ITEMS - 1;
|
||||||
|
selectedIndex = ButtonNavigator::nextIndex(selectedIndex, menuCount);
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
});
|
});
|
||||||
|
|
||||||
buttonNavigator.onPreviousContinuous([this] {
|
buttonNavigator.onPreviousContinuous([this] {
|
||||||
selectedIndex = ButtonNavigator::previousIndex(selectedIndex, MENU_ITEMS);
|
const int menuCount = SETTINGS.useClock ? MENU_ITEMS : MENU_ITEMS - 1;
|
||||||
|
selectedIndex = ButtonNavigator::previousIndex(selectedIndex, menuCount);
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -110,6 +118,9 @@ void StatusBarSettingsActivity::handleSelection() {
|
|||||||
} else if (selectedIndex == 5) {
|
} else if (selectedIndex == 5) {
|
||||||
// Show Battery
|
// Show Battery
|
||||||
SETTINGS.statusBarBattery = (SETTINGS.statusBarBattery + 1) % 2;
|
SETTINGS.statusBarBattery = (SETTINGS.statusBarBattery + 1) % 2;
|
||||||
|
} else if (selectedIndex == 6 && SETTINGS.useClock) {
|
||||||
|
// Show Clock
|
||||||
|
SETTINGS.statusBarClock = (SETTINGS.statusBarClock + 1) % 2;
|
||||||
}
|
}
|
||||||
SETTINGS.saveToFile();
|
SETTINGS.saveToFile();
|
||||||
}
|
}
|
||||||
@@ -124,9 +135,9 @@ void StatusBarSettingsActivity::render(RenderLock&&) {
|
|||||||
tr(STR_CUSTOMISE_STATUS_BAR));
|
tr(STR_CUSTOMISE_STATUS_BAR));
|
||||||
|
|
||||||
const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing;
|
const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing;
|
||||||
const int contentHeight = contentRect.height - contentTop - metrics.verticalSpacing * 2;
|
const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing * 2;
|
||||||
GUI.drawList(
|
GUI.drawList(
|
||||||
renderer, Rect{contentRect.x, contentTop, contentRect.width, contentHeight}, static_cast<int>(MENU_ITEMS),
|
renderer, Rect{0, contentTop, pageWidth, contentHeight}, static_cast<int>(MENU_ITEMS),
|
||||||
static_cast<int>(selectedIndex), [](int index) { return std::string(I18N.get(menuNames[index])); }, nullptr,
|
static_cast<int>(selectedIndex), [](int index) { return std::string(I18N.get(menuNames[index])); }, nullptr,
|
||||||
nullptr,
|
nullptr,
|
||||||
[this](int index) {
|
[this](int index) {
|
||||||
@@ -143,6 +154,8 @@ void StatusBarSettingsActivity::render(RenderLock&&) {
|
|||||||
return I18N.get(titleNames[SETTINGS.statusBarTitle]);
|
return I18N.get(titleNames[SETTINGS.statusBarTitle]);
|
||||||
} else if (index == 5) {
|
} else if (index == 5) {
|
||||||
return SETTINGS.statusBarBattery ? tr(STR_SHOW) : tr(STR_HIDE);
|
return SETTINGS.statusBarBattery ? tr(STR_SHOW) : tr(STR_HIDE);
|
||||||
|
} else if (index == 6) {
|
||||||
|
return SETTINGS.statusBarClock ? tr(STR_SHOW) : tr(STR_HIDE);
|
||||||
} else {
|
} else {
|
||||||
return tr(STR_HIDE);
|
return tr(STR_HIDE);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,197 @@
|
|||||||
|
#include "SyncTimeActivity.h"
|
||||||
|
|
||||||
|
#include <GfxRenderer.h>
|
||||||
|
#include <HalClock.h>
|
||||||
|
#include <I18n.h>
|
||||||
|
#include <Logging.h>
|
||||||
|
#include <WiFi.h>
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
#include "CrossPointSettings.h"
|
||||||
|
#include "MappedInputManager.h"
|
||||||
|
#include "activities/network/WifiSelectionActivity.h"
|
||||||
|
#include "components/UITheme.h"
|
||||||
|
#include "fontIds.h"
|
||||||
|
|
||||||
|
static void formatDuration(char* buf, size_t bufSize, int32_t totalSeconds) {
|
||||||
|
const char* sign = totalSeconds < 0 ? "-" : "+";
|
||||||
|
int32_t abs = totalSeconds < 0 ? -totalSeconds : totalSeconds;
|
||||||
|
|
||||||
|
int32_t days = abs / 86400;
|
||||||
|
int32_t hours = (abs % 86400) / 3600;
|
||||||
|
int32_t mins = (abs % 3600) / 60;
|
||||||
|
int32_t secs = abs % 60;
|
||||||
|
|
||||||
|
if (days > 0) {
|
||||||
|
snprintf(buf, bufSize, "%s%ldd %ldh %ldm", sign, (long)days, (long)hours, (long)mins);
|
||||||
|
} else if (hours > 0) {
|
||||||
|
snprintf(buf, bufSize, "%s%ldh %ldm %lds", sign, (long)hours, (long)mins, (long)secs);
|
||||||
|
} else if (mins > 0) {
|
||||||
|
snprintf(buf, bufSize, "%s%ldm %lds", sign, (long)mins, (long)secs);
|
||||||
|
} else {
|
||||||
|
snprintf(buf, bufSize, "%s%lds", sign, (long)secs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void formatElapsed(char* buf, size_t bufSize, int32_t totalSeconds) {
|
||||||
|
int32_t days = totalSeconds / 86400;
|
||||||
|
int32_t hours = (totalSeconds % 86400) / 3600;
|
||||||
|
int32_t mins = (totalSeconds % 3600) / 60;
|
||||||
|
|
||||||
|
if (days > 0) {
|
||||||
|
snprintf(buf, bufSize, "%ldd %ldh ago", (long)days, (long)hours);
|
||||||
|
} else if (hours > 0) {
|
||||||
|
snprintf(buf, bufSize, "%ldh %ldm ago", (long)hours, (long)mins);
|
||||||
|
} else {
|
||||||
|
snprintf(buf, bufSize, "%ldm ago", (long)mins);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SyncTimeActivity::onEnter() {
|
||||||
|
Activity::onEnter();
|
||||||
|
|
||||||
|
if (WiFi.status() == WL_CONNECTED) {
|
||||||
|
onWifiSelectionComplete(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
startActivityForResult(std::make_unique<WifiSelectionActivity>(renderer, mappedInput),
|
||||||
|
[this](const ActivityResult& result) {
|
||||||
|
if (result.isCancelled) {
|
||||||
|
onWifiSelectionCancelled();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onWifiSelectionComplete(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void SyncTimeActivity::onExit() {
|
||||||
|
Activity::onExit();
|
||||||
|
HalClock::wifiOff(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SyncTimeActivity::onWifiSelectionComplete(bool success) {
|
||||||
|
if (!success) {
|
||||||
|
state = FAILED;
|
||||||
|
requestUpdate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
RenderLock lock(*this);
|
||||||
|
state = SYNCING;
|
||||||
|
}
|
||||||
|
requestUpdateAndWait();
|
||||||
|
|
||||||
|
performSync();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SyncTimeActivity::onWifiSelectionCancelled() { finish(); }
|
||||||
|
|
||||||
|
void SyncTimeActivity::performSync() {
|
||||||
|
hadTimeBeforeSync = HalClock::isSynced();
|
||||||
|
preSyncTime = hadTimeBeforeSync ? time(nullptr) : 0;
|
||||||
|
prevSyncTime = HalClock::lastSyncTime();
|
||||||
|
|
||||||
|
bool ok = HalClock::syncNtp();
|
||||||
|
|
||||||
|
if (ok && hadTimeBeforeSync) {
|
||||||
|
driftSeconds = (int32_t)(time(nullptr) - preSyncTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
HalClock::wifiOff(true);
|
||||||
|
|
||||||
|
state = ok ? SUCCESS : FAILED;
|
||||||
|
requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SyncTimeActivity::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_SYNC_TIME));
|
||||||
|
|
||||||
|
if (state == SYNCING) {
|
||||||
|
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, tr(STR_SYNCING_CLOCK), true, EpdFontFamily::BOLD);
|
||||||
|
renderer.displayBuffer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state == SUCCESS) {
|
||||||
|
int y = pageHeight / 2 - 40;
|
||||||
|
renderer.drawCenteredText(UI_10_FONT_ID, y, tr(STR_TIME_SYNCED), true, EpdFontFamily::BOLD);
|
||||||
|
|
||||||
|
time_t now = HalClock::now();
|
||||||
|
struct tm timeinfo;
|
||||||
|
localtime_r(&now, &timeinfo);
|
||||||
|
char timePart[16];
|
||||||
|
HalClock::formatTime(timePart, sizeof(timePart), !SETTINGS.clockFormat12h);
|
||||||
|
char timeStr[32];
|
||||||
|
snprintf(timeStr, sizeof(timeStr), "%s %04d-%02d-%02d", timePart, timeinfo.tm_year + 1900, timeinfo.tm_mon + 1,
|
||||||
|
timeinfo.tm_mday);
|
||||||
|
y += 30;
|
||||||
|
renderer.drawCenteredText(UI_10_FONT_ID, y, timeStr);
|
||||||
|
|
||||||
|
int32_t elapsedSinceSync = -1;
|
||||||
|
if (prevSyncTime > 0) {
|
||||||
|
elapsedSinceSync = (int32_t)(now - prevSyncTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
char driftStr[80];
|
||||||
|
if (hadTimeBeforeSync) {
|
||||||
|
char driftFmt[24];
|
||||||
|
formatDuration(driftFmt, sizeof(driftFmt), driftSeconds);
|
||||||
|
|
||||||
|
if (elapsedSinceSync > 0) {
|
||||||
|
double hours = (double)elapsedSinceSync / 3600.0;
|
||||||
|
double rate = (double)driftSeconds / hours;
|
||||||
|
char rateFmt[16];
|
||||||
|
snprintf(rateFmt, sizeof(rateFmt), "%+.2f", rate);
|
||||||
|
|
||||||
|
char driftWithRate[48];
|
||||||
|
snprintf(driftWithRate, sizeof(driftWithRate), "%s (%s s/hr)", driftFmt, rateFmt);
|
||||||
|
snprintf(driftStr, sizeof(driftStr), tr(STR_CLOCK_DRIFT), driftWithRate);
|
||||||
|
} else {
|
||||||
|
snprintf(driftStr, sizeof(driftStr), tr(STR_CLOCK_DRIFT), driftFmt);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
snprintf(driftStr, sizeof(driftStr), tr(STR_CLOCK_DRIFT), "N/A");
|
||||||
|
}
|
||||||
|
y += 30;
|
||||||
|
renderer.drawCenteredText(UI_10_FONT_ID, y, driftStr);
|
||||||
|
|
||||||
|
if (elapsedSinceSync > 0) {
|
||||||
|
char elapsedFmt[24];
|
||||||
|
formatElapsed(elapsedFmt, sizeof(elapsedFmt), elapsedSinceSync);
|
||||||
|
char lastSyncStr[48];
|
||||||
|
snprintf(lastSyncStr, sizeof(lastSyncStr), tr(STR_LAST_NTP_SYNC), elapsedFmt);
|
||||||
|
y += 25;
|
||||||
|
renderer.drawCenteredText(UI_10_FONT_ID, y, lastSyncStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
|
||||||
|
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
|
renderer.displayBuffer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state == FAILED) {
|
||||||
|
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, tr(STR_TIME_SYNC_FAILED), true, EpdFontFamily::BOLD);
|
||||||
|
|
||||||
|
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
|
||||||
|
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
|
renderer.displayBuffer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SyncTimeActivity::loop() {
|
||||||
|
if (state == SUCCESS || state == FAILED) {
|
||||||
|
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "activities/Activity.h"
|
||||||
|
|
||||||
|
class SyncTimeActivity final : public Activity {
|
||||||
|
public:
|
||||||
|
explicit SyncTimeActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
|
||||||
|
: Activity("SyncTime", renderer, mappedInput) {}
|
||||||
|
|
||||||
|
void onEnter() override;
|
||||||
|
void onExit() override;
|
||||||
|
void loop() override;
|
||||||
|
void render(RenderLock&&) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
enum State { CONNECTING, SYNCING, SUCCESS, FAILED };
|
||||||
|
State state = CONNECTING;
|
||||||
|
time_t preSyncTime = 0;
|
||||||
|
time_t prevSyncTime = 0;
|
||||||
|
int32_t driftSeconds = 0;
|
||||||
|
bool hadTimeBeforeSync = false;
|
||||||
|
void onWifiSelectionComplete(bool success);
|
||||||
|
void onWifiSelectionCancelled();
|
||||||
|
void performSync();
|
||||||
|
};
|
||||||
@@ -127,7 +127,7 @@ int UITheme::getStatusBarHeight() {
|
|||||||
// Add status bar margin
|
// Add status bar margin
|
||||||
const bool showStatusBar = SETTINGS.statusBarChapterPageCount || SETTINGS.statusBarBookProgressPercentage ||
|
const bool showStatusBar = SETTINGS.statusBarChapterPageCount || SETTINGS.statusBarBookProgressPercentage ||
|
||||||
SETTINGS.statusBarTitle != CrossPointSettings::STATUS_BAR_TITLE::HIDE_TITLE ||
|
SETTINGS.statusBarTitle != CrossPointSettings::STATUS_BAR_TITLE::HIDE_TITLE ||
|
||||||
SETTINGS.statusBarBattery;
|
SETTINGS.statusBarBattery || (SETTINGS.useClock && SETTINGS.statusBarClock);
|
||||||
const bool showProgressBar =
|
const bool showProgressBar =
|
||||||
SETTINGS.statusBarProgressBar != CrossPointSettings::STATUS_BAR_PROGRESS_BAR::HIDE_PROGRESS;
|
SETTINGS.statusBarProgressBar != CrossPointSettings::STATUS_BAR_PROGRESS_BAR::HIDE_PROGRESS;
|
||||||
return (showStatusBar ? (metrics.statusBarVerticalMargin) : 0) +
|
return (showStatusBar ? (metrics.statusBarVerticalMargin) : 0) +
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "BaseTheme.h"
|
#include "BaseTheme.h"
|
||||||
|
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
|
#include <HalClock.h>
|
||||||
#include <HalPowerManager.h>
|
#include <HalPowerManager.h>
|
||||||
#include <HalStorage.h>
|
#include <HalStorage.h>
|
||||||
#include <Logging.h>
|
#include <Logging.h>
|
||||||
@@ -306,6 +307,13 @@ void BaseTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* t
|
|||||||
Rect{batteryX, rect.y + 5, BaseMetrics::values.batteryWidth, BaseMetrics::values.batteryHeight},
|
Rect{batteryX, rect.y + 5, BaseMetrics::values.batteryWidth, BaseMetrics::values.batteryHeight},
|
||||||
showBatteryPercentage);
|
showBatteryPercentage);
|
||||||
|
|
||||||
|
// Draw clock in header
|
||||||
|
if (SETTINGS.useClock) {
|
||||||
|
char clockStr[16];
|
||||||
|
HalClock::formatTime(clockStr, sizeof(clockStr), !SETTINGS.clockFormat12h);
|
||||||
|
renderer.drawText(SMALL_FONT_ID, rect.x + BaseMetrics::values.contentSidePadding, rect.y + 5, clockStr);
|
||||||
|
}
|
||||||
|
|
||||||
if (title) {
|
if (title) {
|
||||||
int padding = rect.width - batteryX + BaseMetrics::values.batteryWidth;
|
int padding = rect.width - batteryX + BaseMetrics::values.batteryWidth;
|
||||||
auto truncatedTitle = renderer.truncatedText(UI_12_FONT_ID, title,
|
auto truncatedTitle = renderer.truncatedText(UI_12_FONT_ID, title,
|
||||||
@@ -745,6 +753,17 @@ void BaseTheme::drawStatusBar(GfxRenderer& renderer, const float bookProgress, c
|
|||||||
showBatteryPercentage);
|
showBatteryPercentage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Draw Clock
|
||||||
|
int clockTextWidth = 0;
|
||||||
|
if (SETTINGS.useClock && SETTINGS.statusBarClock) {
|
||||||
|
char clockStr[16];
|
||||||
|
HalClock::formatTime(clockStr, sizeof(clockStr), !SETTINGS.clockFormat12h);
|
||||||
|
clockTextWidth = renderer.getTextWidth(SMALL_FONT_ID, clockStr);
|
||||||
|
const int batterySize = SETTINGS.statusBarBattery ? (showBatteryPercentage ? 50 : 20) : 0;
|
||||||
|
renderer.drawText(SMALL_FONT_ID, metrics.statusBarHorizontalMargin + orientedMarginLeft + batterySize + 8, textY,
|
||||||
|
clockStr);
|
||||||
|
}
|
||||||
|
|
||||||
// Draw Title
|
// Draw Title
|
||||||
if (!title.empty()) {
|
if (!title.empty()) {
|
||||||
textY -= textYOffset;
|
textY -= textYOffset;
|
||||||
@@ -754,7 +773,8 @@ void BaseTheme::drawStatusBar(GfxRenderer& renderer, const float bookProgress, c
|
|||||||
renderer.getScreenWidth() - (metrics.statusBarHorizontalMargin * 2) - orientedMarginLeft - orientedMarginRight;
|
renderer.getScreenWidth() - (metrics.statusBarHorizontalMargin * 2) - orientedMarginLeft - orientedMarginRight;
|
||||||
|
|
||||||
const int batterySize = SETTINGS.statusBarBattery ? (showBatteryPercentage ? 50 : 20) : 0;
|
const int batterySize = SETTINGS.statusBarBattery ? (showBatteryPercentage ? 50 : 20) : 0;
|
||||||
const int titleMarginLeft = batterySize + 30;
|
const int clockSize = clockTextWidth > 0 ? clockTextWidth + 8 : 0;
|
||||||
|
const int titleMarginLeft = batterySize + clockSize + 30;
|
||||||
const int titleMarginRight = progressTextWidth + 30;
|
const int titleMarginRight = progressTextWidth + 30;
|
||||||
|
|
||||||
// Attempt to center title on the screen, but if title is too wide then later we will center it within the
|
// Attempt to center title on the screen, but if title is too wide then later we will center it within the
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "LyraTheme.h"
|
#include "LyraTheme.h"
|
||||||
|
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
|
#include <HalClock.h>
|
||||||
#include <HalGPIO.h>
|
#include <HalGPIO.h>
|
||||||
#include <HalPowerManager.h>
|
#include <HalPowerManager.h>
|
||||||
#include <HalStorage.h>
|
#include <HalStorage.h>
|
||||||
@@ -150,6 +151,13 @@ void LyraTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* t
|
|||||||
Rect{batteryX, rect.y + 5, LyraMetrics::values.batteryWidth, LyraMetrics::values.batteryHeight},
|
Rect{batteryX, rect.y + 5, LyraMetrics::values.batteryWidth, LyraMetrics::values.batteryHeight},
|
||||||
showBatteryPercentage);
|
showBatteryPercentage);
|
||||||
|
|
||||||
|
// Draw clock in header
|
||||||
|
if (SETTINGS.useClock) {
|
||||||
|
char clockStr[16];
|
||||||
|
HalClock::formatTime(clockStr, sizeof(clockStr), !SETTINGS.clockFormat12h);
|
||||||
|
renderer.drawText(SMALL_FONT_ID, rect.x + LyraMetrics::values.contentSidePadding, rect.y + 5, clockStr);
|
||||||
|
}
|
||||||
|
|
||||||
int maxTitleWidth =
|
int maxTitleWidth =
|
||||||
rect.width - LyraMetrics::values.contentSidePadding * 2 - (subtitle != nullptr ? maxSubtitleWidth : 0);
|
rect.width - LyraMetrics::values.contentSidePadding * 2 - (subtitle != nullptr ? maxSubtitleWidth : 0);
|
||||||
|
|
||||||
|
|||||||
+6
-1
@@ -3,6 +3,7 @@
|
|||||||
#include <FontCacheManager.h>
|
#include <FontCacheManager.h>
|
||||||
#include <FontDecompressor.h>
|
#include <FontDecompressor.h>
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
|
#include <HalClock.h>
|
||||||
#include <HalDisplay.h>
|
#include <HalDisplay.h>
|
||||||
#include <HalGPIO.h>
|
#include <HalGPIO.h>
|
||||||
#include <HalPowerManager.h>
|
#include <HalPowerManager.h>
|
||||||
@@ -184,6 +185,7 @@ void waitForPowerRelease() {
|
|||||||
void enterDeepSleep() {
|
void enterDeepSleep() {
|
||||||
HalPowerManager::Lock powerLock; // Ensure we are at normal CPU frequency for sleep preparation
|
HalPowerManager::Lock powerLock; // Ensure we are at normal CPU frequency for sleep preparation
|
||||||
APP_STATE.lastSleepFromReader = activityManager.isReaderActivity();
|
APP_STATE.lastSleepFromReader = activityManager.isReaderActivity();
|
||||||
|
HalClock::saveBeforeSleep(SETTINGS.useClock);
|
||||||
APP_STATE.saveToFile();
|
APP_STATE.saveToFile();
|
||||||
|
|
||||||
activityManager.goToSleep();
|
activityManager.goToSleep();
|
||||||
@@ -192,7 +194,7 @@ void enterDeepSleep() {
|
|||||||
LOG_DBG("MAIN", "Power button press calibration value: %lu ms", t2 - t1);
|
LOG_DBG("MAIN", "Power button press calibration value: %lu ms", t2 - t1);
|
||||||
LOG_DBG("MAIN", "Entering deep sleep");
|
LOG_DBG("MAIN", "Entering deep sleep");
|
||||||
|
|
||||||
powerManager.startDeepSleep(gpio);
|
powerManager.startDeepSleep(gpio, SETTINGS.useClock);
|
||||||
}
|
}
|
||||||
|
|
||||||
void setupDisplayAndFonts() {
|
void setupDisplayAndFonts() {
|
||||||
@@ -258,6 +260,7 @@ void setup() {
|
|||||||
HalSystem::clearPanic(); // TODO: move this to an activity when we have one to display the panic info
|
HalSystem::clearPanic(); // TODO: move this to an activity when we have one to display the panic info
|
||||||
|
|
||||||
SETTINGS.loadFromFile();
|
SETTINGS.loadFromFile();
|
||||||
|
HalClock::applyTimezone(SETTINGS.timeZone);
|
||||||
I18N.loadSettings();
|
I18N.loadSettings();
|
||||||
KOREADER_STORE.loadFromFile();
|
KOREADER_STORE.loadFromFile();
|
||||||
UITheme::getInstance().reload();
|
UITheme::getInstance().reload();
|
||||||
@@ -289,6 +292,7 @@ void setup() {
|
|||||||
activityManager.goToBoot();
|
activityManager.goToBoot();
|
||||||
|
|
||||||
APP_STATE.loadFromFile();
|
APP_STATE.loadFromFile();
|
||||||
|
HalClock::restore();
|
||||||
RECENT_BOOKS.loadFromFile();
|
RECENT_BOOKS.loadFromFile();
|
||||||
|
|
||||||
// Boot to home screen if no book is open, last sleep was not from reader, back button is held, or reader activity
|
// Boot to home screen if no book is open, last sleep was not from reader, back button is held, or reader activity
|
||||||
@@ -315,6 +319,7 @@ void loop() {
|
|||||||
static unsigned long lastMemPrint = 0;
|
static unsigned long lastMemPrint = 0;
|
||||||
|
|
||||||
gpio.update();
|
gpio.update();
|
||||||
|
HalClock::updatePeriodic();
|
||||||
|
|
||||||
renderer.setFadingFix(SETTINGS.fadingFix);
|
renderer.setFadingFix(SETTINGS.fadingFix);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user