Skip to content

Commit

Permalink
Make SD cards optional to fix power leak on older boards
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisjtwomey committed Jun 28, 2023
1 parent a33e539 commit 2119536
Show file tree
Hide file tree
Showing 6 changed files with 147 additions and 34 deletions.
67 changes: 58 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,21 +29,22 @@ Both a server and client and required. The main workload is in the server which
2. Attempts to get current network time and update real-time clock.
3. (Optional) Attempts to connect a MQTT topic to publish logs. This allows us to see what the ESP32 controller is doing without needing to monitor the serial connection.
4. Attempt to download the PNG image that the server is hosting.
5. Write the downloaded PNG image to SD card.
5. (Optional) Write the downloaded PNG image to SD card.
6. Read the PNG image back from SD card and write to the e-ink display.
7. Returns to deep sleep until the next scheduled wake time (eg. 24 hours).

#### Features:
- Ultra-low power consumption:
- approx 21µA in deep sleep
- approx 240mA awake
- approx 30 seconds awake time daily
- approx 24µA in deep sleep
- approx 120mA awake
- approx 10-20 seconds awake time daily
- **1 - 2 years+** of battery life using a 2000mAh cell.
- Real-time clock for precise sleep/wake times.
- Daylight savings time handled automatically.
- Can publish to a MQTT topic for remote-logging.
- Renders messages on the e-ink display for critical errors (eg. battery low, wifi connect timeout etc.).
- Stores calendar images on SD card.
- Reconfigure client by updating YAML file on SD card and reboot - easy!
- Optional: stores calendar images on SD card.
- Optional: reconfigure client by updating YAML file on SD card and reboot - easy!

#### Power Consumption

Expand All @@ -61,7 +62,9 @@ See the [server/README.md](server/README.md) for more features.

