From e1d608356423082fe21bc1b3152e5dc11500d6bd Mon Sep 17 00:00:00 2001 From: Chris Twomey Date: Sat, 1 Jul 2023 21:59:24 +0100 Subject: [PATCH] Server dictated client refresh times --- .vscode/settings.json | 20 - README.md | 4 +- doc/config.yaml | 33 ++ include/README | 39 -- include/battery.h | 16 + include/defaults.h | 43 ++ include/display_utils.h | 60 +++ include/error_utils.h | 12 + include/file_utils.h | 20 + .../font}/Merienda_Regular12pt7b.h | 2 + .../font}/Merienda_Regular16pt7b.h | 2 + src/battery.h => include/icon/icons_32x32.h | 53 +- include/log_utils.h | 67 +++ include/network_utils.h | 29 ++ include/sleep_utils.h | 25 + include/time_utils.h | 44 ++ lib/README | 46 -- platformio.ini | 8 +- server/config.yaml | 4 + server/server.py | 46 +- src/CMakeLists.txt | 2 +- src/battery.cpp | 49 ++ src/config.h | 28 -- src/defaults.cpp | 43 ++ src/display_utils.cpp | 161 ++++++ src/file_utils.cpp | 43 ++ src/lib.cpp | 472 ------------------ src/lib.h | 221 -------- src/log_utils.cpp | 158 ++++++ src/main.cpp | 113 +++-- src/network_utils.cpp | 129 +++++ src/sleep_utils.cpp | 47 ++ src/time_utils.cpp | 90 ++++ 33 files changed, 1191 insertions(+), 938 deletions(-) delete mode 100644 .vscode/settings.json create mode 100644 doc/config.yaml delete mode 100644 include/README create mode 100644 include/battery.h create mode 100644 include/defaults.h create mode 100644 include/display_utils.h create mode 100644 include/error_utils.h create mode 100644 include/file_utils.h rename {src => include/font}/Merienda_Regular12pt7b.h (99%) rename {src => include/font}/Merienda_Regular16pt7b.h (99%) rename src/battery.h => include/icon/icons_32x32.h (92%) create mode 100644 include/log_utils.h create mode 100644 include/network_utils.h create mode 100644 include/sleep_utils.h create mode 100644 include/time_utils.h delete mode 100644 lib/README create mode 100644 src/battery.cpp delete mode 100644 src/config.h create mode 100644 src/defaults.cpp create mode 100644 src/display_utils.cpp create mode 100644 src/file_utils.cpp delete mode 100644 src/lib.cpp delete mode 100644 src/lib.h create mode 100644 src/log_utils.cpp create mode 100644 src/network_utils.cpp create mode 100644 src/sleep_utils.cpp create mode 100644 src/time_utils.cpp diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 923a2d7..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "cmake.sourceDirectory": "${workspaceFolder}/src", - "files.associations": { - "cmath": "cpp", - "array": "cpp", - "deque": "cpp", - "string": "cpp", - "unordered_map": "cpp", - "unordered_set": "cpp", - "vector": "cpp", - "string_view": "cpp", - "initializer_list": "cpp", - "random": "cpp", - "*.tcc": "cpp", - "fstream": "cpp", - "iosfwd": "cpp", - "utility": "cpp", - "sstream": "cpp" - } -} \ No newline at end of file diff --git a/README.md b/README.md index f7c96a2..e9d03db 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ See the [server/README.md](server/README.md) for more features. - **Optional: 2 GB microSD card ~€5** - **Note: microSD cards are now no longer required and disabled by default. Use build flag `HAS_SDCARD` to re-enable** + **Note: microSD cards are now no longer required and disabled by default. Use build flag `USE_SDCARD` to re-enable** Whatever is the cheapest microSD card you can find, you will not likely need more than few hundred kilobytes of storage. It will be mainly used by Inkplate to cache downloaded images from the server until it needs to refresh the next day. The config file for the code will also need to be stored here. @@ -130,7 +130,7 @@ Make sure to update: **Note: The new/current _SolderedElectronics_ version of Inkplate10 has a MOSFET to control power to the SD card during deep sleep, making this option viable for battery life. https://github.com/SolderedElectronics/Inkplate-Arduino-library/issues/209** -**Note: Use build flag `HAS_SDCARD` to enable SD card usage.** +**Note: Use build flag `USE_SDCARD` to enable SD card usage.** Insert an SD card into your Inkplate board and place a new file `config.yaml` in the root directory: diff --git a/doc/config.yaml b/doc/config.yaml new file mode 100644 index 0000000..607c54f --- /dev/null +++ b/doc/config.yaml @@ -0,0 +1,33 @@ +# This is an example config.yaml to place at the root directory +# of your SD card. The values in these parameters are just examples +# so be sure to update them to suit your own setup. +# +# These parameters override what is set in config.h if SD card is enabled. +server: + # The URL on the server which the client will attempt to download the first image from. + url: http://localhost:8080/download.png + # The number of times to attempt downloading or drawing the server image. + retries: 3 +wifi: + ssid: XXXX + pass: XXXX + # The number of times to attempt WiFi connection before timeout. + retries: 6 +ntp: + # The time server host (keep as pool.ntp.org if in doubt). + host: pool.ntp.org + # The timezone you live in ("Olson" format). + timezone: Europe/Dublin +mqtt_logger: + # Set to true to send publish logs to an MQTT broker. + enabled: false + # The MQTT broker to publish logs to. + broker: localhost + # The port of the MQTT broker. + port: 1883 + # The unique identifier for this project in your MQTT broker. + clientId: inkplate10-weather-calendar + # The name of the MQTT topic to publish to. + topic: mqtt/inkplate10-weather-calendar + # The number of times to attempt MQTT connection before timeout. + retries: 3 diff --git a/include/README b/include/README deleted file mode 100644 index 194dcd4..0000000 --- a/include/README +++ /dev/null @@ -1,39 +0,0 @@ - -This directory is intended for project header files. - -A header file is a file containing C declarations and macro definitions -to be shared between several project source files. You request the use of a -header file in your project source file (C, C++, etc) located in `src` folder -by including it, with the C preprocessing directive `#include'. - -```src/main.c - -#include "header.h" - -int main (void) -{ - ... -} -``` - -Including a header file produces the same results as copying the header file -into each source file that needs it. Such copying would be time-consuming -and error-prone. With a header file, the related declarations appear -in only one place. If they need to be changed, they can be changed in one -place, and programs that include the header file will automatically use the -new version when next recompiled. The header file eliminates the labor of -finding and changing all the copies as well as the risk that a failure to -find one copy will result in inconsistencies within a program. - -In C, the usual convention is to give header files names that end with `.h'. -It is most portable to use only letters, digits, dashes, and underscores in -header file names, and at most one dot. - -Read more about using header files in official GCC documentation: - -* Include Syntax -* Include Operation -* Once-Only Headers -* Computed Includes - -https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html diff --git a/include/battery.h b/include/battery.h new file mode 100644 index 0000000..a2f4b7d --- /dev/null +++ b/include/battery.h @@ -0,0 +1,16 @@ +#ifndef __BATTERY_H__ +#define __BATTERY_H__ +// Define a battery capacity lookup table as an array of structs +struct BatteryCapacity { + double voltage; + int percentage; +}; + +/** + Look up battery capacity percentage based on voltage. + + @param voltage the current voltage of the battery. + @returns the capacity remaining as a percentage integer. +*/ +int getBatteryCapacity(double voltage); +#endif \ No newline at end of file diff --git a/include/defaults.h b/include/defaults.h new file mode 100644 index 0000000..706ba16 --- /dev/null +++ b/include/defaults.h @@ -0,0 +1,43 @@ +#ifndef __DEFAULTS_H__ +#define __DEFAULTS_H__ +/** + * Manually define config params. + * + * Only use this if you are not using the SD card (Inkplate10 V1). + * Otherwise add USE_SDCARD flag to load from SD card config.yaml + * + * These parameters are overriden by the config.yaml if SD card is enabled. + */ + +// The URL on the server which the client will try to download the first +// image from. +extern char serverURL[]; +// The number of times to attempt downloading or drawing the server image. +extern int serverRetries; + +// Wifi config. +extern char wifiSSID[]; +extern char wifiPass[]; +// The number of times to attempt WiFi connection before timeout. +extern int wifiRetries; + +// NTP config. +// The time server (keep as pool.ntp.org if in doubt). +extern char ntpHost[]; +// The timezone you live in ("Olson" format). +extern char ntpTimezone[]; + +// Remote logging config. +// Set to true to send publish logs to an MQTT broker. +extern bool mqttLoggerEnabled; +// The MQTT broker to publish logs to. +extern char mqttLoggerBroker[]; +// The port of the MQTT broker. +extern int mqttLoggerPort; +// The unique identifier for this project in your MQTT broker. +extern char mqttLoggerClientID[]; +// The name of the MQTT topic to publish to. +extern char mqttLoggerTopic[]; +// The number of times to attempt MQTT connection before timeout. +extern int mqttLoggerRetries; +#endif \ No newline at end of file diff --git a/include/display_utils.h b/include/display_utils.h new file mode 100644 index 0000000..efd2b38 --- /dev/null +++ b/include/display_utils.h @@ -0,0 +1,60 @@ +#ifndef __DISPLAY_H__ +#define __DISPLAY_H__ +#include "error_utils.h" + +// Guestimate file size for PNG image @ 1200x825 +#define DEFAULT_BUFFER_SIZE E_INK_WIDTH* E_INK_HEIGHT * 4 + 100 + +/** + Load an image to the display buffer. + + @param filePath the path of the file on disk. + error. + @returns the esp_err_t code: + - ESP_OK if successful. + - ESP_ERR_EFILER if writing file to filePath fails. +*/ +esp_err_t loadImage(const char* filePath); + +/** + Load a PNG image to the display buffer from a data buffer. + + @param buf the data buffer of png. + @param len the size of buffer. + @returns the esp_err_t code: + - ESP_OK if successful. + - ESP_ERR_EDL if download file fails. + - ESP_ERR_EFILEW if writing file to filePath fails. +*/ +esp_err_t loadImage(uint8_t* buf, int32_t len); + +/** + Load a BMP image to the display buffer. + + @param buf the byte array data + @returns the esp_err_t code: + - ESP_OK if successful. + - ESP_ERR_EDL if download file fails. + - ESP_ERR_EFILEW if writing file to filePath fails. +*/ +esp_err_t loadImage(uint8_t* buf, int x, int y, int w, int h); + +/** + Draw the battery status to the display. + + @param batteryRemainingPercent the percentage capacity remaining in the + battery. error. + @param invert flag to invert battery status due to black banner. +*/ +void displayBatteryStatus(int batteryRemainingPercent, bool invert); + +/** + Draw an message to the display. The error message is drawn in the top-left + corner of the display. Error message will overlay previously drawn image. + + @param msg the message to display. + @param batteryRemainingPercent the percentage remaining battery capacity for + battery status display. error. +*/ +void displayMessage(const char* msg, int batteryRemainingPercent); +#endif \ No newline at end of file diff --git a/include/error_utils.h b/include/error_utils.h new file mode 100644 index 0000000..47866fe --- /dev/null +++ b/include/error_utils.h @@ -0,0 +1,12 @@ +#ifndef __ERROR_H__ +#define __ERROR_H__ +#include + +// Enum of errors that might be encountered. +#define ESP_ERR_ERRNO_BASE (0) +#define ESP_ERR_EDL (1 + ESP_ERR_ERRNO_BASE) // Download error +#define ESP_ERR_EDRAW (2 + ESP_ERR_ERRNO_BASE) // Draw error +#define ESP_ERR_EFILEW (3 + ESP_ERR_ERRNO_BASE) // File write error +#define ESP_ERR_EFILER (4 + ESP_ERR_ERRNO_BASE) // File read error +#define ESP_ERR_ENTP (5 + ESP_ERR_ERRNO_BASE) // NTP error +#endif \ No newline at end of file diff --git a/include/file_utils.h b/include/file_utils.h new file mode 100644 index 0000000..300c832 --- /dev/null +++ b/include/file_utils.h @@ -0,0 +1,20 @@ +#ifndef __FILE_H__ +#define __FILE_H__ +#include "error_utils.h" +// The path on SD card where calendar images are downloaded to and read from. +#define CALENDAR_RW_PATH "/calendar.png" +// The file path on SD card to load config. +#define CONFIG_FILE_PATH "/config.yaml" + +/** + Write a data buffer a file at a given path. Store the file on disk at a given path. + + @param buf the data buffer. + @param size the size of the file to write. + @param filePath the path of the file on disk. + @returns the esp_err_t code: + - ESP_OK if successful. + - ESP_ERR_EFILEW if number of retries is exceeded without success. +*/ +esp_err_t writeFile(uint8_t* buf, size_t size, const char* filePath); +#endif \ No newline at end of file diff --git a/src/Merienda_Regular12pt7b.h b/include/font/Merienda_Regular12pt7b.h similarity index 99% rename from src/Merienda_Regular12pt7b.h rename to include/font/Merienda_Regular12pt7b.h index 4d24545..a6d2134 100644 --- a/src/Merienda_Regular12pt7b.h +++ b/include/font/Merienda_Regular12pt7b.h @@ -1,3 +1,5 @@ +#include + const uint8_t Merienda_Regular12pt7bBitmaps[] PROGMEM = { 0x00, 0x31, 0x8E, 0x63, 0x19, 0xCC, 0x63, 0x18, 0xC6, 0x10, 0x00, 0xC7, 0x38, 0x46, 0xCD, 0x9E, 0x3C, 0xD9, 0x91, 0x00, 0x01, 0x83, 0x00, 0x60, diff --git a/src/Merienda_Regular16pt7b.h b/include/font/Merienda_Regular16pt7b.h similarity index 99% rename from src/Merienda_Regular16pt7b.h rename to include/font/Merienda_Regular16pt7b.h index 307bfc4..1fe2b29 100644 --- a/src/Merienda_Regular16pt7b.h +++ b/include/font/Merienda_Regular16pt7b.h @@ -1,3 +1,5 @@ +#include + const uint8_t Merienda_Regular16pt7bBitmaps[] PROGMEM = { 0x00, 0x10, 0x38, 0x70, 0xF1, 0xC3, 0x87, 0x0E, 0x38, 0x70, 0xE1, 0xC3, 0x86, 0x0C, 0x18, 0x30, 0x30, 0x00, 0x00, 0x04, 0x1C, 0x78, 0xF0, 0x42, diff --git a/src/battery.h b/include/icon/icons_32x32.h similarity index 92% rename from src/battery.h rename to include/icon/icons_32x32.h index 811cbc9..ecc3774 100644 --- a/src/battery.h +++ b/include/icon/icons_32x32.h @@ -1,53 +1,6 @@ -#ifndef BATTERY_H -#define BATTERY_H - -// Define a battery capacity lookup table as an array of structs -struct BatteryCapacity { - double voltage; - int percentage; -}; - -#ifdef BATT_2000MAH -// A capacity table based on a 3.7v 2000mAh LiPo discharge profile over ~50 -// days. -BatteryCapacity capacityTable[] = { - {4.25, 100}, {4.22, 99}, {4.19, 98}, {4.17, 97}, {4.15, 96}, {4.14, 95}, - {4.12, 94}, {4.11, 93}, {4.10, 91}, {4.09, 90}, {4.08, 89}, {4.08, 88}, - {4.08, 87}, {4.08, 86}, {4.07, 85}, {4.07, 84}, {4.07, 83}, {4.07, 82}, - {4.06, 81}, {4.06, 80}, {4.05, 79}, {4.04, 78}, {4.03, 77}, {4.02, 76}, - {4.00, 74}, {3.99, 73}, {3.98, 72}, {3.97, 71}, {3.96, 70}, {3.96, 69}, - {3.95, 68}, {3.95, 67}, {3.94, 66}, {3.94, 65}, {3.93, 64}, {3.93, 63}, - {3.92, 62}, {3.91, 61}, {3.90, 60}, {3.89, 59}, {3.87, 57}, {3.86, 56}, - {3.85, 55}, {3.84, 54}, {3.83, 53}, {3.82, 52}, {3.80, 51}, {3.79, 50}, - {3.78, 49}, {3.77, 48}, {3.76, 47}, {3.75, 46}, {3.74, 45}, {3.73, 44}, - {3.72, 43}, {3.71, 41}, {3.70, 40}, {3.70, 39}, {3.69, 38}, {3.69, 37}, - {3.68, 36}, {3.68, 35}, {3.67, 34}, {3.66, 33}, {3.65, 32}, {3.65, 31}, - {3.64, 30}, {3.63, 29}, {3.62, 28}, {3.62, 27}, {3.62, 26}, {3.61, 24}, - {3.60, 23}, {3.59, 22}, {3.57, 21}, {3.56, 20}, {3.54, 19}, {3.53, 18}, - {3.51, 17}, {3.51, 16}, {3.50, 15}, {3.49, 14}, {3.48, 13}, {3.47, 12}, - {3.45, 11}, {3.40, 10}, {3.34, 9}, {3.33, 7}, {3.31, 6}, {3.29, 5}, - {3.26, 4}, {3.24, 3}, {3.21, 2}, {3.16, 1}, {3.10, 0}, -}; -#elif -extern BatteryCapacity capacityTable[]; -#endif - -const int numCapacityEntries = sizeof(capacityTable) / sizeof(BatteryCapacity); - -/** - Look up battery capacity percentage based on voltage. - - @param voltage the current voltage of the battery. - @returns the capacity remaining as a percentage integer. -*/ -int getBatteryCapacity(double voltage) { - for (int i = 0; i < numCapacityEntries; i++) { - if (voltage >= capacityTable[i].voltage) { - return capacityTable[i].percentage; - } - } - return 0; -} +#ifndef ICONS_32X32_H +#define ICONS_32X32_H +#include // 'battery-empty', 32x32px uint8_t epdBitmapBatteryEmpty[] PROGMEM = { diff --git a/include/log_utils.h b/include/log_utils.h new file mode 100644 index 0000000..c5e5932 --- /dev/null +++ b/include/log_utils.h @@ -0,0 +1,67 @@ +#ifndef __LOG_H__ +#define __LOG_H__ +#include "error_utils.h" +#include "time_utils.h" + +// Enum of log verbosity levels. +#define LOG_CRIT 0 +#define LOG_ERROR 1 +#define LOG_WARNING 2 +#define LOG_NOTICE 3 +#define LOG_INFO 4 +#define LOG_DEBUG 5 +#ifndef LOG_LEVEL +// Debug logging by default. +#define LOG_LEVEL LOG_DEBUG +#endif + +// log message entry history size +#define LOG_QUEUE_MAX_ENTRIES 10 + +/** + Connect to a MQTT broker for remote logging. + + @param broker the hostname of the MQTT broker. + @param port the port of the MQTT broker. + @param topic the topic to publish logs to. + @param clientID the name of the logger client to appear as. + @param max_retries the number of connection attempts to make before fallback + to serial-only logging. + @returns the esp_err_t code: + - ESP_OK if successful. + - ESP_ERR_TIMEOUT if number of retries is exceeded without success. +*/ +esp_err_t configureMQTT(const char* broker, int port, const char* topic, + const char* clientID, int max_retries); + +/** + Log a message. + + @param pri the log level / priority of the message, see LOG_LEVEL. + @param msg the message to log. +*/ +void log(uint16_t pri, const char* msg); + +/** + Log a message with formatting. + + @param pri the log level / priority of the message, see LOG_LEVEL. + @param fmt the format of the log message +*/ +void logf(uint16_t pri, const char* fmt, ...); + +/** + Converts a priority into a log level prefix. + + @param pri the log level / priority of the message, see LOG_LEVEL. + @returns the string value of the priority. +*/ +const char* msgPrefix(uint16_t pri); + +/** + Ensure log queue is populated/emptied based on MQTT connection. + + @param msg the log message +*/ +void ensureQueue(char* msg); +#endif \ No newline at end of file diff --git a/include/network_utils.h b/include/network_utils.h new file mode 100644 index 0000000..ea2349d --- /dev/null +++ b/include/network_utils.h @@ -0,0 +1,29 @@ +#ifndef __NETWORK_H__ +#define __NETWORK_H__ +#include "error_utils.h" +/** + Connect to a WiFi network in Station Mode. + + @param ssid the network SSID. + @param pass the network password. + @param retries the number of connection attempts to make before returning an + error. + @returns the esp_err_t code: + - ESP_OK if successful. + - ESP_ERR_TIMEOUT if number of retries is exceeded without success. +*/ +esp_err_t configureWiFi(const char* ssid, const char* pass, int retries); + +/** + Download a file at a given URL. Store the file on disk at a given path. + + @param url the URL of the file to download. + @param size the size of the file to download. + @param retries the number of download attempts to make before returning an + error. + @returns the esp_err_t code: + - ESP_OK if successful. + - ESP_ERR_TIMEOUT if number of retries is exceeded without success. +*/ +uint8_t* downloadFile(const char* url, char* nextRefresh, int32_t* size); +#endif \ No newline at end of file diff --git a/include/sleep_utils.h b/include/sleep_utils.h new file mode 100644 index 0000000..e04ce71 --- /dev/null +++ b/include/sleep_utils.h @@ -0,0 +1,25 @@ +#ifndef __SLEEP_H__ +#define __SLEEP_H__ +#include "time_utils.h" +/** + Enter deep sleep. + + @param refreshTime the time of the day to wake in HH:MM:SS format (eg. + 09:00:00). error. +*/ +void sleep(const char* refreshTime); + +/** + Enter deep sleep. + + @param targetWakeTime the target timestamp to wake up at. +*/ +void sleep(time_t targetWakeTime); + +void sleep(int targetWakeTime); + +/** + Enter deep sleep. +*/ +void deepSleep(); +#endif \ No newline at end of file diff --git a/include/time_utils.h b/include/time_utils.h new file mode 100644 index 0000000..d169b6a --- /dev/null +++ b/include/time_utils.h @@ -0,0 +1,44 @@ +#ifndef __TIME_UTILS_H__ +#define __TIME_UTILS_H__ +#include +#include "error_utils.h" + +#define CalendarYrToTm(Y) ((Y)-1970) +#define SECONDS_IN_DAY 86400 +#define SECONDS_IN_YEAR SECONDS_IN_DAY * 365 + +// Fallback time to refresh. +#define FALLBACK_REFRESH_TIME "09:00:00" +// The number of seconds to sleep if RTC not configured correctly. +#define FALLBACK_SLEEP_SECONDS 120 + +/** + * Return a RFC3339 formatted string of the current time. + * + * @return String the RFC3339 formatted string of the current time. + */ +String nowTzFmt(); + +/** + Connect to an NTP server and synchronize the on-board real-time clock. + + @param ntpHost the hostname of the NTP server (eg. pool.ntp.org). + @param timezoneName the name of the timezone in Olson format (eg. + Europe/Dublin) + @returns the esp_err_t code: + - ESP_OK if successful. + - ESP_ERR_ENTP if updating the NTP client fails. +*/ +esp_err_t configureTime(const char* ntpHost, const char* timezoneName); + +/** + Get the next scheduled time to wake from deep sleep. + + @param refreshTime the time of the day to wake in HH::MM:SS format (eg. + 09:00:00). error. + @returns the epoch time of when to wake. + If the real-time clock is not configured, it will return the last configured + RTC epoch time + FALLBACK_SLEEP_SECONDS. +*/ +time_t getWakeTime(const char* refreshTime); +#endif \ No newline at end of file diff --git a/lib/README b/lib/README deleted file mode 100644 index 6debab1..0000000 --- a/lib/README +++ /dev/null @@ -1,46 +0,0 @@ - -This directory is intended for project specific (private) libraries. -PlatformIO will compile them to static libraries and link into executable file. - -The source code of each library should be placed in a an own separate directory -("lib/your_library_name/[here are source files]"). - -For example, see a structure of the following two libraries `Foo` and `Bar`: - -|--lib -| | -| |--Bar -| | |--docs -| | |--examples -| | |--src -| | |- Bar.c -| | |- Bar.h -| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html -| | -| |--Foo -| | |- Foo.c -| | |- Foo.h -| | -| |- README --> THIS FILE -| -|- platformio.ini -|--src - |- main.c - -and a contents of `src/main.c`: -``` -#include -#include - -int main (void) -{ - ... -} - -``` - -PlatformIO Library Dependency Finder will find automatically dependent -libraries scanning project source files. - -More information about PlatformIO Library Dependency Finder -- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/platformio.ini b/platformio.ini index b9e7cb9..5c3776a 100644 --- a/platformio.ini +++ b/platformio.ini @@ -18,7 +18,7 @@ lib_deps = androbi/MqttLogger@^0.2.3 knolleary/PubSubClient@^2.8 tobozo/YAMLDuino @ ^1.4.0 - e-radionicacom/InkplateLibrary @ ^8.0.0 + https://github.com/chrisjtwomey/Inkplate-Arduino-library.git#draw-from-png-buf https://github.com/bblanchon/ArduinoStreamUtils bblanchon/ArduinoJson @ ^6.21.2 ropg/ezTime @ ^0.8.3 @@ -36,10 +36,9 @@ build_flags = -DHAS_ARDUINOJSON -DYAML_DISABLE_CJSON -mfix-esp32-psram-cache-issue - -DBATT_2000MAH # uncomment below if you want to use an SD card # WARNING: high power consumption on Inkplate10 V1 - # -DHAS_SDCARD + # -DUSE_SDCARD -DLOG_LEVEL=5 -DCORE_DEBUG_LEVEL=4 @@ -54,9 +53,8 @@ build_flags = -DHAS_ARDUINOJSON -DYAML_DISABLE_CJSON -mfix-esp32-psram-cache-issue - -DBATT_2000MAH # uncomment below if you want to use an SD card # WARNING: high power consumption on Inkplate10 V1 - # -DHAS_SDCARD + # -DUSE_SDCARD -DLOG_LEVEL=4 -DCORE_DEBUG_LEVEL=0 \ No newline at end of file diff --git a/server/config.yaml b/server/config.yaml index 0442e7b..bd5452e 100644 --- a/server/config.yaml +++ b/server/config.yaml @@ -4,6 +4,10 @@ server: port: 8080 aliveSeconds: 60 maxServes: 1 + refresh_times: + - "09:00:00" + - "15:00:00" + - "21:00:00" weather: service: accuweather apikey: XXXX diff --git a/server/server.py b/server/server.py index 8d6d319..8609e45 100644 --- a/server/server.py +++ b/server/server.py @@ -14,7 +14,8 @@ from views.calendar import CalendarPage from google.api import GoogleAPIService from werkzeug.serving import make_server -from flask import Flask, send_file, abort +from flask import Flask, make_response, send_file, abort +from datetime import datetime, timedelta cwd = os.path.dirname(os.path.realpath(__file__)) log = None @@ -23,10 +24,11 @@ # number of times served server_num_serves = 0 server_max_serves = 1 +server_refresh_times = [] def main(): - global log, server_max_serves + global log, server_max_serves, server_refresh_times config_file = open(os.path.join(cwd, "config.yaml")) config = yaml.safe_load(config_file) @@ -68,6 +70,7 @@ def main(): config, "server", "aliveSeconds", default=60 ) server_max_serves = get_prop_by_keys(config, "server", "maxServes", default=1) + server_refresh_times = get_prop_by_keys(config, "server", "refresh_times", default=["09:00:00"]) image_width = get_prop_by_keys(config, "image", "width", default=825) image_height = get_prop_by_keys(config, "image", "height", default=1200) @@ -144,8 +147,8 @@ def main(): start_wait_dt = dt.datetime.now() diff = dt.datetime.now() - start_wait_dt while True: - if (enable_max_serves and server_num_serves < server_max_serves) or ( - enable_wait and diff.seconds < server_alive_seconds): + if (enable_max_serves and server_num_serves >= server_max_serves) or ( + enable_wait and diff.seconds > server_alive_seconds): break time.sleep(1) @@ -198,6 +201,30 @@ def on_message(client, userdata, message): return None +def get_next_refresh_time(): + global server_refresh_times + + def get_timestamp(day, refresh_time): + dt = datetime.combine(day, datetime.strptime(refresh_time, '%H:%M:%S').time()) + # dt = dt.replace(tzinfo=timezone.utc) + return int(dt.timestamp()) + + now = int(datetime.now().timestamp()) + today = datetime.today() + + idx = 0 + ts = 0 + for refresh_time in server_refresh_times: + ts = get_timestamp(today, refresh_time) + if ts > now: + break + idx += 1 + + if idx >= len(server_refresh_times): + idx = 0 + + return server_refresh_times[idx] + class ServerThread(threading.Thread): def __init__(self, app, port, max_serves=1): @@ -217,8 +244,8 @@ def shutdown(self, timeout=60): self.server.shutdown() -@app.route("/calendar.png") -def serve_cal_png(): +@app.route("/download.png") +def serve_img_png(): global server_num_serves, server_max_serves """ Returns the calendar image directly through send_file @@ -238,12 +265,15 @@ def serve_cal_png(): if server_max_serves > 0: log.info(f"Served {server_num_serves}/{server_max_serves} times") - return send_file( + rsp = make_response(send_file( stream, mimetype="image/png", as_attachment=True, download_name=os.path.basename(path), - ) + )) + rsp.headers["X-Next-Refresh"] = get_next_refresh_time() + + return rsp if __name__ == "__main__": diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 483bc0c..40067ed 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -3,4 +3,4 @@ FILE(GLOB_RECURSE app_sources ${CMAKE_SOURCE_DIR}/src/*.*) -idf_component_register(SRCS ${app_sources}) +idf_component_register(SRCS ${app_sources}) \ No newline at end of file diff --git a/src/battery.cpp b/src/battery.cpp new file mode 100644 index 0000000..5ede370 --- /dev/null +++ b/src/battery.cpp @@ -0,0 +1,49 @@ +#include "battery.h" + +/** + * The discharge profile of a 3.7v 2000mAh Lithium-Polymer battery. + * + * Capacity tables are only ever a rough approximation based on recording + * voltages over time from fully charged to discharged. Profiles are + * + * The best way to improve accuracy is to record the discharge + * profile of your own battery and plug the data in here. I recorded this + * table by running this project until the voltage cut-off of 3.1v was reached + * and calculated percentage capacity based on the number of days it ran for. + * + */ +BatteryCapacity capacityTable[] = { + {4.25, 100}, {4.22, 99}, {4.19, 98}, {4.17, 97}, {4.15, 96}, {4.14, 95}, + {4.12, 94}, {4.11, 93}, {4.10, 91}, {4.09, 90}, {4.08, 89}, {4.08, 88}, + {4.08, 87}, {4.08, 86}, {4.07, 85}, {4.07, 84}, {4.07, 83}, {4.07, 82}, + {4.06, 81}, {4.06, 80}, {4.05, 79}, {4.04, 78}, {4.03, 77}, {4.02, 76}, + {4.00, 74}, {3.99, 73}, {3.98, 72}, {3.97, 71}, {3.96, 70}, {3.96, 69}, + {3.95, 68}, {3.95, 67}, {3.94, 66}, {3.94, 65}, {3.93, 64}, {3.93, 63}, + {3.92, 62}, {3.91, 61}, {3.90, 60}, {3.89, 59}, {3.87, 57}, {3.86, 56}, + {3.85, 55}, {3.84, 54}, {3.83, 53}, {3.82, 52}, {3.80, 51}, {3.79, 50}, + {3.78, 49}, {3.77, 48}, {3.76, 47}, {3.75, 46}, {3.74, 45}, {3.73, 44}, + {3.72, 43}, {3.71, 41}, {3.70, 40}, {3.70, 39}, {3.69, 38}, {3.69, 37}, + {3.68, 36}, {3.68, 35}, {3.67, 34}, {3.66, 33}, {3.65, 32}, {3.65, 31}, + {3.64, 30}, {3.63, 29}, {3.62, 28}, {3.62, 27}, {3.62, 26}, {3.61, 24}, + {3.60, 23}, {3.59, 22}, {3.57, 21}, {3.56, 20}, {3.54, 19}, {3.53, 18}, + {3.51, 17}, {3.51, 16}, {3.50, 15}, {3.49, 14}, {3.48, 13}, {3.47, 12}, + {3.45, 11}, {3.40, 10}, {3.34, 9}, {3.33, 7}, {3.31, 6}, {3.29, 5}, + {3.26, 4}, {3.24, 3}, {3.21, 2}, {3.16, 1}, {3.10, 0}, +}; + +const int numCapacityEntries = sizeof(capacityTable) / sizeof(BatteryCapacity); + +/** + Look up battery capacity percentage based on voltage. + + @param voltage the current voltage of the battery. + @returns the capacity remaining as a percentage integer. +*/ +int getBatteryCapacity(double voltage) { + for (int i = 0; i < numCapacityEntries; i++) { + if (voltage >= capacityTable[i].voltage) { + return capacityTable[i].percentage; + } + } + return 0; +} \ No newline at end of file diff --git a/src/config.h b/src/config.h deleted file mode 100644 index 272f727..0000000 --- a/src/config.h +++ /dev/null @@ -1,28 +0,0 @@ -#ifndef CONFIG_H -#define CONFIG_H - -// Assign config values. -const char* calendarUrl = "http://localhost:8080/calendar.png"; -const char* calendarDailyRefreshTime = "09:00:00"; -const int calendarRetries = 3; // number of times to retry draw/download - -// Wifi config. -const char* wifiSSID = "XXXX"; -const char* wifiPass = "XXXX"; -const int wifiRetries = 6; // number of times to retry WiFi connection - -// NTP config. -const char* ntpHost = - "pool.ntp.org"; // the time server host (keep as pool.ntp.org if in doubt) -const char* ntpTimezone = "Europe/Dublin"; - -// Remote logging config. -bool mqttLoggerEnabled = - false; // set to true for remote logging to a MQTT broker -const char* mqttLoggerBroker = "localhost"; // the broker host -const int mqttLoggerPort = 1883; -const char* mqttLoggerClientID = "inkplate10-weather-client"; -const char* mqttLoggerTopic = "mqtt/inkplate10-weather-client"; -const int mqttLoggerRetries = 3; // number of times to retry MQTT connection - -#endif \ No newline at end of file diff --git a/src/defaults.cpp b/src/defaults.cpp new file mode 100644 index 0000000..a063abd --- /dev/null +++ b/src/defaults.cpp @@ -0,0 +1,43 @@ +#ifndef __DEFAULTS_H__ +#define __DEFAULTS_H__ +/** + * Manually define config params. + * + * Only use this if you are not using the SD card (Inkplate10 V1). + * Otherwise add USE_SDCARD flag to load from SD card config.yaml + * + * These parameters are overriden by the config.yaml if SD card is enabled. + */ + +// The URL on the server which the client will try to download the first +// image from. +char serverURL[] = "http://localhost:8080/download.png"; +// The number of times to attempt downloading or drawing the server image. +int serverRetries = 3; + +// Wifi config. +char wifiSSID[] = "XXXX"; +char wifiPass[] = "XXXX"; +// The number of times to attempt WiFi connection before timeout. +int wifiRetries = 6; + +// NTP config. +// The time server (keep as pool.ntp.org if in doubt). +char ntpHost[] = "pool.ntp.org"; +// The timezone you live in ("Olson" format). +char ntpTimezone[] = "Europe/Dublin"; + +// Remote logging config. +// Set to true to send publish logs to an MQTT broker. +bool mqttLoggerEnabled = false; +// The MQTT broker to publish logs to. +char mqttLoggerBroker[] = "localhost"; +// The port of the MQTT broker. +int mqttLoggerPort = 1883; +// The unique identifier for this project in your MQTT broker. +char mqttLoggerClientID[] = "inkplate10-weather-client"; +// The name of the MQTT topic to publish to. +char mqttLoggerTopic[] = "mqtt/inkplate10-weather-client"; +// The number of times to attempt MQTT connection before timeout. +int mqttLoggerRetries = 3; +#endif \ No newline at end of file diff --git a/src/display_utils.cpp b/src/display_utils.cpp new file mode 100644 index 0000000..a7080be --- /dev/null +++ b/src/display_utils.cpp @@ -0,0 +1,161 @@ +#include "display_utils.h" +#include +#include "icon/icons_32x32.h" +#include "font/Merienda_Regular12pt7b.h" +#include "font/Merienda_Regular16pt7b.h" + +#include "log_utils.h" + +// The Inkplate board driver instance. +extern Inkplate board; + +/** + Load an image to the display buffer. + + @param filePath the path of the file on disk. + @returns the esp_err_t code: + - ESP_OK if successful. + - ESP_ERR_EDL if download file fails. + - ESP_ERR_EFILEW if writing file to filePath fails. +*/ +esp_err_t loadImage(const char* filePath) { + logf(LOG_INFO, "drawing image from path: %s", filePath); + + if (!board.drawImage(filePath, 0, 0, false, true)) { + return ESP_ERR_EDRAW; + } + + return ESP_OK; +} + +/** + Load a PNG image to the display buffer from a data buffer. + + @param buf the data buffer of png. + @param len the size of buffer. + @returns the esp_err_t code: + - ESP_OK if successful. + - ESP_ERR_EDL if download file fails. + - ESP_ERR_EFILEW if writing file to filePath fails. +*/ +esp_err_t loadImage(uint8_t* buf, int32_t len) { + log(LOG_INFO, "drawing image from buffer"); + + if (!board.drawPngFromBuffer(buf, len, 0, 0, false, true)) { + return ESP_ERR_EDRAW; + } + + return ESP_OK; +} + +/** + Load a BMP image to the display buffer. + + @param buf the byte array buffer. + @returns the esp_err_t code: + - ESP_OK if successful. + - ESP_ERR_EDL if download file fails. + - ESP_ERR_EFILEW if writing file to filePath fails. +*/ +esp_err_t loadImage(uint8_t* buf, int x, int y, int w, int h) { + log(LOG_DEBUG, "drawing image from byte array..."); + + if (!board.drawImage(buf, x, y, w, h, BLACK, WHITE)) { + return ESP_ERR_EDRAW; + } + + return ESP_OK; +} + +/** + Draw an message to the display. The error message is drawn in the top-left + corner of the display. Error message will overlay previously drawn image. + + @param msg the message to display. + error. +*/ +void displayMessage(const char* msg, int batteryRemainingPercent) { + board.clearDisplay(); + +#if defined(USE_SDCARD) + // If previous image exists, load into board buffer. + esp_err_t err = loadImage(CALENDAR_RW_PATH); + if (err != ESP_OK) { + log(LOG_WARNING, "load previous image error"); + } +#endif + + int cX = E_INK_HEIGHT / 2; + int cY = 16; // 16pt font + int16_t x, y; + uint16_t w, h; + board.setFont(&Merienda_Regular16pt7b); + board.setTextSize(1); + board.setTextColor(BLACK); + board.setTextWrap(true); + board.getTextBounds(msg, 0, 0, &x, &y, &w, &h); + board.fillRect(0, 0, E_INK_HEIGHT, h * 1.5, 0x8080); + board.setCursor(cX - w / 2, cY + h / 2); + board.setTextColor(0xFFFF); + board.print(msg); + + displayBatteryStatus(batteryRemainingPercent, true); + + board.display(); +} + +/** + Draw the battery status to the display. + + @param batteryRemainingPercent the percentage capacity remaining in the + battery. error. +*/ +void displayBatteryStatus(int batteryRemainingPercent, bool invert) { + // PS apologies for all the hackiness here... + char msg[4]; + sprintf(msg, "%d%%", batteryRemainingPercent); + board.setFont(&Merienda_Regular12pt7b); + board.setTextSize(1); + if (invert) { + board.setTextColor(0xFF); + } else { + board.setTextColor(0x00); + } + + int16_t tX, tY; + uint16_t tW, tH; + board.getTextBounds(msg, E_INK_HEIGHT * 0.9, batteryIconSize, &tX, &tY, &tW, + &tH); + // who knows why 0.75 but that lines things up + board.setCursor(tX, tY + tH * 0.75); + board.print(msg); + + // epdBitmapBatteryFull + int idx; + if (batteryRemainingPercent > 66 && batteryRemainingPercent <= 100) { + idx = 0; + } else if (batteryRemainingPercent > 33 && batteryRemainingPercent <= 66) { + // epdBitmapBatteryHalf + idx = 1; + } else if (batteryRemainingPercent > 10 && batteryRemainingPercent <= 33) { + // epdBitmapBatteryLow + idx = 2; + } else { + // epdBitmapBatteryEmpty + idx = 3; + } + + uint8_t* buf; + if (invert) { + buf = epdBitmapAllInverted[idx]; + } else { + buf = epdBitmapAll[idx]; + } + + // Draw battery icon bitmap. + esp_err_t err = loadImage(buf, tX - batteryIconSize, tY - tH / 2, + batteryIconSize, batteryIconSize); + if (err != ESP_OK) { + log(LOG_WARNING, "Failed to draw epd_bitmap"); + } +} \ No newline at end of file diff --git a/src/file_utils.cpp b/src/file_utils.cpp new file mode 100644 index 0000000..3b11a49 --- /dev/null +++ b/src/file_utils.cpp @@ -0,0 +1,43 @@ +#include "file_utils.h" +#include +#include +#include +#include +#include +#include + +#include "log_utils.h" + +// The Inkplate board driver instance. +extern Inkplate board; + +/** + Write a data buffer a file at a given path. Store the file on disk at a given path. + + @param buf the data buffer. + @param size the size of the file to write. + @param filePath the path of the file on disk. + @returns the esp_err_t code: + - ESP_OK if successful. + - ESP_ERR_EFILEW if number of retries is exceeded without success. +*/ + +esp_err_t writeFile(uint8_t* buf, int32_t size, const char* filePath) { + logf(LOG_DEBUG, "writing file to path %s", filePath); + SdFat sd = board.getSdFat(); + + // Write image buffer to SD card + if (sd.exists(filePath)) { + sd.remove(filePath); + } + + File sdfile = sd.open(filePath, FILE_WRITE); + if (!sdfile) { + return ESP_ERR_EFILEW; + } + + sdfile.write(buf, size); + sdfile.close(); + + return ESP_OK; +} \ No newline at end of file diff --git a/src/lib.cpp b/src/lib.cpp deleted file mode 100644 index f31cada..0000000 --- a/src/lib.cpp +++ /dev/null @@ -1,472 +0,0 @@ -#include "lib.h" - -// remote mqtt logger -WiFiClient espClient; -PubSubClient client(espClient); -MqttLogger mqttLogger(client, "", MqttLoggerMode::SerialOnly); -// queue to store messages to publish once mqtt connection is established. -cppQueue logQ(sizeof(char) * 100, LOG_QUEUE_MAX_ENTRIES, FIFO, true); -// inkplate10 board driver -Inkplate board(INKPLATE_3BIT); -// timezone store -Timezone myTz; - -/** - Connect to a WiFi network in Station Mode. - - @param ssid the network SSID. - @param pass the network password. - @param retries the number of connection attempts to make before returning an - error. - @returns the esp_err_t code: - - ESP_OK if successful. - - ESP_ERR_TIMEOUT if number of retries is exceeded without success. -*/ -esp_err_t configureWiFi(const char* ssid, const char* pass, int retries) { - WiFi.mode(WIFI_STA); - WiFi.begin(ssid, pass); - logf(LOG_INFO, "connecting to WiFi SSID %s...", ssid); - - // Retry until success or give up - int attempts = 0; - while (attempts++ <= retries && WiFi.status() != WL_CONNECTED) { - logf(LOG_DEBUG, "connection attempt #%d...", attempts); - delay(1000); - } - - // If still not connected, error with timeout. - if (WiFi.status() != WL_CONNECTED) { - return ESP_ERR_TIMEOUT; - } - // Print the IP address - logf(LOG_DEBUG, "IP address: %s", WiFi.localIP().toString()); - - return ESP_OK; -} - -/** - Download a file at a given URL. Store the file on disk at a given path. - - @param url the URL of the file to download. - @param size the size of the file to download. - @param retries the number of download attempts to make before returning an - error. - @returns the esp_err_t code: - - ESP_OK if successful. - - ESP_ERR_TIMEOUT if number of retries is exceeded without success. -*/ -esp_err_t downloadFile(const char* url, int32_t size, const char* filePath) { - logf(LOG_INFO, "downloading file at URL %s", url); - - // Download file from URL - uint8_t* buf = board.downloadFile(url, &size); - if (!buf) { - return ESP_ERR_EDL; - } - - logf(LOG_DEBUG, "writing file to path %s", filePath); - SdFat sd = board.getSdFat(); - - // Write image buffer to SD card - if (sd.exists(filePath)) { - sd.remove(filePath); - } - - File sdfile = sd.open(filePath, FILE_WRITE); - if (!sdfile) { - return ESP_ERR_EFILEW; - } - - sdfile.write(buf, size); - sdfile.close(); - - return ESP_OK; -} - -/** - Load an image to the display buffer. - - @param filePath the path of the file on disk. - @returns the esp_err_t code: - - ESP_OK if successful. - - ESP_ERR_EDL if download file fails. - - ESP_ERR_EFILEW if writing file to filePath fails. -*/ -esp_err_t loadImage(const char* filePath) { - logf(LOG_INFO, "drawing image from path: %s", filePath); - - if (!board.drawImage(filePath, 0, 0, false, true)) { - return ESP_ERR_EDRAW; - } - - return ESP_OK; -} - -/** - Load an image to the display buffer. - - @param buf the byte array buffer. - @returns the esp_err_t code: - - ESP_OK if successful. - - ESP_ERR_EDL if download file fails. - - ESP_ERR_EFILEW if writing file to filePath fails. -*/ -esp_err_t loadImage(uint8_t* buf, int x, int y, int w, int h) { - log(LOG_DEBUG, "drawing image from byte array..."); - - if (!board.drawImage(buf, x, y, w, h, BLACK, WHITE)) { - return ESP_ERR_EDRAW; - } - - return ESP_OK; -} - -/** - Draw the battery status to the display. - - @param batteryRemainingPercent the percentage capacity remaining in the - battery. error. -*/ -void displayBatteryStatus(int batteryRemainingPercent, bool invert) { - // PS apologies for all the hackiness here... - char msg[4]; - sprintf(msg, "%d%%", batteryRemainingPercent); - board.setFont(&Merienda_Regular12pt7b); - board.setTextSize(1); - if (invert) { - board.setTextColor(0xFF); - } else { - board.setTextColor(0x00); - } - - int16_t tX, tY; - uint16_t tW, tH; - board.getTextBounds(msg, E_INK_HEIGHT * 0.9, batteryIconSize, &tX, &tY, &tW, - &tH); - // who knows why 0.75 but that lines things up - board.setCursor(tX, tY + tH * 0.75); - board.print(msg); - - // epdBitmapBatteryFull - int idx; - if (batteryRemainingPercent > 66 && batteryRemainingPercent <= 100) { - idx = 0; - } else if (batteryRemainingPercent > 33 && batteryRemainingPercent <= 66) { - // epdBitmapBatteryHalf - idx = 1; - } else if (batteryRemainingPercent > 10 && batteryRemainingPercent <= 33) { - // epdBitmapBatteryLow - idx = 2; - } else { - // epdBitmapBatteryEmpty - idx = 3; - } - - uint8_t* buf; - if (invert) { - buf = epdBitmapAllInverted[idx]; - } else { - buf = epdBitmapAll[idx]; - } - - // Draw battery icon bitmap. - esp_err_t err = loadImage(buf, tX - batteryIconSize, tY - tH / 2, - batteryIconSize, batteryIconSize); - if (err != ESP_OK) { - log(LOG_WARNING, "Failed to draw epd_bitmap"); - } -} - -/** - Draw an message to the display. The error message is drawn in the top-left - corner of the display. Error message will overlay previously drawn image. - - @param msg the message to display. - error. -*/ -void displayMessage(const char* msg, int batteryRemainingPercent) { - board.clearDisplay(); - // If previous image exists, load into board buffer. - esp_err_t err = loadImage(CALENDAR_RW_PATH); - if (err != ESP_OK) { - log(LOG_WARNING, "load previous image error"); - } - - int cX = E_INK_HEIGHT / 2; - int cY = 16; // 16pt font - int16_t x, y; - uint16_t w, h; - board.setFont(&Merienda_Regular16pt7b); - board.setTextSize(1); - board.setTextColor(BLACK); - board.setTextWrap(true); - board.getTextBounds(msg, 0, 0, &x, &y, &w, &h); - board.fillRect(0, 0, E_INK_HEIGHT, h * 1.5, 0x8080); - board.setCursor(cX - w / 2, cY + h / 2); - board.setTextColor(0xFFFF); - board.print(msg); - - displayBatteryStatus(batteryRemainingPercent, true); - - board.display(); -} - -/** - Connect to a MQTT broker for remote logging. - - @param broker the hostname of the MQTT broker. - @param port the port of the MQTT broker. - @param topic the topic to publish logs to. - @param clientID the name of the logger client to appear as. - @param max_retries the number of connection attempts to make before fallback - to serial-only logging. - @returns the esp_err_t code: - - ESP_OK if successful. - - ESP_ERR_TIMEOUT if number of retries is exceeded without success. -*/ -esp_err_t configureMQTT(const char* broker, int port, const char* topic, - const char* clientID, int max_retries) { - log(LOG_INFO, "configuring remote MQTT logging..."); - - client.setServer(broker, port); - // Attempt to connect to MQTT broker. - int attempts = 0; - while (attempts++ <= max_retries && !client.connect(clientID)) { - logf(LOG_DEBUG, "connection attempt #%d...", attempts); - delay(250); - } - - if (!client.connected()) { - return ESP_ERR_TIMEOUT; - } - - mqttLogger.setTopic(topic); - mqttLogger.setMode(MqttLoggerMode::MqttAndSerial); - - logf(LOG_INFO, "connected to MQTT broker %s:%d", broker, port); - - return ESP_OK; -} - -/** - Converts a priority into a log level prefix. - - @param pri the log level / priority of the message, see LOG_LEVEL. - @returns the string value of the priority. -*/ -const char* msgPrefix(uint16_t pri) { - char* priority; - - switch (pri) { - case LOG_CRIT: - priority = (char*)"CRITICAL"; - break; - case LOG_ERROR: - priority = (char*)"ERROR"; - break; - case LOG_WARNING: - priority = (char*)"WARNING"; - break; - case LOG_NOTICE: - priority = (char*)"NOTICE"; - break; - case LOG_INFO: - priority = (char*)"INFO"; - break; - case LOG_DEBUG: - priority = (char*)"DEBUG"; - break; - default: - priority = (char*)"INFO"; - break; - } - - char* prefix = new char[35]; - sprintf(prefix, "%s - %s - ", myTz.dateTime(RFC3339).c_str(), priority); - return prefix; -} - -/** - Log a message. - - @param pri the log level / priority of the message, see LOG_LEVEL. - @param msg the message to log. -*/ -void log(uint16_t pri, const char* msg) { - if (pri > LOG_LEVEL) return; - - const char* prefix = msgPrefix(pri); - size_t prefixLen = strlen(prefix); - size_t msgLen = strlen(msg); - char buf[prefixLen + msgLen + 1]; - strcpy(buf, prefix); - strcat(buf, msg); - ensureQueue(buf); -} - -/** - Log a message with formatting. - - @param pri the log level / priority of the message, see LOG_LEVEL. - @param fmt the format of the log message -*/ -void logf(uint16_t pri, const char* fmt, ...) { - if (pri > LOG_LEVEL) return; - - const char* prefix = msgPrefix(pri); - size_t prefixLen = strlen(prefix); - size_t msgLen = strlen(fmt); - char a[prefixLen + msgLen + 1]; - strcpy(a, prefix); - strcat(a, fmt); - - va_list args; - va_start(args, fmt); - size_t size = snprintf(NULL, 0, a, args); - char b[size + 1]; - vsprintf(b, a, args); - ensureQueue(b); - va_end(args); -} - -/** - Ensure log queue is populated/emptied based on MQTT connection. - - @param msg the log message. -*/ -void ensureQueue(char* logMsg) { - if (!client.connected()) { - // populate log queue while no mqtt connection - logQ.push(logMsg); - } else { - // send queued logs once we are connected. - if (logQ.getCount() > 0) { - mqttLogger.setMode(MqttLoggerMode::MqttOnly); - while (!logQ.isEmpty()) { - logQ.pop(logMsg); - mqttLogger.println(logMsg); - } - mqttLogger.setMode(MqttLoggerMode::MqttAndSerial); - } - } - // print/send the current log - mqttLogger.println(logMsg); -} - -/** - Connect to an NTP server and synchronize the on-board real-time clock. - - @param host the hostname of the NTP server (eg. pool.ntp.org). - @param timezoneName the name of the timezone in Olson format (eg. - Europe/Dublin) - @returns the esp_err_t code: - - ESP_OK if successful. - - ESP_ERR_ENTP if updating the NTP client fails. -*/ -esp_err_t configureTime(const char* ntpHost, const char* timezoneName) { - log(LOG_INFO, "configuring network time and RTC..."); - - setServer(ntpHost); - - if (!waitForSync()) { - return ESP_ERR_ENTP; - } - myTz.setLocation(F(timezoneName)); - - updateNTP(); - // Sync RTC with NTP time - // time_t nowTime = now(); - time_t nowTime = myTz.now(); - board.rtcSetEpoch(nowTime); - logf(LOG_DEBUG, "RTC synced to %s", dateTime(nowTime, RFC3339).c_str()); - - return ESP_OK; -} - -/** - Get the next scheduled time to wake from deep sleep. - - @param refreshTime the time of the day to wake in HH::MM:SS format (eg. - 09:00:00). error. - @returns the epoch time of when to wake. - If the real-time clock is not configured, it will return the last configured - RTC epoch time + DEEP_SLEEP_FALLBACK_SECONDS. -*/ -time_t getWakeTime(const char* refreshTime) { - if (!board.rtcIsSet()) { - log(LOG_WARNING, "cannot determine wake time: RTC not set"); - return board.rtcGetEpoch() + DEEP_SLEEP_FALLBACK_SECONDS; - } - - tmElements_t tm; - int hr, min, sec; - sscanf(refreshTime, "%d:%d:%d", &hr, &min, &sec); - - tm.Hour = hr; - tm.Minute = min; - tm.Second = sec; - - tm.Day = myTz.day(); - tm.Month = myTz.month(); - tm.Year = CalendarYrToTm(myTz.year()); - - time_t targetTime = makeTime(tm); - time_t nowTime = myTz.now(); - - // Rollover to tomorrow - if (nowTime > targetTime) { - targetTime += SECS_PER_DAY; - } - - return targetTime; -} - -/** - Enter deep sleep with RTC alarm. - - @param refreshTime the time of the day to wake in HH:MM:SS format (eg. - 09:00:00). error. -*/ -void sleep(const char* refreshTime) { - logf(LOG_DEBUG, "setting deep sleep RTC wakeup on pin %d", GPIO_NUM_39); - - time_t targetWakeTime = getWakeTime(refreshTime); - board.rtcSetAlarmEpoch(targetWakeTime, RTC_ALARM_MATCH_DHHMMSS); - esp_sleep_enable_ext0_wakeup(GPIO_NUM_39, 0); - - logf(LOG_DEBUG, "waking at %s", dateTime(targetWakeTime, RFC3339).c_str()); - - deepSleep(); -} - -/** - Enter deep sleep with RTC alarm. - - @param seconds the number of seconds to sleep for. -*/ -void sleep(const int seconds) { - logf(LOG_DEBUG, "setting deep sleep RTC wakeup on pin %d", GPIO_NUM_39); - - time_t targetWakeTime = myTz.now() + seconds; - board.rtcSetAlarmEpoch(targetWakeTime, RTC_ALARM_MATCH_DHHMMSS); - esp_sleep_enable_ext0_wakeup(GPIO_NUM_39, 0); - - logf(LOG_DEBUG, "waking at %s", dateTime(targetWakeTime, RFC3339).c_str()); - - deepSleep(); -} - -/** - Enter deep sleep. -*/ -void deepSleep() { - log(LOG_NOTICE, "deep sleeping now"); - WiFi.disconnect(); - WiFi.mode(WIFI_OFF); - -#if defined(HAS_SDCARD) - board.sdCardSleep(); -#endif - - esp_deep_sleep_start(); -} \ No newline at end of file diff --git a/src/lib.h b/src/lib.h deleted file mode 100644 index da3baf7..0000000 --- a/src/lib.h +++ /dev/null @@ -1,221 +0,0 @@ -#ifndef LIB_H -#define LIB_H -#include -#include -#include -#include -#include -#include -#include - -#include "Merienda_Regular16pt7b.h" -#include "Merienda_Regular12pt7b.h" -#include "MqttLogger.h" - -#define CalendarYrToTm(Y) ((Y)-1970) -#define SECONDS_IN_YEAR 86400 * 365 -// The number of seconds to sleep if RTC not configured correctly. -#define DEEP_SLEEP_FALLBACK_SECONDS 120 -// log message entry history size -#define LOG_QUEUE_MAX_ENTRIES 10 -// The file path on SD card to load config. -#define CONFIG_FILE_PATH "/config.yaml" -// Fallback time to refresh. -#define CONFIG_DEFAULT_CALENDAR_DAILY_REFRESH_TIME "09:00:00" -// The path on SD card where calendar images are downloaded to and read from. -#define CALENDAR_RW_PATH "/calendar.png" -// Guestimate file size for PNG image @ 1200x825 -#define CALENDAR_IMAGE_SIZE E_INK_WIDTH* E_INK_HEIGHT * 4 + 100 - -// Enum of errors that might be encountered. -#define ESP_ERR_ERRNO_BASE (0) -#define ESP_ERR_EDL (1 + ESP_ERR_ERRNO_BASE) // Download error -#define ESP_ERR_EDRAW (2 + ESP_ERR_ERRNO_BASE) // Draw error -#define ESP_ERR_EFILEW (3 + ESP_ERR_ERRNO_BASE) // File write error -#define ESP_ERR_ENTP (4 + ESP_ERR_ERRNO_BASE) // NTP error - -// Enum of log verbosity levels. -#define LOG_CRIT 0 -#define LOG_ERROR 1 -#define LOG_WARNING 2 -#define LOG_NOTICE 3 -#define LOG_INFO 4 -#define LOG_DEBUG 5 - -#ifndef LOG_LEVEL -// Debug logging by default. -#define LOG_LEVEL LOG_DEBUG -#endif - -// The remote logging instance. -extern MqttLogger mqttLogger; -// The log message queue. -extern cppQueue logQ; -// The Inkplate board driver instance. -extern Inkplate board; -// The timezone object to store localised time -extern Timezone myTz; -// Battery icon bitmap array. -extern uint8_t* epdBitmapAll[4]; -extern uint8_t* epdBitmapAllInverted[4]; -extern const int batteryIconSize; - -/** - Connect to a WiFi network in Station Mode. - - @param ssid the network SSID. - @param pass the network password. - @param retries the number of connection attempts to make before returning an - error. - @returns the esp_err_t code: - - ESP_OK if successful. - - ESP_ERR_TIMEOUT if number of retries is exceeded without success. -*/ -esp_err_t configureWiFi(const char* ssid, const char* pass, int retries); - -/** - Download a file at a given URL. Store the file on disk at a given path. - - @param url the URL of the file to download. - @param size the size of the file to download. - @param retries the number of download attempts to make before returning an - error. - @returns the esp_err_t code: - - ESP_OK if successful. - - ESP_ERR_TIMEOUT if number of retries is exceeded without success. -*/ -esp_err_t downloadFile(const char* url, int32_t size, const char* filePath); - -/** - Load an image to the display buffer. - - @param filePath the path of the file on disk. - error. - @returns the esp_err_t code: - - ESP_OK if successful. - - ESP_ERR_EDL if download file fails. - - ESP_ERR_EFILEW if writing file to filePath fails. -*/ -esp_err_t loadImage(const char* filePath); - -/** - Load an image to the display buffer. - - @param buf the byte array data - @returns the esp_err_t code: - - ESP_OK if successful. - - ESP_ERR_EDL if download file fails. - - ESP_ERR_EFILEW if writing file to filePath fails. -*/ -esp_err_t loadImage(uint8_t* buf, int x, int y, int w, int h); - -/** - Draw the battery status to the display. - - @param batteryRemainingPercent the percentage capacity remaining in the - battery. error. - @param invert flag to invert battery status due to black banner. -*/ -void displayBatteryStatus(int batteryRemainingPercent, bool invert); - -/** - Draw an message to the display. The error message is drawn in the top-left - corner of the display. Error message will overlay previously drawn image. - - @param msg the message to display. - @param batteryRemainingPercent the percentage remaining battery capacity for - battery status display. error. -*/ -void displayMessage(const char* msg, int batteryRemainingPercent); - -/** - Connect to an NTP server and synchronize the on-board real-time clock. - - @param ntpHost the hostname of the NTP server (eg. pool.ntp.org). - @param timezoneName the name of the timezone in Olson format (eg. - Europe/Dublin) - @returns the esp_err_t code: - - ESP_OK if successful. - - ESP_ERR_ENTP if updating the NTP client fails. -*/ -esp_err_t configureTime(const char* ntpHost, const char* timezoneName); - -/** - Get the next scheduled time to wake from deep sleep. - - @param refreshTime the time of the day to wake in HH::MM:SS format (eg. - 09:00:00). error. - @returns the epoch time of when to wake. - If the real-time clock is not configured, it will return the last configured - RTC epoch time + DEEP_SLEEP_FALLBACK_SECONDS. -*/ -time_t getWakeTime(const char* refreshTime); - -/** - Enter deep sleep. - - @param refreshTime the time of the day to wake in HH:MM:SS format (eg. - 09:00:00). error. -*/ -void sleep(const char* refreshTime); - -/** - Enter deep sleep. - - @param seconds the number of seconds to sleep for. -*/ -void sleep(const int seconds); - -/** - Enter deep sleep. -*/ -void deepSleep(); - -/** - Connect to a MQTT broker for remote logging. - - @param broker the hostname of the MQTT broker. - @param port the port of the MQTT broker. - @param topic the topic to publish logs to. - @param clientID the name of the logger client to appear as. - @param max_retries the number of connection attempts to make before fallback - to serial-only logging. - @returns the esp_err_t code: - - ESP_OK if successful. - - ESP_ERR_TIMEOUT if number of retries is exceeded without success. -*/ -esp_err_t configureMQTT(const char* broker, int port, const char* topic, - const char* clientID, int max_retries); - -/** - Log a message. - - @param pri the log level / priority of the message, see LOG_LEVEL. - @param msg the message to log. -*/ -void log(uint16_t pri, const char* msg); - -/** - Log a message with formatting. - - @param pri the log level / priority of the message, see LOG_LEVEL. - @param fmt the format of the log message -*/ -void logf(uint16_t pri, const char* fmt, ...); - -/** - Converts a priority into a log level prefix. - - @param pri the log level / priority of the message, see LOG_LEVEL. - @returns the string value of the priority. -*/ -const char* msgPrefix(uint16_t pri); - -/** - Ensure log queue is populated/emptied based on MQTT connection. - - @param msg the log message -*/ -void ensureQueue(char* msg); - -#endif \ No newline at end of file diff --git a/src/log_utils.cpp b/src/log_utils.cpp new file mode 100644 index 0000000..496afdb --- /dev/null +++ b/src/log_utils.cpp @@ -0,0 +1,158 @@ +#include "log_utils.h" +#include +#include +#include +#include +#include + +#include "error_utils.h" + +// remote mqtt logger +WiFiClient espClient; +PubSubClient client(espClient); +MqttLogger mqttLogger(client, "", MqttLoggerMode::SerialOnly); +// queue to store messages to publish once mqtt connection is established. +cppQueue logQ(sizeof(char) * 100, LOG_QUEUE_MAX_ENTRIES, FIFO, true); + +/** + Connect to a MQTT broker for remote logging. + + @param broker the hostname of the MQTT broker. + @param port the port of the MQTT broker. + @param topic the topic to publish logs to. + @param clientID the name of the logger client to appear as. + @param max_retries the number of connection attempts to make before + fallback to serial-only logging. + @returns the esp_err_t code: + - ESP_OK if successful. + - ESP_ERR_TIMEOUT if number of retries is exceeded without success. +*/ +esp_err_t configureMQTT(const char* broker, int port, const char* topic, + const char* clientID, int max_retries) { + log(LOG_INFO, "configuring remote MQTT logging..."); + + client.setServer(broker, port); + // Attempt to connect to MQTT broker. + int attempts = 0; + while (attempts++ <= max_retries && !client.connect(clientID)) { + logf(LOG_DEBUG, "connection attempt #%d...", attempts); + delay(250); + } + + if (!client.connected()) { + return ESP_ERR_TIMEOUT; + } + + mqttLogger.setTopic(topic); + mqttLogger.setMode(MqttLoggerMode::MqttAndSerial); + + logf(LOG_INFO, "connected to MQTT broker %s:%d", broker, port); + + return ESP_OK; +} + +/** + Converts a priority into a log level prefix. + + @param pri the log level / priority of the message, see LOG_LEVEL. + @returns the string value of the priority. +*/ +const char* msgPrefix(uint16_t pri) { + char* priority; + + switch (pri) { + case LOG_CRIT: + priority = (char*)"CRITICAL"; + break; + case LOG_ERROR: + priority = (char*)"ERROR"; + break; + case LOG_WARNING: + priority = (char*)"WARNING"; + break; + case LOG_NOTICE: + priority = (char*)"NOTICE"; + break; + case LOG_INFO: + priority = (char*)"INFO"; + break; + case LOG_DEBUG: + priority = (char*)"DEBUG"; + break; + default: + priority = (char*)"INFO"; + break; + } + + char* prefix = new char[35]; + String nowFmt = nowTzFmt(); + sprintf(prefix, "%s - %s - ", nowFmt.c_str(), priority); + return prefix; +} + +/** + Log a message. + + @param pri the log level / priority of the message, see LOG_LEVEL. + @param msg the message to log. +*/ +void log(uint16_t pri, const char* msg) { + if (pri > LOG_LEVEL) return; + + const char* prefix = msgPrefix(pri); + size_t prefixLen = strlen(prefix); + size_t msgLen = strlen(msg); + char buf[prefixLen + msgLen + 1]; + strcpy(buf, prefix); + strcat(buf, msg); + ensureQueue(buf); +} + +/** + Log a message with formatting. + + @param pri the log level / priority of the message, see LOG_LEVEL. + @param fmt the format of the log message +*/ +void logf(uint16_t pri, const char* fmt, ...) { + if (pri > LOG_LEVEL) return; + + const char* prefix = msgPrefix(pri); + size_t prefixLen = strlen(prefix); + size_t msgLen = strlen(fmt); + char a[prefixLen + msgLen + 1]; + strcpy(a, prefix); + strcat(a, fmt); + + va_list args; + va_start(args, fmt); + size_t size = snprintf(NULL, 0, a, args); + char b[size + 1]; + vsprintf(b, a, args); + ensureQueue(b); + va_end(args); +} + +/** + Ensure log queue is populated/emptied based on MQTT connection. + + @param msg the log message. +*/ +void ensureQueue(char* logMsg) { + if (!client.connected()) { + // populate log queue while no mqtt connection + logQ.push(logMsg); + } else { + // send queued logs once we are connected. + if (logQ.getCount() > 0) { + mqttLogger.setMode(MqttLoggerMode::MqttOnly); + while (!logQ.isEmpty()) { + logQ.pop(logMsg); + mqttLogger.println(logMsg); + } + mqttLogger.setMode(MqttLoggerMode::MqttAndSerial); + } + } + // print/send the current log + mqttLogger.println(logMsg); +} \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 954c42b..b53bc12 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,12 +1,21 @@ -#include -#include -#include -#if defined(HAS_SDCARD) -#include -#endif +#include +#include +#include -#include "lib.h" #include "battery.h" +#include "defaults.h" +#include "display_utils.h" +#include "error_utils.h" +#include "log_utils.h" +#include "network_utils.h" +#include "sleep_utils.h" +#include "time_utils.h" +#if defined(USE_SDCARD) +#include "file_utils.h" +#endif + +// inkplate10 board driver +Inkplate board(INKPLATE_3BIT); void setup() { Serial.begin(115200); @@ -14,7 +23,6 @@ void setup() { board.begin(); // Set board to portait mode. board.setRotation(1); - // Set clock from RTC board.rtcGetRtcData(); time_t bootTime = board.rtcGetEpoch(); @@ -51,21 +59,22 @@ void setup() { int batteryRemainingPercent = getBatteryCapacity(bvolt); logf(LOG_INFO, "approx battery capacity: %d%%", batteryRemainingPercent); -#if defined(HAS_SDCARD) +#if defined(USE_SDCARD) // Init storage. if (!board.sdCardInit()) { const char* errMsg = "SD card init failure"; log(LOG_ERROR, errMsg); displayMessage(errMsg, batteryRemainingPercent); - sleep(CONFIG_DEFAULT_CALENDAR_DAILY_REFRESH_TIME); + sleep(board.rtcGetEpoch() + SECONDS_IN_DAY); } #endif if (batteryRemainingPercent <= 1) { log(LOG_NOTICE, "battery near empty! - sleeping until charged"); - displayMessage("Battery empty, please charge!", batteryRemainingPercent); + displayMessage("Battery empty, please charge!", + batteryRemainingPercent); // Sleep instead of proceeding when battery is too low. - sleep(SECONDS_IN_YEAR); + sleep(board.rtcGetEpoch() + SECONDS_IN_YEAR); } else if (batteryRemainingPercent <= 10) { log(LOG_WARNING, "battery low, charge soon!"); } @@ -73,14 +82,14 @@ void setup() { // Init err state. esp_err_t err = ESP_OK; -#if defined(HAS_SDCARD) +#if defined(USE_SDCARD) // Attempt to get config yaml file. File file = sd.open(CONFIG_FILE_PATH, FILE_READ); if (!file) { const char* errMsg = "Failed to open config file"; logf(LOG_ERROR, errMsg); displayMessage(errMsg, batteryRemainingPercent); - sleep(CONFIG_DEFAULT_CALENDAR_DAILY_REFRESH_TIME); + sleep(FALLBACK_REFRESH_TIME); } // Attempt to parse yaml file. @@ -91,36 +100,33 @@ void setup() { const char* errMsg = "Failed to load config from file"; logf(LOG_ERROR, "failed to deserialize YAML: %s", dse.c_str()); displayMessage(errMsg, batteryRemainingPercent); - sleep(CONFIG_DEFAULT_CALENDAR_DAILY_REFRESH_TIME); + sleep(FALLBACK_REFRESH_TIME); } file.close(); // Assign config values. - JsonObject calendarCfg = doc["calendar"]; - const char* calendarUrl = calendarCfg["url"]; - const char* calendarDailyRefreshTime = calendarCfg["daily_refresh_time"]; - int calendarRetries = calendarCfg["retries"]; + JsonObject serverCfg = doc["server"]; + serverURL = serverCfg["url"]; + serverRetries = serverCfg["retries"]; // Wifi config. JsonObject wifiCfg = doc["wifi"]; - const char* wifiSSID = wifiCfg["ssid"]; - const char* wifiPass = wifiCfg["pass"]; - int wifiRetries = wifiCfg["retries"]; + wifiSSID = wifiCfg["ssid"]; + wifiPass = wifiCfg["pass"]; + wifiRetries = wifiCfg["retries"]; // NTP config. - const char* ntpHost = doc["ntp"]["host"]; - const char* ntpTimezone = doc["ntp"]["timezone"]; + ntpHost = doc["ntp"]["host"]; + ntpTimezone = doc["ntp"]["timezone"]; // Remote logging config. JsonObject mqttLoggerCfg = doc["mqtt_logger"]; - bool mqttLoggerEnabled = mqttLoggerCfg["enabled"]; - const char* mqttLoggerBroker = mqttLoggerCfg["broker"]; - int mqttLoggerPort = mqttLoggerCfg["port"]; - const char* mqttLoggerClientID = mqttLoggerCfg["clientId"]; - const char* mqttLoggerTopic = mqttLoggerCfg["topic"]; - int mqttLoggerRetries = mqttLoggerCfg["retries"]; -#else - #include "config.h" + mqttLoggerEnabled = mqttLoggerCfg["enabled"]; + mqttLoggerBroker = mqttLoggerCfg["broker"]; + mqttLoggerPort = mqttLoggerCfg["port"]; + mqttLoggerClientID = mqttLoggerCfg["clientId"]; + mqttLoggerTopic = mqttLoggerCfg["topic"]; + mqttLoggerRetries = mqttLoggerCfg["retries"]; #endif // Attempt to connect to WiFi. @@ -129,7 +135,7 @@ void setup() { const char* errMsg = "wifi connect timeout"; log(LOG_ERROR, errMsg); displayMessage(errMsg, batteryRemainingPercent); - sleep(calendarDailyRefreshTime); + sleep(board.rtcGetEpoch() + 60); } // Attempt to synchronize clocks with network time. @@ -152,19 +158,33 @@ void setup() { err = ESP_FAIL; const char* errMsg; int attempts = 0; -#if defined(HAS_SDCARD) - const char* imagePath = CALENDAR_RW_PATH; + int32_t defaultLen = E_INK_WIDTH * E_INK_HEIGHT * 8 + 100; + uint8_t *buf = 0; + // Default to a known refresh time. + // (len("XX:XX:XX") = 8) + 1 = 9 + char nextRefreshTime[9] = FALLBACK_REFRESH_TIME; do { logf(LOG_DEBUG, "calendar download attempt #%d", attempts + 1); - err = downloadFile(calendarUrl, CALENDAR_IMAGE_SIZE, imagePath); - if (err != ESP_OK) { + buf = downloadFile(serverURL, nextRefreshTime, &defaultLen); + if (!buf) { errMsg = "file download error"; log(LOG_ERROR, errMsg); continue; } - } while (err != ESP_OK && ++attempts <= calendarRetries); + err = ESP_OK; + + logf(LOG_INFO, "next refresh time: %s", nextRefreshTime); +#if defined(USE_SDCARD) + err = writeFile(buf, defaultLen, CALENDAR_RW_PATH); + if (err != ESP_OK) { + errMsg = "file write error"; + log(LOG_ERROR, errMsg); + continue; + } +#endif + } while (err != ESP_OK && ++attempts <= serverRetries); // Disconnect and turn off WiFi radio to save power. // Remove the below lines if you want to stay connected @@ -177,12 +197,9 @@ void setup() { if (err != ESP_OK) { displayMessage(errMsg, batteryRemainingPercent); // Deep sleep until next refresh time - sleep(calendarDailyRefreshTime); + sleep(nextRefreshTime); } -#else - const char* imagePath = calendarUrl; -#endif - + // Reset err state. err = ESP_FAIL; attempts = 0; @@ -190,7 +207,11 @@ void setup() { logf(LOG_DEBUG, "calendar draw attempt #%d", attempts + 1); board.clearDisplay(); - err = loadImage(imagePath); +#if defined(USE_SDCARD) + err = loadImage(CALENDAR_RW_PATH); +#else + err = loadImage(buf, defaultLen); +#endif if (err != ESP_OK) { errMsg = "image load error"; log(LOG_ERROR, errMsg); @@ -201,7 +222,7 @@ void setup() { // Send buffer to eink display. board.display(); - } while (err != ESP_OK && ++attempts <= calendarRetries); + } while (err != ESP_OK && ++attempts <= serverRetries); // If we were not successful, print the error msg to the inkplate display. if (err != ESP_OK) { @@ -209,7 +230,7 @@ void setup() { } // Deep sleep until next refresh time - sleep(calendarDailyRefreshTime); + sleep(nextRefreshTime); } void loop() {} diff --git a/src/network_utils.cpp b/src/network_utils.cpp new file mode 100644 index 0000000..45cc97c --- /dev/null +++ b/src/network_utils.cpp @@ -0,0 +1,129 @@ +#include "network_utils.h" + +#include +#include + +#include "error_utils.h" +#include "log_utils.h" + +/** + Connect to a WiFi network in Station Mode. + + @param ssid the network SSID. + @param pass the network password. + @param retries the number of connection attempts to make before returning an + error. + @returns the esp_err_t code: + - ESP_OK if successful. + - ESP_ERR_TIMEOUT if number of retries is exceeded without success. +*/ +esp_err_t configureWiFi(const char* ssid, const char* pass, int retries) { + WiFi.mode(WIFI_STA); + WiFi.begin(ssid, pass); + logf(LOG_INFO, "connecting to WiFi SSID %s...", ssid); + + // Retry until success or give up + int attempts = 0; + while (attempts++ <= retries && WiFi.status() != WL_CONNECTED) { + logf(LOG_DEBUG, "connection attempt #%d...", attempts); + delay(1000); + } + + // If still not connected, error with timeout. + if (WiFi.status() != WL_CONNECTED) { + return ESP_ERR_TIMEOUT; + } + // Print the IP address + logf(LOG_DEBUG, "IP address: %s", WiFi.localIP().toString()); + + return ESP_OK; +} + +/** + Download a file at the given URL to a data buffer. + + If the server also sends X-Next-Refresh header, the value at pointer + nextRefresh will be populated indicating when to wake up next. + + @param buf the data buffer for the downloaded file. + @param size the size of the file to download. + @param url the URL where to download the file. + @param nextRefresh a pointer storing the next time to refresh / wake up. + error. + @returns the esp_err_t code: + - ESP_OK if successful. + - ESP_ERR_TIMEOUT if number of retries is exceeded without success. +*/ +uint8_t* downloadFile(const char* url, char* nextRefresh, int32_t* defaultLen) { + logf(LOG_INFO, "downloading file at URL %s", url); + + bool sleep = WiFi.getSleep(); + WiFi.setSleep(false); + + HTTPClient http; + http.getStream().setNoDelay(true); + http.getStream().setTimeout(5); + + const char* headersToCollect[] = { + "X-Next-Refresh", + }; + const size_t numberOfHeaders = 1; + http.collectHeaders(headersToCollect, numberOfHeaders); + + // Connect with HTTP + http.begin(url); + + int httpCode = http.GET(); + + int32_t size = http.getSize(); + if (size == -1) + size = *defaultLen; + else + *defaultLen = size; + + uint8_t* buffer = (uint8_t *)ps_malloc(size); + uint8_t *buffPtr = buffer; + + if (httpCode != HTTP_CODE_OK) { + logf(LOG_ERROR, "Non-200 response from URL %s: %d", url, httpCode); + return buffer; + } + + if (http.hasHeader("X-Next-Refresh")) { + // Get the next refresh header value from the server. + // We use this to determine when to wake up next. + String headerVal = http.header("X-Next-Refresh"); + const char* headerValPtr = headerVal.c_str(); + strcpy(nextRefresh, headerValPtr); + + logf(LOG_DEBUG, "received header X-Next-Refresh: %s", nextRefresh); + } else { + logf(LOG_WARNING, "header X-Next-Refresh not found in response"); + } + + int32_t total = http.getSize(); + int32_t len = total; + + uint8_t buff[512] = {0}; + + WiFiClient* stream = http.getStreamPtr(); + while (http.connected() && (len > 0 || len == -1)) { + size_t size = stream->available(); + + if (size) { + int c = stream->readBytes( + buff, ((size > sizeof(buff)) ? sizeof(buff) : size)); + memcpy(buffPtr, buff, c); + + if (len > 0) len -= c; + buffPtr += c; + } else if (len == -1) { + len = 0; + } + } + + http.end(); + WiFi.setSleep(sleep); + + return buffer; +} \ No newline at end of file diff --git a/src/sleep_utils.cpp b/src/sleep_utils.cpp new file mode 100644 index 0000000..674dc77 --- /dev/null +++ b/src/sleep_utils.cpp @@ -0,0 +1,47 @@ +#include "sleep_utils.h" +#include +#include +#include +#include + +#include "log_utils.h" + +// The Inkplate board driver instance. +extern Inkplate board; + +/** + Enter deep sleep with RTC alarm. + + @param refreshTime the time of the day to wake in HH:MM:SS format (eg. + 09:00:00). error. +*/ +void sleep(const char* refreshTime) { + time_t targetWakeTime = getWakeTime(refreshTime); + sleep(targetWakeTime); +} + +void sleep(time_t targetWakeTime) { + logf(LOG_DEBUG, "setting deep sleep RTC wakeup on pin %d", GPIO_NUM_39); + + board.rtcSetAlarmEpoch(targetWakeTime, RTC_ALARM_MATCH_DHHMMSS); + esp_sleep_enable_ext0_wakeup(GPIO_NUM_39, 0); + + logf(LOG_DEBUG, "waking at %s", dateTime(targetWakeTime, RFC3339).c_str()); + + deepSleep(); +} + +/** + Enter deep sleep. +*/ +void deepSleep() { + log(LOG_NOTICE, "deep sleeping now"); + WiFi.disconnect(); + WiFi.mode(WIFI_OFF); + +#if defined(USE_SDCARD) + board.sdCardSleep(); +#endif + + esp_deep_sleep_start(); +} \ No newline at end of file diff --git a/src/time_utils.cpp b/src/time_utils.cpp new file mode 100644 index 0000000..5549df7 --- /dev/null +++ b/src/time_utils.cpp @@ -0,0 +1,90 @@ +#include "time_utils.h" +#include +#include +#include + +#include "error_utils.h" +#include "log_utils.h" + +// The timezone store +Timezone myTz; + +// The Inkplate board driver instance. +extern Inkplate board; + +/** + * Return a RFC3339 formatted string of the current time. + * + * @return String the RFC3339 formatted string of the current time. + */ +String nowTzFmt() { + return dateTime(myTz.now(), RFC3339); +} + +/** + Connect to an NTP server and synchronize the on-board real-time clock. + + @param host the hostname of the NTP server (eg. pool.ntp.org). + @param timezoneName the name of the timezone in Olson format (eg. + Europe/Dublin) + @returns the esp_err_t code: + - ESP_OK if successful. + - ESP_ERR_ENTP if updating the NTP client fails. +*/ +esp_err_t configureTime(const char* ntpHost, const char* timezoneName) { + log(LOG_INFO, "configuring network time and RTC..."); + + setServer(ntpHost); + + if (!waitForSync()) { + return ESP_ERR_ENTP; + } + myTz.setLocation(F(timezoneName)); + + updateNTP(); + // Sync RTC with NTP time + // time_t nowTime = now(); + time_t nowTime = myTz.now(); + board.rtcSetEpoch(nowTime); + logf(LOG_DEBUG, "RTC synced to %s", dateTime(nowTime, RFC3339).c_str()); + + return ESP_OK; +} + +/** + Get the next scheduled time to wake from deep sleep. + + @param refreshTime the time of the day to wake in HH::MM:SS format (eg. + 09:00:00). error. + @returns the epoch time of when to wake. + If the real-time clock is not configured, it will return the last + configured RTC epoch time + FALLBACK_SLEEP_SECONDS. +*/ +time_t getWakeTime(const char* refreshTime) { + if (!board.rtcIsSet()) { + log(LOG_WARNING, "cannot determine wake time: RTC not set"); + return board.rtcGetEpoch() + FALLBACK_SLEEP_SECONDS; + } + + tmElements_t tm; + int hr, min, sec; + sscanf(refreshTime, "%d:%d:%d", &hr, &min, &sec); + + tm.Hour = hr; + tm.Minute = min; + tm.Second = sec; + + tm.Day = myTz.day(); + tm.Month = myTz.month(); + tm.Year = CalendarYrToTm(myTz.year()); + + time_t targetTime = makeTime(tm); + time_t nowTime = myTz.now(); + + // Rollover to tomorrow + if (nowTime > targetTime) { + targetTime += SECS_PER_DAY; + } + + return targetTime; +}