Add fetching from wttr.in and moon display
All checks were successful
Compile / Compile (push) Successful in 1m26s

This commit is contained in:
Lewis Jackson 2023-06-02 18:45:26 +03:00
parent e11bcdc9cd
commit 1b6f89faea
5 changed files with 344 additions and 43 deletions

View file

@ -23,6 +23,7 @@ namespace Icons
namespace Weather
{
// 30x30
const unsigned char wind[] PROGMEM = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
@ -34,6 +35,7 @@ namespace Icons
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
// 30x30
const unsigned char humidity[] PROGMEM = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x07, 0x80, 0x00,
@ -45,6 +47,41 @@ namespace Icons
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
// 30x30
const unsigned char sunrise [] PROGMEM = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x03, 0x00,
0x01, 0x80, 0x06, 0x00, 0x00, 0x80, 0x04, 0x00, 0x00, 0x0f, 0xc0, 0x00, 0x00, 0x18, 0x60, 0x00,
0x00, 0x30, 0x30, 0x00, 0x00, 0x20, 0x10, 0x00, 0x00, 0x60, 0x18, 0x00, 0x1e, 0x60, 0x19, 0xe0,
0x00, 0x60, 0x18, 0x00, 0x00, 0x20, 0x10, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x07, 0x80, 0x00,
0x00, 0x7c, 0xfc, 0x00, 0x00, 0x70, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
// 30x30
const unsigned char sunset [] PROGMEM = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00,
0x01, 0x80, 0x06, 0x00, 0x00, 0x80, 0x04, 0x00, 0x00, 0x0f, 0xc0, 0x00, 0x00, 0x1c, 0xe0, 0x00,
0x00, 0x30, 0x30, 0x00, 0x00, 0x20, 0x10, 0x00, 0x00, 0x60, 0x18, 0x00, 0x1e, 0x60, 0x19, 0xe0,
0x00, 0x60, 0x18, 0x00, 0x00, 0x20, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x78, 0x78, 0x00, 0x00, 0x7c, 0xf8, 0x00, 0x00, 0x07, 0x80, 0x00, 0x00, 0x03, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
const unsigned char noon [] PROGMEM = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, 0xe0, 0x00, 0x00, 0x73, 0x38, 0x00, 0x00, 0xc3, 0x0c, 0x00,
0x01, 0x83, 0x06, 0x00, 0x03, 0x03, 0x03, 0x00, 0x02, 0x03, 0x01, 0x00, 0x06, 0x03, 0x01, 0x80,
0x06, 0x03, 0x00, 0x80, 0x04, 0x03, 0x00, 0x80, 0x04, 0x00, 0x00, 0x80, 0x04, 0x00, 0x00, 0x80,
0x04, 0x00, 0x00, 0x80, 0x06, 0x00, 0x01, 0x80, 0x02, 0x00, 0x01, 0x00, 0x03, 0x00, 0x03, 0x00,
0x01, 0x80, 0x06, 0x00, 0x00, 0xc0, 0x0c, 0x00, 0x00, 0x60, 0x18, 0x00, 0x00, 0x3f, 0xf0, 0x00,
0x00, 0x07, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
// 50x50
const unsigned char day_clear[] PROGMEM = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,

View file

@ -6,13 +6,30 @@
#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 int WatchFacePages::Weather::m_lastCalculatedDay = 0XFFFFFFFF;
RTC_DATA_ATTR float WatchFacePages::Weather::m_locationLat = DEFAULT_LATITUDE;
RTC_DATA_ATTR float WatchFacePages::Weather::m_locationLon = DEFAULT_LONGITUDE;
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 uint8_t WatchFacePages::Weather::m_currentWeatherIcon;
WatchFacePages::Weather::Weather(WatchyDisplay & display, WatchFeatures::WatchFeatures & features)
: m_display(display), m_features(features)
{
@ -37,20 +54,10 @@ void WatchFacePages::Weather::DrawPage(bool partialRefresh)
m_display.fillScreen(GxEPD_WHITE);
m_display.setTextColor(GxEPD_BLACK);
if (m_lastSyncTime == 0) {
m_display.setFont(&FreeSans12pt7b);
int16_t x, y;
uint16_t w, h;
m_display.getTextBounds("Have not synced", 0, 0, &x, &y, &w, &h);
m_display.setCursor(DISPLAY_WIDTH / 2 - w / 2, 110);
m_display.print("Have not synced");
m_display.display(partialRefresh);
return;
}
bool weatherOutdated = false;
if (m_features.rtc.GetTimestamp() - m_lastSyncTime > 86400) {
// 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;
}
@ -60,42 +67,78 @@ void WatchFacePages::Weather::DrawPage(bool partialRefresh)
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 {
m_display.drawBitmap(5, 125, Icons::Weather::day_clear, 75, 75, GxEPD_BLACK);
m_display.setFont(&FreeSans12pt7b);
std::string temperature = "-20.5 C";
std::ostringstream temperature;
temperature << (int)m_currentTemperature << " C";
m_display.getTextBounds(temperature.c_str(), 0, 0, &x, &y, &w, &h);
m_display.getTextBounds(temperature.str().c_str(), 0, 0, &x, &y, &w, &h);
m_display.setCursor(190 - w, 142);
m_display.print(temperature.c_str());
m_display.print(temperature.str().c_str());
// Hacky degree symbol
m_display.fillCircle(190 - 23, 129, 3, GxEPD_BLACK);
m_display.fillCircle(190 - 23, 129, 1, GxEPD_WHITE);
// Wind speed
m_display.drawBitmap(165, 145, Icons::Weather::wind, 30, 30, GxEPD_BLACK);
std::string windSpeed = "99 m/s";
std::ostringstream windSpeed;
windSpeed << std::fixed << std::setprecision(1) << m_currentWindSpeed << " m/s";
m_display.setFont(&FreeSans9pt7b);
m_display.getTextBounds(windSpeed.c_str(), 0, 0, &x, &y, &w, &h);
m_display.getTextBounds(windSpeed.str().c_str(), 0, 0, &x, &y, &w, &h);
m_display.setCursor(160 - w, 165);
m_display.print(windSpeed.c_str());
m_display.print(windSpeed.str().c_str());
// Humidity
m_display.drawBitmap(165, 170, Icons::Weather::humidity, 30, 30, GxEPD_BLACK);
std::string humidity = "100";
m_display.getTextBounds(humidity.c_str(), 0, 0, &x, &y, &w, &h);
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.c_str());
m_display.print(humidity.str().c_str());
}
m_display.display(partialRefresh);
}
@ -112,10 +155,11 @@ void WatchFacePages::Weather::Resync()
return;
}
if (DO_GEOLOCATION) {
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) {
@ -151,7 +195,167 @@ void WatchFacePages::Weather::Resync()
m_locationLon = std::stof(lines[8]);
}
// https://wttr.in/60.170833,24.9375?0Q&format=+%x:%t:%h
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");
client.begin(url.str().c_str());
int httpCode = client.GET();
m_features.wifi.Disconnect();
if (httpCode != 200) {
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) {
return;
}
// 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> iconLookupTable = {"?", "mm", "=", "///", "//", "**", "/*/", "/", ".", "x", "x/", "*", "*/", "m", "o", "/!/", "!/", "*!*", "mmm"};
std::string iconName = parts[1];
for (int i = 0; i < iconLookupTable.size(); i++) {
if (iconName == iconLookupTable[i]) {
m_currentWeatherIcon = i;
}
}
std::string temperature = parts[2].substr(0, parts[2].length() - 2);
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 = std::round(windSpeedKmh * 0.277778);
m_features.wifi.Disconnect();
Recalc();
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;
}

