diff --git a/include/internet_time.h b/include/internet_time.h new file mode 100644 index 0000000..fd6c9a9 --- /dev/null +++ b/include/internet_time.h @@ -0,0 +1,36 @@ +#ifndef INTERNET_TIME_H +#define INTERNET_TIME_H + +#include + +struct InternetTimeInfo +{ + bool valid; + uint16_t year; + uint8_t month; + uint8_t day; + uint8_t hour; + uint8_t minute; +}; + +class InternetTime +{ +public: + InternetTime(); + void update(unsigned long now, bool wifiConnected); + InternetTimeInfo info() const; + bool consumeDirty(); + +private: + void fetch(unsigned long now); + void updateCurrentTime(unsigned long now); + InternetTimeInfo infoFromEpoch(unsigned long epoch) const; + + unsigned long lastEpoch_; + unsigned long lastSyncMillis_; + unsigned long nextAttempt_; + InternetTimeInfo current_; + bool dirty_; +}; + +#endif diff --git a/include/screens.h b/include/screens.h index 05f6907..e46c0bb 100644 --- a/include/screens.h +++ b/include/screens.h @@ -4,9 +4,11 @@ #include #include "am2302_sensor.h" +#include "internet_time.h" #include "wifi_connection.h" -void drawScreen1(U8G2 &u8g2); +void setInternetTimeInfo(InternetTimeInfo timeInfo); +void drawTimeScreen(U8G2 &u8g2); void drawHelloWorldScreen(U8G2 &u8g2); void drawInternetConnectionScreen(U8G2 &u8g2, WifiConnectionInfo wifiInfo); void setEnvironmentReading(Am2302Reading reading); diff --git a/include/wifi_credentials.example.h b/include/wifi_credentials.example.h index c2b1a7f..ad8f93c 100644 --- a/include/wifi_credentials.example.h +++ b/include/wifi_credentials.example.h @@ -4,4 +4,7 @@ const char WIFI_SSID[] = "your-wifi-name"; const char WIFI_PASSWORD[] = "your-wifi-password"; +// Offset from UTC in seconds. Examples: Finland winter 7200, Finland summer 10800. +#define TIME_UTC_OFFSET_SECONDS 0 + #endif diff --git a/src/internet_time.cpp b/src/internet_time.cpp new file mode 100644 index 0000000..6e5c193 --- /dev/null +++ b/src/internet_time.cpp @@ -0,0 +1,142 @@ +#include "internet_time.h" + +#include + +#include "wifi_credentials.h" + +#ifndef TIME_UTC_OFFSET_SECONDS +#define TIME_UTC_OFFSET_SECONDS 0 +#endif + +namespace +{ +const unsigned long retryInterval = 15000; +const unsigned long refreshInterval = 3600000; +const unsigned long secondsPerDay = 86400; + +bool isLeapYear(uint16_t year) +{ + return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0; +} + +uint8_t daysInMonth(uint16_t year, uint8_t month) +{ + const uint8_t days[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; + if (month == 2 && isLeapYear(year)) { + return 29; + } + return days[month - 1]; +} + +unsigned long applyTimeOffset(unsigned long epoch) +{ + if (TIME_UTC_OFFSET_SECONDS >= 0) { + return epoch + TIME_UTC_OFFSET_SECONDS; + } + + const unsigned long offset = static_cast(-TIME_UTC_OFFSET_SECONDS); + return epoch > offset ? epoch - offset : 0; +} +} + +InternetTime::InternetTime() + : lastEpoch_(0), + lastSyncMillis_(0), + nextAttempt_(0), + current_({false, 0, 0, 0, 0, 0}), + dirty_(true) {} + +void InternetTime::update(unsigned long now, bool wifiConnected) +{ + if (current_.valid) { + updateCurrentTime(now); + } + + if (!wifiConnected) { + return; + } + + if (now >= nextAttempt_) { + fetch(now); + } +} + +InternetTimeInfo InternetTime::info() const +{ + return current_; +} + +bool InternetTime::consumeDirty() +{ + const bool d = dirty_; + dirty_ = false; + return d; +} + +void InternetTime::fetch(unsigned long now) +{ + const unsigned long epoch = WiFi.getTime(); + nextAttempt_ = now + retryInterval; + + if (epoch == 0) { + return; + } + + lastEpoch_ = epoch; + lastSyncMillis_ = now; + nextAttempt_ = now + refreshInterval; + updateCurrentTime(now); +} + +void InternetTime::updateCurrentTime(unsigned long now) +{ + const unsigned long elapsedSeconds = (now - lastSyncMillis_) / 1000; + const InternetTimeInfo updated = infoFromEpoch(lastEpoch_ + elapsedSeconds); + + if (!current_.valid || + current_.year != updated.year || + current_.month != updated.month || + current_.day != updated.day || + current_.hour != updated.hour || + current_.minute != updated.minute) { + current_ = updated; + dirty_ = true; + } +} + +InternetTimeInfo InternetTime::infoFromEpoch(unsigned long epoch) const +{ + unsigned long adjustedEpoch = applyTimeOffset(epoch); + unsigned long days = adjustedEpoch / secondsPerDay; + unsigned long secondsToday = adjustedEpoch % secondsPerDay; + + uint16_t year = 1970; + while (true) { + const uint16_t yearDays = isLeapYear(year) ? 366 : 365; + if (days < yearDays) { + break; + } + days -= yearDays; + year++; + } + + uint8_t month = 1; + while (true) { + const uint8_t monthDays = daysInMonth(year, month); + if (days < monthDays) { + break; + } + days -= monthDays; + month++; + } + + InternetTimeInfo info = { + true, + year, + month, + static_cast(days + 1), + static_cast(secondsToday / 3600), + static_cast((secondsToday % 3600) / 60) + }; + return info; +} diff --git a/src/main.cpp b/src/main.cpp index 3d32040..4c23e85 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -4,6 +4,7 @@ #include "am2302_sensor.h" #include "carousel.h" +#include "internet_time.h" #include "menu.h" #include "screens.h" #include "wifi_connection.h" @@ -16,6 +17,7 @@ uint8_t buttonNext = A4; uint8_t buttonSelect = A5; Am2302Sensor am2302(A3); WifiConnection wifiConnection; +InternetTime internetTime; enum class AppMode { @@ -26,7 +28,7 @@ enum class AppMode }; Screen screens[] = { - { drawScreen1 }, + { drawTimeScreen }, { drawEnvironmentScreen }, { drawWeatherIconScreen } }; @@ -142,6 +144,14 @@ void drawActiveView() void loop(){ const unsigned long now = millis(); wifiConnection.update(now); + internetTime.update(now, wifiConnection.info().connected); + + if (internetTime.consumeDirty()) { + setInternetTimeInfo(internetTime.info()); + if (appMode == AppMode::Carousel) { + carousel.requestRedraw(); + } + } if (appMode == AppMode::Carousel) { carousel.update(now); diff --git a/src/screens.cpp b/src/screens.cpp index 5957c13..98b282f 100644 --- a/src/screens.cpp +++ b/src/screens.cpp @@ -3,6 +3,15 @@ namespace { Am2302Reading environmentReading = {0.0F, 0.0F, false}; +InternetTimeInfo internetTimeInfo = {false, 0, 0, 0, 0, 0}; + +void printTwoDigits(U8G2 &u8g2, uint8_t value) +{ + if (value < 10) { + u8g2.print("0"); + } + u8g2.print(value); +} void drawSun(U8G2 &u8g2, uint8_t x, uint8_t y) { @@ -27,10 +36,33 @@ void drawCloud(U8G2 &u8g2, uint8_t x, uint8_t y) } } -void drawScreen1(U8G2 &u8g2) +void setInternetTimeInfo(InternetTimeInfo timeInfo) { - u8g2.setFont(u8g2_font_ncenB14_tr); - u8g2.drawStr(0, 20, "Screen 1"); + internetTimeInfo = timeInfo; +} + +void drawTimeScreen(U8G2 &u8g2) +{ + if (!internetTimeInfo.valid) { + u8g2.setFont(u8g2_font_6x12_tr); + u8g2.drawStr(0, 24, "Time has not"); + u8g2.drawStr(0, 40, "been updated"); + return; + } + + u8g2.setFont(u8g2_font_6x12_tr); + u8g2.setCursor(0, 14); + printTwoDigits(u8g2, internetTimeInfo.day); + u8g2.print("."); + printTwoDigits(u8g2, internetTimeInfo.month); + u8g2.print("."); + u8g2.print(internetTimeInfo.year); + + u8g2.setFont(u8g2_font_ncenB24_tr); + u8g2.setCursor(0, 54); + printTwoDigits(u8g2, internetTimeInfo.hour); + u8g2.print(":"); + printTwoDigits(u8g2, internetTimeInfo.minute); } void drawHelloWorldScreen(U8G2 &u8g2)