network.cpp 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. #include "network.h"
  2. #include <WiFi.h>
  3. #include <time.h>
  4. #include "config.h"
  5. #include "portal.h"
  6. #include "rtc.h"
  7. #include "settings.h"
  8. namespace {
  9. bool g_synced = false;
  10. uint32_t g_lastAttemptMs = 0;
  11. uint32_t g_lastSyncMs = 0;
  12. bool g_connecting = false;
  13. bool g_portalStarted = false;
  14. bool g_suspended = false;
  15. void startConnect()
  16. {
  17. const settings::Config &cfg = settings::get();
  18. Serial.printf("[net] connecting to %s\n", cfg.ssid);
  19. WiFi.disconnect();
  20. WiFi.begin(cfg.ssid, cfg.password);
  21. g_lastAttemptMs = millis();
  22. g_connecting = true;
  23. }
  24. } // namespace
  25. namespace network {
  26. void begin()
  27. {
  28. // The timezone is applied to the system clock regardless of connectivity, so
  29. // the RTC-derived time renders correctly offline.
  30. setenv("TZ", settings::get().timezone, 1);
  31. tzset();
  32. if (!settings::hasWifi()) {
  33. Serial.println("[net] no Wi-Fi configured");
  34. return; // main() has already raised the setup portal
  35. }
  36. WiFi.mode(WIFI_STA);
  37. WiFi.setHostname(DEVICE_HOSTNAME);
  38. WiFi.setAutoReconnect(true);
  39. WiFi.setSleep(true); // the display is idle most of the time
  40. startConnect();
  41. }
  42. void loop()
  43. {
  44. if (!settings::hasWifi() || g_suspended) {
  45. return;
  46. }
  47. const uint32_t nowMs = millis();
  48. if (WiFi.status() == WL_CONNECTED) {
  49. if (g_connecting) {
  50. g_connecting = false;
  51. Serial.printf("[net] connected, ip=%s\n", WiFi.localIP().toString().c_str());
  52. syncTimeNow();
  53. }
  54. if (!g_portalStarted) {
  55. // Serve the settings page on the LAN so the outdoor URL and timezone can
  56. // be changed without re-provisioning.
  57. portal::startStation();
  58. g_portalStarted = true;
  59. }
  60. if (g_synced && nowMs - g_lastSyncMs >= NTP_RESYNC_MS) {
  61. syncTimeNow();
  62. }
  63. return;
  64. }
  65. // Not connected: retry, but never more often than WIFI_RETRY_MS. The device
  66. // deliberately does not fall back to the setup AP here — the clock and the
  67. // indoor reading must stay on screen through a router reboot.
  68. const uint32_t since = nowMs - g_lastAttemptMs;
  69. if (g_connecting && since < WIFI_CONNECT_TIMEOUT_MS) {
  70. return;
  71. }
  72. if (since >= WIFI_RETRY_MS || (g_connecting && since >= WIFI_CONNECT_TIMEOUT_MS)) {
  73. if (g_connecting) {
  74. Serial.println("[net] connect attempt timed out");
  75. g_connecting = false;
  76. g_lastAttemptMs = nowMs; // hold off for WIFI_RETRY_MS before retrying
  77. return;
  78. }
  79. startConnect();
  80. }
  81. }
  82. bool isConnected()
  83. {
  84. return WiFi.status() == WL_CONNECTED;
  85. }
  86. bool syncTimeNow()
  87. {
  88. if (!isConnected()) {
  89. return false;
  90. }
  91. const settings::Config &cfg = settings::get();
  92. configTzTime(cfg.timezone, cfg.ntpServer, NTP_SERVER_2);
  93. // Wait briefly for the first SNTP response. A plausible year means the
  94. // system clock has been set from the network rather than left at the epoch.
  95. struct tm local = {};
  96. const uint32_t deadline = millis() + 10000UL;
  97. bool got = false;
  98. while (millis() < deadline) {
  99. if (getLocalTime(&local, 200) && local.tm_year + 1900 >= 2024) {
  100. got = true;
  101. break;
  102. }
  103. delay(100);
  104. }
  105. if (!got) {
  106. Serial.println("[net] NTP sync failed");
  107. return false;
  108. }
  109. // Persist UTC into the RTC so timekeeping survives the next outage.
  110. const time_t utcNow = time(nullptr);
  111. struct tm utc;
  112. gmtime_r(&utcNow, &utc);
  113. if (!rtc::setTime(utc)) {
  114. Serial.println("[net] NTP ok but RTC write failed");
  115. }
  116. g_synced = true;
  117. g_lastSyncMs = millis();
  118. Serial.printf("[net] time synced: %04d-%02d-%02d %02d:%02d local\n",
  119. local.tm_year + 1900, local.tm_mon + 1, local.tm_mday,
  120. local.tm_hour, local.tm_min);
  121. return true;
  122. }
  123. bool hasSyncedTime()
  124. {
  125. return g_synced;
  126. }
  127. void suspend()
  128. {
  129. if (g_suspended || !settings::hasWifi()) {
  130. return;
  131. }
  132. WiFi.disconnect(true);
  133. WiFi.mode(WIFI_OFF);
  134. g_suspended = true;
  135. g_connecting = false;
  136. }
  137. void resume()
  138. {
  139. if (!g_suspended) {
  140. return;
  141. }
  142. g_suspended = false;
  143. WiFi.mode(WIFI_STA);
  144. WiFi.setHostname(DEVICE_HOSTNAME);
  145. WiFi.setAutoReconnect(true);
  146. WiFi.setSleep(true);
  147. // The settings page and mDNS have to be re-announced on the new association.
  148. g_portalStarted = false;
  149. startConnect();
  150. }
  151. } // namespace network