main.cpp 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. // Indoor/outdoor temperature and clock display for the
  2. // WaveShare ESP32-S3-RLCD-4.2.
  3. //
  4. // Startup order matters: the panel, RTC and onboard sensor all come up before
  5. // Wi-Fi is touched, so a working clock and indoor reading are on screen within
  6. // a second or two even if the network never associates.
  7. #include <Arduino.h>
  8. #include <time.h>
  9. #include "ambient.h"
  10. #include "audio.h"
  11. #include "battery.h"
  12. #include "config.h"
  13. #include "display.h"
  14. #include "network.h"
  15. #include "outdoor.h"
  16. #include "portal.h"
  17. #include "power.h"
  18. #include "rtc.h"
  19. #include "settings.h"
  20. #include "temperature.h"
  21. namespace {
  22. bool g_indoorValid = false;
  23. float g_indoorC = 0.0f;
  24. uint32_t g_lastIndoorMs = 0;
  25. uint32_t g_lastDisplayTickMs = 0;
  26. bool g_setupMode = false;
  27. void readIndoor()
  28. {
  29. float c = 0.0f;
  30. if (temperature::read(c)) {
  31. g_indoorC = c;
  32. g_indoorValid = true;
  33. } else {
  34. // One bad read is usually noise on the bus; only give up after a while.
  35. static uint8_t failures = 0;
  36. if (++failures >= 3) {
  37. g_indoorValid = false;
  38. failures = 0;
  39. }
  40. }
  41. g_lastIndoorMs = millis();
  42. }
  43. // True if KEY is held down for KEY_FORCE_SETUP_MS at power-up. This is the
  44. // only way back into the setup portal once Wi-Fi has been stored.
  45. bool keyHeldAtBoot()
  46. {
  47. pinMode(KEY_BUTTON_PIN, INPUT_PULLUP);
  48. if (digitalRead(KEY_BUTTON_PIN) != LOW) {
  49. return false;
  50. }
  51. Serial.println("[boot] KEY down, hold to enter setup");
  52. const uint32_t deadline = millis() + KEY_FORCE_SETUP_MS;
  53. while (millis() < deadline) {
  54. if (digitalRead(KEY_BUTTON_PIN) != LOW) {
  55. return false; // released early: treat as a normal boot
  56. }
  57. delay(20);
  58. }
  59. return true;
  60. }
  61. display::Model buildModel()
  62. {
  63. display::Model m;
  64. // localtime() reads the system clock, which is seeded from the RTC at boot
  65. // and corrected by NTP when available.
  66. const time_t t = time(nullptr);
  67. struct tm local;
  68. localtime_r(&t, &local);
  69. const settings::Config &cfg = settings::get();
  70. m.timeValid = rtc::hasValidTime() || network::hasSyncedTime();
  71. if (m.timeValid) {
  72. m.hour = (uint8_t)local.tm_hour;
  73. m.minute = (uint8_t)local.tm_min;
  74. m.twelveHour = !cfg.clock24h;
  75. display::formatDateLine(local, cfg.dateFormat, cfg.dateUppercase,
  76. m.dateLine, sizeof(m.dateLine));
  77. }
  78. m.indoorValid = g_indoorValid;
  79. m.indoorC = g_indoorC;
  80. m.outdoorValid = outdoor::hasReading();
  81. m.outdoorC = outdoor::lastCelsius();
  82. m.outdoorStale = outdoor::isStale();
  83. m.batteryValid = cfg.showBattery && battery::hasReading();
  84. m.batteryPercent = battery::percent();
  85. return m;
  86. }
  87. void enterSetupMode()
  88. {
  89. g_setupMode = true;
  90. portal::startAccessPoint();
  91. char url[32];
  92. snprintf(url, sizeof(url), "http://%s", portal::apIp().toString().c_str());
  93. display::showSetupScreen(portal::apSsid(), portal::apPassword(), url);
  94. }
  95. } // namespace
  96. void setup()
  97. {
  98. // Before Serial, so the UART dividers are set up for the final clock.
  99. power::begin();
  100. Serial.begin(115200);
  101. delay(200);
  102. Serial.println("\n[boot] ESP32-S3-RLCD-4.2 temperature display");
  103. const bool forceSetup = keyHeldAtBoot();
  104. if (!display::begin()) {
  105. Serial.println("[boot] display init failed");
  106. }
  107. display::showBootMessage("Starting...");
  108. settings::begin();
  109. power::applyPanelMode();
  110. rtc::begin();
  111. rtc::syncSystemClockFromRtc();
  112. temperature::begin();
  113. readIndoor();
  114. battery::begin();
  115. ambient::begin();
  116. outdoor::begin();
  117. // Ahead of the setup-mode branch, because the portal serves /audio in both
  118. // modes. Probes the codec and mounts the card; it does not power up the
  119. // microphones or install the I2S driver, so it costs little before the first
  120. // draw.
  121. audio::begin();
  122. // Without stored credentials there is nothing to connect to and no way for
  123. // the user to supply them except the portal, so it takes over the screen.
  124. if (forceSetup || !settings::hasWifi()) {
  125. Serial.println(forceSetup ? "[boot] setup forced by KEY"
  126. : "[boot] no Wi-Fi stored, entering setup");
  127. enterSetupMode();
  128. return;
  129. }
  130. // Draw everything we already know before the network attempt, so the screen
  131. // is useful immediately.
  132. display::update(buildModel());
  133. network::begin();
  134. }
  135. void loop()
  136. {
  137. portal::loop();
  138. // Drains the encoder to a listening browser and tears a finished recording
  139. // down. Bounded work, so the clock below still runs on time.
  140. audio::loop();
  141. if (g_setupMode) {
  142. // The setup screen is static and the clock is almost certainly wrong;
  143. // serving the portal promptly is all that matters here.
  144. delay(5);
  145. return;
  146. }
  147. // Nothing on a reflective panel is readable in the dark, so this is where
  148. // most of the battery is saved. Returns only once the room is lit again.
  149. if (power::sleepWhileDark()) {
  150. readIndoor();
  151. battery::sample();
  152. }
  153. network::loop();
  154. outdoor::loop();
  155. battery::loop();
  156. const uint32_t nowMs = millis();
  157. if (nowMs - g_lastIndoorMs >= INDOOR_READ_MS) {
  158. readIndoor();
  159. }
  160. // The panel itself is only refreshed when a displayed value actually
  161. // changes; this tick just decides how promptly that is noticed.
  162. if (nowMs - g_lastDisplayTickMs >= DISPLAY_TICK_MS) {
  163. g_lastDisplayTickMs = nowMs;
  164. display::update(buildModel());
  165. }
  166. delay(10);
  167. }