If you want to use a more generic e-ink display or you just want to do a DIY build, there is a [branch](https://github.com/chrisjtwomey/inkplate10-weather-cal/tree/gxepd2) that's designed to work with GXEPD2, but it's likely missing fixes and features from the main branch.

- **2 GB microSD card ~€5**
- **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**

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.

Expand All @@ -83,7 +86,53 @@ See the [server/README.md](server/README.md) for more features.

## Setup

Place `config.yaml` in the root directory of an SD card and connect it to your Inkplate 10 board.
### Option #1: using config header file _(recommended for E-Radionica Inkplate10)_

**Note: The old _E-Radionica_ version of Inkplate10 is missing hardware to control power to the SD card module which results in up to 2mA power consumption during deep sleep, therefore it's recommended you use this option to preserve battery life. See https://github.com/SolderedElectronics/Inkplate-Arduino-library/issues/209.**

Update `config.h` with the:
```
// 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
```

Make sure to update:
- `wifiSSID` - the SSID if your WiFi network.
- `wifiPass` - the WiFi password.
- `calendarUrl` - the hostname or IP address of your server which the client will attempt to download the image from.
- `calendarDailyRefreshTime` - the time you want the client to wake each day, in `HH:MM:SS` format.
- `ntpTimezone` - the timezone you live in (in "Olson" format), otherwise the client might not wake at the expected time.
- `mqttLoggerBroker` - the hostname or IP address of your server (likely the same server as the image host).


### Option #2: Using a microSD card _(recommended for SolderedElectronics Inkplate10)_

**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.**

Insert an SD card into your Inkplate board and place a new file `config.yaml` in the root directory:

```
calendar:
Expand All @@ -106,7 +155,7 @@ mqtt_logger:
retries: 3
```

Likely parameters you'll need to change is
Make sure to update:
- `wifi.ssid` - the SSID if your WiFi network.
- `wifi.pass` - the WiFi password.
- `calendar.url` - the hostname or IP address of your server which the client will attempt to download the image from.
Expand Down
52 changes: 34 additions & 18 deletions doc/power-consumption.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,40 @@
# Power Consumption

The goal of this page is to document the power consumption performance of this project, findings and improvements along with some plots of voltage curves of performance test runs. This will be formatted like a timeline as I learn about long-term battery performance and ESP32 deep sleep states so if you're looking for the [conclusion](#conclusions), scroll to the bottom.
The goal of this page is to document the power consumption performance of this project, findings and improvements along with some plots of voltage curves of performance test runs. This will be formatted like a timeline as I learn about long-term battery performance and ESP32 deep sleep states.

With MQTT remote logging enabled, the client sends a log message to the server with its battery voltage. The server logs include the client MQTT messages which we can use to plot a graph of battery voltage over time.

The client has a cut-off voltage of 3.1v after which normal code execution is no longer allowed. At this point, a notice to recharge is displayed before entering a long deep sleep to protect the battery.

The expectations of performance when there is a single daily reboot with optimal WiFi connection (line-of-sight, minimal failed connection attempts):
The predictions of battery life when there is a single daily reboot with optimal WiFi connection (line-of-sight, minimal failed connection attempts):
- **3.7v 2000mAh LiPo/Li-ion @ 0.2C:**
- Pessimistic: **3 months**
- Optimistic: **6 months**
- Theoretical: **1 year+**
- Pessimistic: **6 months**
- Realistic: **1 year**
- Optimistic: **2+ years**
- Theoretical: **4+ years**
- **3.7v 6700mAh Li-ion @ 0.2C:**
- Pessimistic: **10 months**
- Optimistic: **20 months**
- Theoretical: **3.5 years+**
- Pessimistic: **20 months**
- Realistic: **3+ years**
- Optimistic: **6+ years**
- Theoretical: **13+ years**

## Update June 18 2023
Keep in mind, these predictions do not take into account the average life span of batteries which may cap these predictions at [2-3 years on average](https://www.newark.com/pdfs/techarticles/tektronix/LIBMG.pdf) nor do they take into account the natural depletion of charge and capacity over time. Expectations of performance should therefore fall somewhere between the pessimistic and optimistic ranges for battery life.

<img width="800" src="https://github.com/chrisjtwomey/inkplate10-weather-cal/assets/5797356/dae1b40f-39d7-4685-a556-7cbf117b608e" />

**Some takeaways:**
- Current performance is worse than expectations at **47 days**
- Deep Sleep power consumption is _very_ high - the rate of voltage change each day suggests significant power usage while device is in deep sleep state.
- Voltage plot does not fit a [typical LiPo/Li-ion voltage curve](https://www.researchgate.net/figure/Typical-discharge-curve-of-a-battery-showing-the-nominal-battery-voltage-which-is_fig1_256454266) - an initial voltage drop is expected, followed by a stabilising around the nominal voltage of 3.7v and then a cliff-like drop off towards the end of capacity.
- While there are expected dips at the start and end of discharge, the overall rate of voltage drop remains more or less linear.
**Note: be sure to check the health of your battery every few months regardless of reported battery percentage.**

## Update June 28 2023

I picked up a [PPK2](https://www.nordicsemi.com/Products/Development-hardware/Power-Profiler-Kit-2) as I wanted to measure deep sleep current draw with the June 20 version of the code:

![sleep](https://user-images.githubusercontent.com/5797356/248981718-ed4f92e4-3e3d-40b8-b4d7-cf3b38c8a948.png)

As you can see, deep sleep current draw is _far_ too high. This explains why the initial test run only lasted 50-ish days. Something was consuming milliamps of power even when we are deep sleeping.

While I initially suspected it to either be my code, or an [ESP32 issue](https://github.com/espressif/arduino-esp32/issues/1113) itself, the problem boiled down to my version of Inkplate10 hardware being old and missing some hardware that sleeps the SD card module and reduces power consumption.

See [the issue in Inkplate's repo](https://github.com/SolderedElectronics/Inkplate-Arduino-library/issues/209) to get the latest updates. Otherwise, for best battery life I recommend you **do not an SD card if you have an E-Radionica Inkplate10**.

I've made an update which makes SD cards **optional**. Once you disable and remove the SD card, deep sleep power consumption returns to **24µA** which theoretically gives this project years of battery life even on small capacity batteries.

## Update June 20 2023

Expand All @@ -41,6 +51,12 @@ A number of changes have been made around optimising performance and minimising

A second series has been added to the graph above which will track the long-term power consumption on the same battery for comparison. Like the last time, I will update the graph every few weeks.

# Conclusions
## Update June 18 2023

TODO
<img width="800" src="https://github.com/chrisjtwomey/inkplate10-weather-cal/assets/5797356/dae1b40f-39d7-4685-a556-7cbf117b608e" />

**Some takeaways:**
- Current performance is worse than expectations at **47 days**
- Deep Sleep power consumption is _very_ high - the rate of voltage change each day suggests significant power usage while device is in deep sleep state.
- Voltage plot does not fit a [typical LiPo/Li-ion voltage curve](https://www.researchgate.net/figure/Typical-discharge-curve-of-a-battery-showing-the-nominal-battery-voltage-which-is_fig1_256454266) - an initial voltage drop is expected, followed by a stabilising around the nominal voltage of 3.7v and then a cliff-like drop off towards the end of capacity.
- While there are expected dips at the start and end of discharge, the overall rate of voltage drop remains more or less linear.
6 changes: 6 additions & 0 deletions platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ build_flags =
-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
-DLOG_LEVEL=5
-DCORE_DEBUG_LEVEL=4

Expand All @@ -52,5 +55,8 @@ build_flags =
-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
-DLOG_LEVEL=4
-DCORE_DEBUG_LEVEL=0
28 changes: 28 additions & 0 deletions src/config.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#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
8 changes: 4 additions & 4 deletions src/lib.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ esp_err_t configureWiFi(const char* ssid, const char* pass, int retries) {
int attempts = 0;
while (attempts++ <= retries && WiFi.status() != WL_CONNECTED) {
logf(LOG_DEBUG, "connection attempt #%d...", attempts);
delay(500);
delay(1000);
}

// If still not connected, error with timeout.
Expand Down Expand Up @@ -464,9 +464,9 @@ void deepSleep() {
WiFi.disconnect();
WiFi.mode(WIFI_OFF);

esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_SLOW_MEM, ESP_PD_OPTION_OFF);
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_FAST_MEM, ESP_PD_OPTION_OFF);
esp_sleep_pd_config(ESP_PD_DOMAIN_XTAL, ESP_PD_OPTION_OFF);
#if defined(HAS_SDCARD)
board.sdCardSleep();
#endif

esp_deep_sleep_start();
}
20 changes: 17 additions & 3 deletions src/main.cpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
#include <ArduinoJson.h>
#include <ArduinoYaml.h>
#include <SdFat.h>
#include <StreamUtils.h>
#if defined(HAS_SDCARD)
#include <SdFat.h>
#endif

#include "lib.h"
#include "battery.h"
Expand Down Expand Up @@ -49,13 +51,15 @@ void setup() {
int batteryRemainingPercent = getBatteryCapacity(bvolt);
logf(LOG_INFO, "approx battery capacity: %d%%", batteryRemainingPercent);

#if defined(HAS_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);
}
#endif

if (batteryRemainingPercent <= 1) {
log(LOG_NOTICE, "battery near empty! - sleeping until charged");
Expand All @@ -69,6 +73,7 @@ void setup() {
// Init err state.
esp_err_t err = ESP_OK;

#if defined(HAS_SDCARD)
// Attempt to get config yaml file.
File file = sd.open(CONFIG_FILE_PATH, FILE_READ);
if (!file) {
Expand Down Expand Up @@ -114,6 +119,9 @@ void setup() {
const char* mqttLoggerClientID = mqttLoggerCfg["clientId"];
const char* mqttLoggerTopic = mqttLoggerCfg["topic"];
int mqttLoggerRetries = mqttLoggerCfg["retries"];
#else
#include "config.h"
#endif

// Attempt to connect to WiFi.
err = configureWiFi(wifiSSID, wifiPass, wifiRetries);
Expand Down Expand Up @@ -144,10 +152,13 @@ void setup() {
err = ESP_FAIL;
const char* errMsg;
int attempts = 0;
#if defined(HAS_SDCARD)
const char* imagePath = CALENDAR_RW_PATH;

do {
logf(LOG_DEBUG, "calendar download attempt #%d", attempts + 1);

err = downloadFile(calendarUrl, CALENDAR_IMAGE_SIZE, CALENDAR_RW_PATH);
err = downloadFile(calendarUrl, CALENDAR_IMAGE_SIZE, imagePath);
if (err != ESP_OK) {
errMsg = "file download error";
log(LOG_ERROR, errMsg);
Expand All @@ -168,6 +179,9 @@ void setup() {
// Deep sleep until next refresh time
sleep(calendarDailyRefreshTime);
}
#else
const char* imagePath = calendarUrl;
#endif

// Reset err state.
err = ESP_FAIL;
Expand All @@ -176,7 +190,7 @@ void setup() {
logf(LOG_DEBUG, "calendar draw attempt #%d", attempts + 1);

board.clearDisplay();
err = loadImage(CALENDAR_RW_PATH);
err = loadImage(imagePath);
if (err != ESP_OK) {
errMsg = "image load error";
log(LOG_ERROR, errMsg);
Expand Down

0 comments on commit 2119536

Please sign in to comment.