// Indoor/outdoor temperature and clock display for the // WaveShare ESP32-S3-RLCD-4.2. // // Startup order matters: the panel, RTC and onboard sensor all come up before // Wi-Fi is touched, so a working clock and indoor reading are on screen within // a second or two even if the network never associates. #include #include #include "ambient.h" #include "audio.h" #include "battery.h" #include "config.h" #include "display.h" #include "network.h" #include "outdoor.h" #include "portal.h" #include "power.h" #include "rtc.h" #include "settings.h" #include "temperature.h" namespace { bool g_indoorValid = false; float g_indoorC = 0.0f; uint32_t g_lastIndoorMs = 0; uint32_t g_lastDisplayTickMs = 0; bool g_setupMode = false; void readIndoor() { float c = 0.0f; if (temperature::read(c)) { g_indoorC = c; g_indoorValid = true; } else { // One bad read is usually noise on the bus; only give up after a while. static uint8_t failures = 0; if (++failures >= 3) { g_indoorValid = false; failures = 0; } } g_lastIndoorMs = millis(); } // True if KEY is held down for KEY_FORCE_SETUP_MS at power-up. This is the // only way back into the setup portal once Wi-Fi has been stored. bool keyHeldAtBoot() { pinMode(KEY_BUTTON_PIN, INPUT_PULLUP); if (digitalRead(KEY_BUTTON_PIN) != LOW) { return false; } Serial.println("[boot] KEY down, hold to enter setup"); const uint32_t deadline = millis() + KEY_FORCE_SETUP_MS; while (millis() < deadline) { if (digitalRead(KEY_BUTTON_PIN) != LOW) { return false; // released early: treat as a normal boot } delay(20); } return true; } display::Model buildModel() { display::Model m; // localtime() reads the system clock, which is seeded from the RTC at boot // and corrected by NTP when available. const time_t t = time(nullptr); struct tm local; localtime_r(&t, &local); const settings::Config &cfg = settings::get(); m.timeValid = rtc::hasValidTime() || network::hasSyncedTime(); if (m.timeValid) { m.hour = (uint8_t)local.tm_hour; m.minute = (uint8_t)local.tm_min; m.twelveHour = !cfg.clock24h; display::formatDateLine(local, cfg.dateFormat, cfg.dateUppercase, m.dateLine, sizeof(m.dateLine)); } m.indoorValid = g_indoorValid; m.indoorC = g_indoorC; m.outdoorValid = outdoor::hasReading(); m.outdoorC = outdoor::lastCelsius(); m.outdoorStale = outdoor::isStale(); m.batteryValid = cfg.showBattery && battery::hasReading(); m.batteryPercent = battery::percent(); return m; } void enterSetupMode() { g_setupMode = true; portal::startAccessPoint(); char url[32]; snprintf(url, sizeof(url), "http://%s", portal::apIp().toString().c_str()); display::showSetupScreen(portal::apSsid(), portal::apPassword(), url); } } // namespace void setup() { // Before Serial, so the UART dividers are set up for the final clock. power::begin(); Serial.begin(115200); delay(200); Serial.println("\n[boot] ESP32-S3-RLCD-4.2 temperature display"); const bool forceSetup = keyHeldAtBoot(); if (!display::begin()) { Serial.println("[boot] display init failed"); } display::showBootMessage("Starting..."); settings::begin(); power::applyPanelMode(); rtc::begin(); rtc::syncSystemClockFromRtc(); temperature::begin(); readIndoor(); battery::begin(); ambient::begin(); outdoor::begin(); // Ahead of the setup-mode branch, because the portal serves /audio in both // modes. Probes the codec and mounts the card; it does not power up the // microphones or install the I2S driver, so it costs little before the first // draw. audio::begin(); // Without stored credentials there is nothing to connect to and no way for // the user to supply them except the portal, so it takes over the screen. if (forceSetup || !settings::hasWifi()) { Serial.println(forceSetup ? "[boot] setup forced by KEY" : "[boot] no Wi-Fi stored, entering setup"); enterSetupMode(); return; } // Draw everything we already know before the network attempt, so the screen // is useful immediately. display::update(buildModel()); network::begin(); } void loop() { portal::loop(); // Drains the encoder to a listening browser and tears a finished recording // down. Bounded work, so the clock below still runs on time. audio::loop(); if (g_setupMode) { // The setup screen is static and the clock is almost certainly wrong; // serving the portal promptly is all that matters here. delay(5); return; } // Nothing on a reflective panel is readable in the dark, so this is where // most of the battery is saved. Returns only once the room is lit again. if (power::sleepWhileDark()) { readIndoor(); battery::sample(); } network::loop(); outdoor::loop(); battery::loop(); const uint32_t nowMs = millis(); if (nowMs - g_lastIndoorMs >= INDOOR_READ_MS) { readIndoor(); } // The panel itself is only refreshed when a displayed value actually // changes; this tick just decides how promptly that is noticed. if (nowMs - g_lastDisplayTickMs >= DISPLAY_TICK_MS) { g_lastDisplayTickMs = nowMs; display::update(buildModel()); } delay(10); }