| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206 |
- // 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 <Arduino.h>
- #include <time.h>
- #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);
- }
|