#include "Weather.h" #include "../SevenSegment.h" #include "../Icons.h" #include #include #include #include #include #include #include #include RTC_DATA_ATTR uint64_t WatchFacePages::Weather::m_lastSyncTime = 0; RTC_DATA_ATTR uint64_t WatchFacePages::Weather::m_lastSyncAttemptTime = 0; RTC_DATA_ATTR int WatchFacePages::Weather::m_lastCalculatedDay = 0XFFFFFFFF; RTC_DATA_ATTR double WatchFacePages::Weather::m_locationLat = DEFAULT_LATITUDE; RTC_DATA_ATTR double WatchFacePages::Weather::m_locationLon = DEFAULT_LONGITUDE; RTC_DATA_ATTR char WatchFacePages::Weather::m_locationCity[128]; RTC_DATA_ATTR uint8_t WatchFacePages::Weather::m_sunriseHour; RTC_DATA_ATTR uint8_t WatchFacePages::Weather::m_sunriseMinute; RTC_DATA_ATTR uint8_t WatchFacePages::Weather::m_noonHour; RTC_DATA_ATTR uint8_t WatchFacePages::Weather::m_noonMinute; RTC_DATA_ATTR uint8_t WatchFacePages::Weather::m_sunsetHour; RTC_DATA_ATTR uint8_t WatchFacePages::Weather::m_sunsetMinute; RTC_DATA_ATTR double WatchFacePages::Weather::m_moonPhase; RTC_DATA_ATTR uint8_t WatchFacePages::Weather::m_moonBitmap[8 * 64]; RTC_DATA_ATTR double WatchFacePages::Weather::m_currentTemperature; RTC_DATA_ATTR float WatchFacePages::Weather::m_currentWindSpeed; RTC_DATA_ATTR uint8_t WatchFacePages::Weather::m_currentHumidity; RTC_DATA_ATTR char WatchFacePages::Weather::m_currentWeatherCondition[4]; WatchFacePages::Weather::Weather(WatchyDisplay & display, WatchFeatures::WatchFeatures & features) : m_display(display), m_features(features) { } void WatchFacePages::Weather::InitBoot() { memcpy(m_locationCity, DEFAULT_CITY_NAME, sizeof(DEFAULT_CITY_NAME)); m_locationCity[sizeof(DEFAULT_CITY_NAME)] = '\0'; } void WatchFacePages::Weather::InitWake() { } void WatchFacePages::Weather::DrawPage(bool partialRefresh) { Resync(); Recalc(); m_display.setFullWindow(); m_display.fillScreen(GxEPD_WHITE); m_display.setTextColor(GxEPD_BLACK); bool weatherOutdated = false; // If it hasn't synced in 24 hours, assume weather is outdated if (m_lastSyncTime == 0 || m_features.rtc.GetTimestamp() - m_lastSyncTime > 86400) { weatherOutdated = true; } // City name and icon m_display.setFont(&FreeSans9pt7b); int16_t x, y; uint16_t w, h; m_display.drawBitmap(5, 5, Icons::city, 29, 23, GxEPD_BLACK); m_display.setCursor(39, 22); m_display.print(m_locationCity); // Separator m_display.fillRect(10, 35, DISPLAY_WIDTH - 20, 2, GxEPD_BLACK); // Moon m_display.drawBitmap(200 - 64 - 10, 45, m_moonBitmap, 64, 64, GxEPD_BLACK); m_display.drawCircle(200 - 64 - 10 + 32, 45 + 32, 32, GxEPD_BLACK); // Sunrise, solar noon and sunset m_display.drawBitmap(10, 38, Icons::Weather::sunrise, 30, 30, GxEPD_BLACK); std::ostringstream sunrise; sunrise << std::setfill('0') << std::setw(2) << (int)m_sunriseHour << ":" << std::setw(2) << (int)m_sunriseMinute; m_display.setFont(&FreeSans9pt7b); m_display.setCursor(45, 56); m_display.setTextColor(GxEPD_BLACK); m_display.print(sunrise.str().c_str()); m_display.drawBitmap(10, 62, Icons::Weather::noon, 30, 30, GxEPD_BLACK); std::ostringstream noon; noon << std::setfill('0') << std::setw(2) << (int)m_noonHour << ":" << std::setw(2) << (int)m_noonMinute; m_display.setCursor(45, 80); m_display.print(noon.str().c_str()); m_display.drawBitmap(10, 87, Icons::Weather::sunset, 30, 30, GxEPD_BLACK); std::ostringstream sunset; sunset << std::setfill('0') << std::setw(2) << (int)m_sunsetHour << ":" << std::setw(2) << (int)m_sunsetMinute; m_display.setFont(&FreeSans9pt7b); m_display.setCursor(45, 106); m_display.print(sunset.str().c_str()); // Temperature and weather icon // Separator m_display.fillRect(10, 116, DISPLAY_WIDTH - 20, 2, GxEPD_BLACK); if (weatherOutdated) { m_display.setFont(&FreeSans9pt7b); m_display.getTextBounds("No Weather Data", 0, 0, &x, &y, &w, &h); m_display.setCursor(DISPLAY_WIDTH / 2 - w / 2, 162); m_display.print("No Weather Data"); } else { // Unknown, Cloudy, Fog, Heavy Rain, Heavy Showers, Heavy Snow, Heavy Snow Showers, Light Rain, Light Showers, Light Sleet, Light Sleet Showers, Light Snow, Light Snow Showers, Partly Cloudy, Sunny, Thundery Heavy Rain, Thundery Showers, Thundery Snow Showers, Very Cloudy std::vector iconLookupCondition = {"?", "mm", "=", "///", "//", "**", "/*/", "/", ".", "x", "x/", "*", "*/", "m", "o", "/!/", "!/", "*!*", "mmm"}; std::string iconName = m_currentWeatherCondition; int iconNum = 0; for (int i = 0; i < iconLookupCondition.size(); i++) { if (iconName == iconLookupCondition[i]) { iconNum = i; break; } } // Get current time to see if it's day or night tmElements_t currentTime; m_features.rtc.Get(currentTime); m_features.rtc.OffsetTime(currentTime, m_features.storage.GetTzOffset()); const unsigned char * iconBitmap = nullptr; if (currentTime.Hour >= m_sunriseHour && currentTime.Hour < m_sunsetHour) { const std::vector dayIconLookupBitmap = { Icons::Weather::alien, Icons::Weather::day_cloudy, Icons::Weather::day_fog, Icons::Weather::day_rain_wind, Icons::Weather::day_rain_mix, Icons::Weather::day_snow, Icons::Weather::day_snow_wind, Icons::Weather::day_rain, Icons::Weather::day_showers, Icons::Weather::day_sleet, Icons::Weather::day_sleet, Icons::Weather::day_snow, Icons::Weather::day_snow, Icons::Weather::day_cloudy, Icons::Weather::day_sunny, Icons::Weather::day_thunderstorm, Icons::Weather::day_thunderstorm, Icons::Weather::day_snow_thunderstorm, Icons::Weather::cloudy }; iconBitmap = dayIconLookupBitmap[iconNum]; } else { const std::vector nightIconLookupBitmap = { Icons::Weather::alien, Icons::Weather::night_cloudy, Icons::Weather::night_fog, Icons::Weather::night_rain_wind, Icons::Weather::night_rain_mix, Icons::Weather::night_snow, Icons::Weather::night_snow_wind, Icons::Weather::night_rain, Icons::Weather::night_showers, Icons::Weather::night_sleet, Icons::Weather::night_sleet, Icons::Weather::night_snow, Icons::Weather::night_snow, Icons::Weather::night_cloudy, Icons::Weather::night_clear, Icons::Weather::night_thunderstorm, Icons::Weather::night_thunderstorm, Icons::Weather::night_snow_thunderstorm, Icons::Weather::cloudy }; iconBitmap = nightIconLookupBitmap[iconNum]; } m_display.drawBitmap(5, 125, iconBitmap, 75, 75, GxEPD_BLACK); m_display.setFont(&FreeSans12pt7b); std::ostringstream temperature; temperature << (int)m_currentTemperature << " C"; m_display.getTextBounds(temperature.str().c_str(), 0, 0, &x, &y, &w, &h); m_display.setCursor(190 - w, 142); m_display.print(temperature.str().c_str()); // Hacky degree symbol m_display.fillCircle(190 - 19, 129, 3, GxEPD_BLACK); m_display.fillCircle(190 - 19, 129, 1, GxEPD_WHITE); // Wind speed m_display.drawBitmap(165, 145, Icons::Weather::wind, 30, 30, GxEPD_BLACK); std::ostringstream windSpeed; windSpeed << std::fixed << std::setprecision(1) << m_currentWindSpeed << " m/s"; m_display.setFont(&FreeSans9pt7b); m_display.getTextBounds(windSpeed.str().c_str(), 0, 0, &x, &y, &w, &h); m_display.setCursor(160 - w, 165); m_display.print(windSpeed.str().c_str()); // Humidity m_display.drawBitmap(165, 170, Icons::Weather::humidity, 30, 30, GxEPD_BLACK); std::ostringstream humidity; humidity << (int)m_currentHumidity; m_display.getTextBounds(humidity.str().c_str(), 0, 0, &x, &y, &w, &h); m_display.setCursor(160 - w, 190); m_display.print(humidity.str().c_str()); } m_display.display(partialRefresh); } void WatchFacePages::Weather::Resync() { uint64_t currentTime = m_features.rtc.GetTimestamp(); if(m_lastSyncTime > 0 && currentTime - m_lastSyncTime < WEATHER_UPDATE_INTERVAL) { return; } if (m_lastSyncTime > 0 && currentTime - m_lastSyncAttemptTime < WEATHER_UPDATE_BACKOFF) { return; } if(!m_features.wifi.Connect()) { m_lastSyncAttemptTime = currentTime; return; } HTTPClient client; client.setConnectTimeout(3000); // 3 second max timeout // Get geolocation from IP if (DO_GEOLOCATION) { client.begin("http://ip-api.com/line/"); int httpCode = client.GET(); if (httpCode != 200) { m_features.wifi.Disconnect(); m_lastSyncAttemptTime = currentTime; return; } String payload = client.getString(); client.end(); std::string reponse(payload.c_str()); // Split into lines std::vector lines; std::string::size_type pos = 0; std::string::size_type prev = 0; char delimiter = '\n'; while ((pos = reponse.find(delimiter, prev)) != std::string::npos) { lines.push_back(reponse.substr(prev, pos - prev)); prev = pos + 1; } // Check if we got enough lines if (lines.size() < 14) { m_features.wifi.Disconnect(); m_lastSyncAttemptTime = currentTime; return; } std::string location = lines[5]; location = location.substr(0, 127); m_locationCity[location.length()] = '\0'; memcpy(m_locationCity, location.c_str(), location.length()); m_locationLat = std::stof(lines[7]); m_locationLon = std::stof(lines[8]); // Force recalculation next time m_lastCalculatedDay = 0xFFFFFFFF; } std::ostringstream url; url << "http://wttr.in/" << m_locationLat << "," << m_locationLon << "?0Q&format=%x:%t:%h:%w"; // Grr. wttr.in does a redirect to HTTPS if your agent isn't curl client.setUserAgent("curl/8.0.1"); Serial.println(url.str().c_str()); client.begin(url.str().c_str()); int httpCode = client.GET(); m_features.wifi.Disconnect(); if (httpCode != 200) { m_lastSyncAttemptTime = currentTime; return; } String payload = client.getString(); client.end(); std::string reponse(payload.c_str()); // Split on : std::vector parts; std::string::size_type pos = 0; std::string::size_type prev = 0; char delimiter = ':'; while ((pos = reponse.find(delimiter, prev)) != std::string::npos) { parts.push_back(reponse.substr(prev, pos - prev)); prev = pos + 1; } parts.push_back(reponse.substr(prev)); if (parts.size() != 4) { m_lastSyncAttemptTime = currentTime; return; } memcpy(m_currentWeatherCondition, parts[0].c_str(), parts[0].length()); m_currentWeatherCondition[parts[0].length()] = '\0'; std::string temperature = parts[1].substr(1, parts[1].length() - 4); Serial.println(temperature.c_str()); m_currentTemperature = std::stoi(temperature); std::string humidity = parts[2].substr(0, parts[2].length() - 1); m_currentHumidity = std::stoi(humidity); std::string windSpeed = parts[3].substr(3, parts[3].length() - 7); float windSpeedKmh = std::stof(windSpeed); // Convert to m/s m_currentWindSpeed = windSpeedKmh * 0.277778; m_features.wifi.Disconnect(); m_lastSyncTime = currentTime; } void WatchFacePages::Weather::Recalc() { tmElements_t tm; m_features.rtc.Get(tm); if (m_lastCalculatedDay == tm.Day) { return; } unsigned int dayOfYear = m_features.rtc.GetDayOfYear(tm); double denominator = tm.Year / 4 ? 366.0 : 365.0; double fractionalYear = (2.0 * M_PI / denominator) * (dayOfYear - 1 + ((0 - 12.0) / 24.0)); double eqtime = 229.18 * (0.000075 + 0.001868 * cos(fractionalYear) - 0.032077 * sin(fractionalYear) - 0.014615 * cos(2.0 * fractionalYear) - 0.040849 * sin(2.0 * fractionalYear)); double declination = 0.006918 - 0.399912 * cos(fractionalYear) + 0.070257 * sin(fractionalYear) - 0.006758 * cos(2.0 * fractionalYear) + 0.000907 * sin(2.0 * fractionalYear) - 0.002697 * cos(3.0 * fractionalYear) + 0.00148 * sin(3.0 * fractionalYear); const double latitudeRadians = (double)m_locationLat * M_PI / 180.0; const double longitudeRadians = (double)m_locationLon * M_PI / 180.0; const double sunriseSunsetAngle = 1.5853349200000815; double timeOffset = eqtime + 4.0 * (double)m_locationLon - 60.0; double trueSolarTime = tm.Hour * 60.0 + tm.Minute + tm.Second / 60.0 + timeOffset; double hourAngle = acos(cos(sunriseSunsetAngle) / (cos(latitudeRadians) * cos(declination)) - tan(latitudeRadians) * tan(declination)); hourAngle = hourAngle * 180.0 / M_PI; double sunrise = 720 - 4.0 * ((double)m_locationLon + hourAngle) - eqtime; double sunset = 720 - 4.0 * ((double)m_locationLon - hourAngle) - eqtime; double noon = 720 - 4.0 * ((double)m_locationLon) - eqtime; sunrise += m_features.storage.GetTzOffset() / 60; sunset += m_features.storage.GetTzOffset() / 60; noon += m_features.storage.GetTzOffset() / 60; m_sunriseHour = sunrise / 60; m_sunriseMinute = sunrise - m_sunriseHour * 60; m_sunsetHour = sunset / 60; m_sunsetMinute = sunset - m_sunsetHour * 60; m_noonHour = noon / 60; m_noonMinute = noon - m_noonHour * 60; tmElements_t newMoon; newMoon.Year = 2023 - 1970; newMoon.Month = 5; newMoon.Day = 19; unsigned int daysDifference = m_features.rtc.DaysDifference(newMoon, tm); m_moonPhase = fmod((double)daysDifference, 29.530588853) / 29.530588853; // Draw a 1-bit bitmap with the moon phase bool pixels[64 * 64]; for (int i = 0; i < 64 * 64; i++) { pixels[i] = false; } for (int x = 0; x < 64; x++) { for (int y = 0; y < 64; y++) { float distance = sqrt(powf(fabsf(x - 32), 2) + powf(fabsf(y - 32), 2)); if (distance < 32) { float inclination = asinf(((float)y - 32.0f) / 32.0f); float azimuth = acosf((float)(x - 32) / (-32.0f * cosf(inclination))); azimuth += M_PI; azimuth += (m_moonPhase) * M_PI * 2.0f; while (azimuth > M_PI * 2.0f) { azimuth -= M_PI * 2.0f; } while (azimuth < 0) { azimuth += M_PI * 2.0f; } if (azimuth > M_PI) { pixels[x + y * 64] = true; } } else { pixels[x + y * 64] = false; } } } // Needs to be split into two parts because pio does too aggressive optimisation for (unsigned int y = 0; y < 32; y++) { for (unsigned int x = 0; x < 8; x++) { uint8_t value = 0; for (unsigned int i = 0; i < 8; i++) { value |= pixels[(x * 8 + i) + (y * 64)] << (7 - i); } m_moonBitmap[x + y * 8] = value; } } for (unsigned int y = 32; y < 64; y++) { for (unsigned int x = 0; x < 8; x++) { uint8_t value = 0; for (unsigned int i = 0; i < 8; i++) { value |= pixels[(x * 8 + i) + (y * 64)] << (7 - i); } m_moonBitmap[x + y * 8] = value; } } m_lastCalculatedDay = tm.Day; }