437 lines
No EOL
15 KiB
C++
437 lines
No EOL
15 KiB
C++
#include "Weather.h"
|
|
#include "../SevenSegment.h"
|
|
#include "../Icons.h"
|
|
#include <Fonts/FreeSans9pt7b.h>
|
|
#include <Fonts/FreeSansBold9pt7b.h>
|
|
#include <Fonts/FreeSans12pt7b.h>
|
|
#include <Fonts/FreeSans18pt7b.h>
|
|
#include <HTTPClient.h>
|
|
#include <math.h>
|
|
#include <sstream>
|
|
#include <iomanip>
|
|
|
|
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<std::string> 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<const unsigned char * PROGMEM> 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<const unsigned char * PROGMEM> 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 (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<std::string> 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<std::string> 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;
|
|
} |