492 lines
16 KiB
C++
492 lines
16 KiB
C++
#define USE_HSPI_FOR_EPD
|
|
|
|
// base class GxEPD2_GFX can be used to pass references or pointers to the display instance as parameter, uses ~1.2k more code
|
|
// enable or disable GxEPD2_GFX base class
|
|
#define ENABLE_GxEPD2_GFX 0
|
|
|
|
#include <GxEPD2_BW.h>
|
|
#include <Fonts/FreeMonoBold9pt7b.h>
|
|
#define GxEPD2_DISPLAY_CLASS GxEPD2_BW
|
|
|
|
#define GxEPD2_DRIVER_CLASS GxEPD2_750_GDEY075T7 // GDEY075T7 800x480, UC8179 (GD7965), (FPC-C001 20.08.20)
|
|
|
|
// somehow there should be an easier way to do this
|
|
#define GxEPD2_BW_IS_GxEPD2_BW true
|
|
#define GxEPD2_1248_IS_GxEPD2_1248 true
|
|
#define IS_GxEPD(c, x) (c##x)
|
|
#define IS_GxEPD2_BW(x) IS_GxEPD(GxEPD2_BW_IS_, x)
|
|
#define IS_GxEPD2_1248(x) IS_GxEPD(GxEPD2_1248_IS_, x)
|
|
|
|
#if defined(ESP32)
|
|
#define MAX_DISPLAY_BUFFER_SIZE 65536ul // e.g.
|
|
#if IS_GxEPD2_BW(GxEPD2_DISPLAY_CLASS)
|
|
#define MAX_HEIGHT(EPD) (EPD::HEIGHT <= MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8) ? EPD::HEIGHT : MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8))
|
|
#elif IS_GxEPD2_3C(GxEPD2_DISPLAY_CLASS)
|
|
#define MAX_HEIGHT(EPD) (EPD::HEIGHT <= (MAX_DISPLAY_BUFFER_SIZE / 2) / (EPD::WIDTH / 8) ? EPD::HEIGHT : (MAX_DISPLAY_BUFFER_SIZE / 2) / (EPD::WIDTH / 8))
|
|
#elif IS_GxEPD2_7C(GxEPD2_DISPLAY_CLASS)
|
|
#define MAX_HEIGHT(EPD) (EPD::HEIGHT <= (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2) ? EPD::HEIGHT : (MAX_DISPLAY_BUFFER_SIZE) / (EPD::WIDTH / 2))
|
|
#endif
|
|
GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)> display(GxEPD2_DRIVER_CLASS(/*CS=*/ 15, /*DC=*/ 27, /*RST=*/ 26, /*BUSY=*/ 25));
|
|
#endif
|
|
#if defined(ESP32) && defined(USE_HSPI_FOR_EPD)
|
|
SPIClass hspi(HSPI);
|
|
#endif
|
|
|
|
// <Local Includes>
|
|
#include "bitmap.h"
|
|
#include "config_loader.h"
|
|
#include "weather.h"
|
|
#include "helper_fn.h"
|
|
#include "nextcloud_cal.h"
|
|
|
|
// <Fonts>
|
|
#include <Fonts/FreeMono9pt7b.h>
|
|
#include <Fonts/FreeMonoBold9pt7b.h>
|
|
#include <Fonts/FreeMonoBold12pt7b.h>
|
|
#include <Fonts/FreeSans9pt7b.h>
|
|
#include <Fonts/FreeSansBold24pt7b.h>
|
|
|
|
// <Connectivity>
|
|
#include <WiFi.h>
|
|
#include <WiFiClientSecure.h>
|
|
#include <time.h>
|
|
#include <HTTPClient.h>
|
|
#include "tls_certificates.h"
|
|
#include <ArduinoJson.h>
|
|
|
|
// Globals
|
|
Config conf;
|
|
ActiveFeatures activeFeatures;
|
|
int loopIteration = 0;
|
|
|
|
void setup()
|
|
{
|
|
Serial.begin(115200);
|
|
delay(1000);
|
|
Serial.println();
|
|
Serial.println("setup");
|
|
// *** special handling for Waveshare ESP32 Driver board *** //
|
|
// ********************************************************* //
|
|
#if defined(ESP32) && defined(USE_HSPI_FOR_EPD)
|
|
hspi.begin(13, 12, 14, 15); // remap hspi for EPD (swap pins)
|
|
display.epd2.selectSPI(hspi, SPISettings(4000000, MSBFIRST, SPI_MODE0));
|
|
#endif
|
|
// *** end of special handling for Waveshare ESP32 Driver board *** //
|
|
// **************************************************************** //
|
|
display.init(115200);
|
|
// first update should be full refresh
|
|
delay(1000);
|
|
bootLogo();
|
|
// Do Selftest and Initiation here... Check for WiFi, check NTP Update, check connectivity.
|
|
int text_y = 155;
|
|
int line_height = 20;
|
|
bootMsg("v0.0.1-alpha", text_y);
|
|
text_y += line_height;
|
|
bootMsg("https://git.fjla.uk/fred.boniface/esphub", text_y);
|
|
text_y += line_height;
|
|
conf = loadConfig();
|
|
if (connect_WiFi(conf.ssid.c_str(), conf.wifiPass.c_str())) {
|
|
bootMsg("WiFi Connection: OK", text_y);
|
|
activeFeatures.network = true;
|
|
} else {
|
|
bootMsg("WiFi Connection: FAIL", text_y);
|
|
activeFeatures.network = false;
|
|
}
|
|
text_y += line_height;
|
|
if (update_time()) {
|
|
bootMsg("Network Time Update: OK", text_y);
|
|
} else {
|
|
bootMsg("Network Time Update: FAIL", text_y);
|
|
}
|
|
text_y += line_height;
|
|
if (checkUrlPing(conf.haUrl)) { // FAILS FOR SOME REASON -- DEBUG ME
|
|
bootMsg("Home Assistant URL Reachable: OK", text_y);
|
|
text_y += line_height;
|
|
if (haAnybodyHome() < 0) {
|
|
bootMsg("Home Assistant Home Entity Check: FAIL", text_y);
|
|
activeFeatures.homeassistant = false;
|
|
} else {
|
|
activeFeatures.homeassistant = true;
|
|
bootMsg("Home Assistant Home Entity Check: OK", text_y);
|
|
}
|
|
text_y += line_height;
|
|
} else {
|
|
bootMsg("Home Assistant URL Reachable: FAIL", text_y);
|
|
text_y += line_height;
|
|
activeFeatures.homeassistant = false;
|
|
}
|
|
if (getWeather(conf.latitude, conf.longitude).latitude) {
|
|
bootMsg("Fetch Weather Data: OK", text_y);
|
|
activeFeatures.weather = true;
|
|
} else {
|
|
bootMsg("Fetch Weather Data: FAIL", text_y);
|
|
activeFeatures.weather = false;
|
|
}
|
|
text_y += line_height;
|
|
// Check Nextcloud reachable here
|
|
// Nextcloud dev check:
|
|
StartEnd timestamp = calculateStartEnd();
|
|
String cal_test = downloadICSFile(conf.calUrls[0], String(timestamp.start), String(timestamp.end), conf.calUser, conf.calKey);
|
|
Serial.println(cal_test);
|
|
displayActiveFeatures(activeFeatures);
|
|
bootMsg("Weather Icons sourced from flaticon.com", text_y);
|
|
text_y += 2 * line_height;
|
|
bootMsg("Starting main loop...", text_y);
|
|
}
|
|
|
|
void loop() {
|
|
// Loop Vars
|
|
WeatherData weather;
|
|
// CalendarData calendar;
|
|
|
|
display.setFullWindow(); // Set Full Update Mode
|
|
if (!activeFeatures.network ||
|
|
(activeFeatures.homeassistant && haAnybodyHome() < 1)) {
|
|
displayFullScreenArt(); // Show artwork if no network or nobody is home
|
|
} else {
|
|
struct tm timeinfo;
|
|
if (!getLocalTime(&timeinfo)) {
|
|
Serial.println("Unable to get time, some features will be buggy");
|
|
}
|
|
|
|
// Fetch weather data if it is active
|
|
if (activeFeatures.weather) {
|
|
weather = getWeather(conf.latitude, conf.longitude);
|
|
}
|
|
|
|
// Fetch calendar data if it is active
|
|
if (activeFeatures.calendar) {
|
|
// FETCH CALENDAR DATA HERE
|
|
}
|
|
|
|
Serial.printf("Loop iteration %d\n", loopIteration);
|
|
|
|
// Update the display
|
|
do {
|
|
display.fillScreen(GxEPD_WHITE);
|
|
display.fillRect(0, 0, 800, 80, GxEPD_BLACK); // Header background
|
|
displayHeaderText(timeinfo);
|
|
if (activeFeatures.weather && weather.latitude != 0 && weather.longitude != 0) {
|
|
displayWeather(weather);
|
|
}
|
|
if (!activeFeatures.calendar) { // Temporarily inverse the statenment during tests
|
|
displayCalendar(timeinfo);
|
|
}
|
|
} while (display.nextPage());
|
|
}
|
|
|
|
display.hibernate();
|
|
delay(3600000); // Sleep for 60 minutes
|
|
loopIteration++;
|
|
}
|
|
|
|
|
|
// Places the boot logo in position at the top of the screen
|
|
void bootLogo() {
|
|
constexpr int img_x = (800 - 648) / 2;
|
|
display.setFullWindow();
|
|
display.firstPage();
|
|
do
|
|
{
|
|
display.fillScreen(GxEPD_WHITE);
|
|
display.drawBitmap(img_x, 15, esphub_lrg, 648, 120, GxEPD_BLACK);
|
|
}
|
|
while (display.nextPage());
|
|
}
|
|
|
|
// Prints a one-line message to the screen, aligned with the boot logo at `y_pos`
|
|
void bootMsg(const char* msg, const uint16_t y_pos) {
|
|
constexpr uint16_t x_pos = 76; // Manually calculated based on values in bootLogo();
|
|
// Define the partial window area
|
|
display.setPartialWindow(0, 0, display.width(), display.height());
|
|
display.setFont(&FreeMono9pt7b); // Set font
|
|
display.setTextColor(GxEPD_BLACK); // Set color
|
|
display.setCursor(x_pos, y_pos); // Set the cursor for printing
|
|
display.print(msg); // Print the message
|
|
display.nextPage(); // Only needed for e-ink paginated updates
|
|
}
|
|
|
|
void displayActiveFeatures(const ActiveFeatures activeFeatures) {
|
|
constexpr uint16_t x_pos = 650; // X Position
|
|
uint16_t y_pos = 180; // Y Position
|
|
uint16_t line_height = 17;
|
|
display.setPartialWindow(0, 0, display.width(), display.height());
|
|
display.setFont(&FreeSans9pt7b); // Set font
|
|
display.setTextColor(GxEPD_WHITE); // Set color
|
|
//display.firstPage();
|
|
do {
|
|
// Convert booleans to "Y"/"N"
|
|
auto boolToYN = [](bool value) -> const char* { return value ? "Y" : "N"; };
|
|
|
|
display.fillRect(x_pos - line_height, y_pos - line_height, 170, 7 * line_height, GxEPD_BLACK);
|
|
|
|
display.setCursor(x_pos, y_pos);
|
|
display.print("Enabled Features\n");
|
|
|
|
display.setCursor(x_pos, y_pos + 2 * line_height);
|
|
display.print("Network: ");
|
|
display.print(boolToYN(activeFeatures.network));
|
|
|
|
display.setCursor(x_pos, y_pos + 3 * line_height);
|
|
display.print("HASS: ");
|
|
display.print(boolToYN(activeFeatures.homeassistant));
|
|
|
|
display.setCursor(x_pos, y_pos + 4 * line_height);
|
|
display.print("Calendar: ");
|
|
display.print(boolToYN(activeFeatures.calendar));
|
|
|
|
display.setCursor(x_pos, y_pos + 5 * line_height);
|
|
display.print("Weather: ");
|
|
display.print(boolToYN(activeFeatures.weather));
|
|
|
|
}
|
|
while (display.nextPage());
|
|
}
|
|
|
|
bool connect_WiFi(String ssid, String pass) {
|
|
Serial.print("Connecting to SSID ");
|
|
Serial.println(ssid);
|
|
WiFi.begin(ssid, pass);
|
|
int retries = 10;
|
|
while (WiFi.status() != WL_CONNECTED && retries > 0) {
|
|
delay(1000);
|
|
retries --;
|
|
}
|
|
if (WiFi.status() != WL_CONNECTED) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool update_time() {
|
|
configTime(0, 0, "time.fjla.net", "pool.ntp.org");
|
|
|
|
struct tm timeinfo;
|
|
if (!getLocalTime(&timeinfo, 20000)) {
|
|
Serial.println("Failed to obtain time");
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool checkUrlPing(const String& url) {
|
|
WiFiClientSecure *client = new WiFiClientSecure;
|
|
if (client) {
|
|
client -> setCACert(letsencrypt_root_ca);
|
|
{
|
|
HTTPClient https;
|
|
Serial.println("Starting HTTPS Request");
|
|
if (https.begin(*client, url)) {
|
|
int httpCode = https.GET();
|
|
Serial.printf("HTTPS GET code: %d\n", httpCode);
|
|
|
|
if (httpCode > 0) {
|
|
if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY) {
|
|
https.end();
|
|
delete client;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
https.end();
|
|
delete client;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
float haAnybodyHome() {
|
|
WiFiClientSecure *client = new WiFiClientSecure;
|
|
if (client) {
|
|
client -> setCACert(letsencrypt_root_ca);
|
|
|
|
String url = conf.haUrl + "/api/states/" + conf.homeEntity;
|
|
Serial.println("URL: " + url);
|
|
HTTPClient https;
|
|
|
|
Serial.println("Starting HTTPS Request to Home Assistant");
|
|
if (https.begin(*client, url)) {
|
|
https.addHeader("Authorization", "Bearer " + conf.haKey);
|
|
int httpCode = https.GET();
|
|
Serial.printf("Home Assistant GET Code: %d\n", httpCode);
|
|
|
|
if (httpCode == HTTP_CODE_OK) {
|
|
String payload = https.getString();
|
|
DynamicJsonDocument doc(1024);
|
|
|
|
DeserializationError error = deserializeJson(doc, payload);
|
|
if (error) {
|
|
Serial.println("Failed to Parse JSON Response");
|
|
https.end();
|
|
delete client;
|
|
return -0.1;
|
|
}
|
|
|
|
float state = doc["state"].as<float>();
|
|
Serial.printf("Entity %s state: %f\n", conf.homeEntity.c_str(), state);
|
|
|
|
https.end();
|
|
delete client;
|
|
return state;
|
|
}
|
|
return -0.2;
|
|
}
|
|
}
|
|
}
|
|
|
|
String getGreeting(struct tm timeinfo) {
|
|
if (!getLocalTime(&timeinfo)) {
|
|
Serial.println("Failed to obtain time from RTC");
|
|
return "Hello";
|
|
}
|
|
int currentHour = timeinfo.tm_hour; // Get the current hour
|
|
|
|
if (currentHour >= 5 && currentHour < 12) {
|
|
return "Good morning";
|
|
} else if (currentHour >= 12 && currentHour < 18) {
|
|
return "Good afternoon";
|
|
} else if (currentHour >= 18 && currentHour < 22) {
|
|
return "Good evening";
|
|
} else {
|
|
return "Good night";
|
|
}
|
|
}
|
|
|
|
String getDate(struct tm timeinfo) {
|
|
char dateStr[11];
|
|
strftime(dateStr, sizeof(dateStr), "%d/%m/%Y", &timeinfo);
|
|
|
|
return String(dateStr); // Convert the C string to a String object and return it
|
|
}
|
|
|
|
String getDayOfWeek(struct tm timeinfo) {
|
|
char dayStr[10];
|
|
strftime(dayStr, sizeof(dayStr), "%A", &timeinfo);
|
|
return String(dayStr);
|
|
}
|
|
|
|
void displayHeaderText(struct tm timeinfo) {
|
|
display.setTextColor(GxEPD_WHITE);
|
|
|
|
// Define vars for bound calculations
|
|
int16_t tbx, tby;
|
|
uint16_t tbw, tbh;
|
|
|
|
// Get Greeting and notable day
|
|
String greeting = getGreeting(timeinfo);
|
|
String notableDay = getNotableDay(timeinfo.tm_mon + 1, timeinfo.tm_mday);
|
|
String date = getDate(timeinfo);
|
|
String dayOfWeek = getDayOfWeek(timeinfo);
|
|
|
|
display.setFont(&FreeSansBold24pt7b); // Set Greeting Font
|
|
|
|
// Calculate Bounds for Greeting
|
|
display.getTextBounds(greeting, 0, 0, &tbx, &tby, &tbw, &tbh);
|
|
uint16_t gx = (display.width() - tbw) / 2;
|
|
uint16_t gy = 42;
|
|
|
|
// Print Greeting
|
|
display.setCursor(gx, gy);
|
|
display.print(greeting);
|
|
|
|
display.setFont(&FreeMonoBold9pt7b); // Set Notable Day Font
|
|
|
|
// Calculate Bounds for Notable Day
|
|
display.getTextBounds(notableDay, 0, 0, &tbx, &tby, &tbw, &tbh);
|
|
uint16_t nx = (display.width() - tbw) / 2;
|
|
uint16_t ny = 70;
|
|
// Print Notable Day
|
|
display.setCursor(nx, ny);
|
|
display.print(notableDay);
|
|
|
|
// Print date
|
|
display.setCursor(680, 25);
|
|
display.print(date);
|
|
display.setCursor(680, 60);
|
|
display.print(dayOfWeek);
|
|
}
|
|
|
|
void displayWeather(WeatherData weather) {
|
|
display.setFont(&FreeMonoBold12pt7b);
|
|
display.setTextColor(GxEPD_WHITE);
|
|
|
|
// Display Current Temperature
|
|
display.setCursor(14, 23);
|
|
display.print(String((int)round(weather.current.temperature_2m)) + "°C");
|
|
display.setFont(&FreeMonoBold9pt7b); // Set font for remaining prints
|
|
|
|
// Display Max Temp
|
|
display.drawBitmap(5, 27, hi, 22, 22, GxEPD_WHITE); // Draw 'max temp' bitmap
|
|
display.setCursor(23, 44);
|
|
display.print(String((int)round(weather.daily.temperature_2m_max)) + "°C");
|
|
|
|
// Display Min Temp
|
|
display.drawBitmap(5, 50, lo, 22, 22, GxEPD_WHITE); // Draw 'min temp' bitmap
|
|
display.setCursor(23, 64);
|
|
display.print(String((int)round(weather.daily.temperature_2m_min)) + "°C");
|
|
|
|
// Calculate the width of the longest temperature string & adjust x-position of weather bitmap and display
|
|
int longestTempWidth = 0;
|
|
int tempMaxWidth = String((int)round(weather.daily.temperature_2m_max)).length();
|
|
int tempMinWidth = String((int)round(weather.daily.temperature_2m_min)).length();
|
|
longestTempWidth = max(tempMaxWidth, tempMinWidth);
|
|
int weather_x = 58; // Default position
|
|
if (longestTempWidth > 2) {
|
|
weather_x += (longestTempWidth > 3 ? 9 : 5); // 67 is the correct X value if more than three. =2 will be halfway btwn Default and 67.
|
|
}
|
|
display.drawBitmap(weather_x, 8, getWeatherBitmap(weather.daily.weather_code), 64, 64, GxEPD_WHITE); // Draw Weather Bitmap
|
|
|
|
// Display wind & gust speed
|
|
display.drawBitmap(126, 8, wind, 22, 22, GxEPD_WHITE); // Draw 'wind speed' bitmap
|
|
display.setCursor(150, 23);
|
|
display.print("km/h");
|
|
display.setCursor(133, 45);
|
|
display.print(String(weather.daily.wind_speed_10m_max));
|
|
display.setCursor(133, 64);
|
|
display.print(String(weather.daily.wind_gusts_10m_max));
|
|
}
|
|
|
|
void displayFullScreenArt() {
|
|
display.setFullWindow();
|
|
int randomIdx = random(0, full_art_array_size);
|
|
display.firstPage();
|
|
do
|
|
{
|
|
display.fillScreen(GxEPD_WHITE);
|
|
display.drawBitmap(0, 0, full_art_array[randomIdx], 800, 480, GxEPD_BLACK);
|
|
}
|
|
while (display.nextPage());
|
|
}
|
|
|
|
void displayCalendar(struct tm timeinfo) {
|
|
const int cal_y = 80;
|
|
const int cal_head = 30;
|
|
const int cal_margin = 30;
|
|
const char* weekDays[] = {"S", "M", "T", "W", "T", "F", "S"};
|
|
const char* calNames[] = {"Shared", "Fred", "Jade", "Lucy", "Ava-Rose"};
|
|
|
|
// Draw Calendar Lines
|
|
display.drawLine(0, cal_y + cal_head, 480, cal_y + cal_head, GxEPD_BLACK); // Bottom of Header
|
|
for (int y = cal_y + cal_head; y <= 480; y += 74) { // Loop to separate into five rows
|
|
display.drawLine(0, y, 800, y, GxEPD_BLACK);
|
|
}
|
|
display.drawLine(cal_margin, cal_y, cal_margin, 480, GxEPD_BLACK); // Right of margin
|
|
for (int x = cal_margin; x <= 800; x += 154) { // Loop to separate into seven columns
|
|
display.drawLine(x, cal_y, x, 480, GxEPD_BLACK);
|
|
}
|
|
|
|
// Print Calendar Labels
|
|
display.setFont(&FreeMonoBold12pt7b);
|
|
display.setTextColor(GxEPD_BLACK);
|
|
for (int i = 0; i < 5; i++) {
|
|
display.setCursor(cal_margin + i * 154 + 10, cal_y + 23);
|
|
display.print(calNames[i]);
|
|
}
|
|
|
|
// Print Day/Date
|
|
for (int i = 0; i < 5; i++) {
|
|
int dayOfWk = (timeinfo.tm_wday + i) % 7;
|
|
display.setCursor(3, cal_head + i * 74 + 96);
|
|
display.print(weekDays[dayOfWk]);
|
|
}
|
|
} |