View file

@ -22,11 +22,26 @@ public:
void Resync();
private:
void Recalc(); // Offline daily ephemeris like sunrise/sunset
WatchyDisplay & m_display;
WatchFeatures::WatchFeatures & m_features;
static RTC_DATA_ATTR uint64_t m_lastSyncTime;
static RTC_DATA_ATTR int m_lastCalculatedDay;
static RTC_DATA_ATTR char m_locationCity[128];
static RTC_DATA_ATTR float m_locationLat, m_locationLon;
static RTC_DATA_ATTR double m_locationLat, m_locationLon;
static RTC_DATA_ATTR uint8_t m_sunriseHour;
static RTC_DATA_ATTR uint8_t m_sunriseMinute;
static RTC_DATA_ATTR uint8_t m_noonHour;
static RTC_DATA_ATTR uint8_t m_noonMinute;
static RTC_DATA_ATTR uint8_t m_sunsetHour;
static RTC_DATA_ATTR uint8_t m_sunsetMinute;
static RTC_DATA_ATTR double m_moonPhase;
static RTC_DATA_ATTR uint8_t m_moonBitmap[8 * 64];
static RTC_DATA_ATTR double m_currentTemperature;
static RTC_DATA_ATTR float m_currentWindSpeed;
static RTC_DATA_ATTR uint8_t m_currentHumidity;
static RTC_DATA_ATTR uint8_t m_currentWeatherIcon;
};

View file

@ -106,6 +106,49 @@ void WatchFeatures::RTC::OffsetTime(tmElements_t & tm, int offsetInSeconds)
breakTime(t, tm);
}
unsigned int WatchFeatures::RTC::GetDayOfYear(tmElements_t & tm)
{
unsigned int dayOfYear = 1;
for (int i = 1; i < tm.Month; i++) {
switch (i) {
case 2:
dayOfYear += (tm.Year % 4 == 0) ? 29 : 28;
break;
case 4:
case 6:
case 9:
case 11:
dayOfYear += 30;
break;
default:
dayOfYear += 31;
break;
}
}
dayOfYear += tm.Day;
return dayOfYear;
}
unsigned int WatchFeatures::RTC::DaysDifference(tmElements_t & tm1, tmElements_t & tm2)
{
unsigned int dayOfYear1 = GetDayOfYear(tm1);
unsigned int dayOfYear2 = GetDayOfYear(tm2);
unsigned int days = 0;
if (tm1.Year == tm2.Year) {
days = dayOfYear2 - dayOfYear1;
} else {
days = 365 - dayOfYear1 + dayOfYear2;
for (int i = tm1.Year + 1; i < tm2.Year; i++) {
days += (i % 4 == 0) ? 366 : 365;
}
}
return days;
}
// TODO: implement more advanced wakeup logic, i.e. > 255 seconds that are not a multiple of 60
bool WatchFeatures::RTC::CheckWakeup()
{

View file

@ -22,6 +22,8 @@ public:
void Resync(); // Resync the timer cycle, both initially and after RTC resync
static void OffsetTime(tmElements_t & tm, int offsetInSeconds);
static unsigned int GetDayOfYear(tmElements_t & tm);
static unsigned int DaysDifference(tmElements_t & tm1, tmElements_t & tm2);
private:
Rtc_Pcf8563 m_rtcPcf;