531 lines
15 KiB
C++
531 lines
15 KiB
C++
#include "Adafruit_ThinkInk.h"
|
|
#include <ArduinoJson.h>
|
|
#include <WiFi.h>
|
|
#include <HTTPClient.h>
|
|
#include <time.h>
|
|
#include "esp_sleep.h"
|
|
|
|
//PIN LAYOUTS
|
|
#define SRAM_CS 32
|
|
#define EPD_CS 15
|
|
#define EPD_DC 33
|
|
#define EPD_RESET -1 // can set to -1 and share with microcontroller Reset!
|
|
#define EPD_BUSY 27 // can set to -1 to not use a pin (will wait a fixed delay)
|
|
#define EPD_ENABLE 12
|
|
#define BATTERY_PIN A13
|
|
|
|
ThinkInk_290_Tricolor_Z10 display(EPD_DC, EPD_RESET, EPD_CS, SRAM_CS, EPD_BUSY);
|
|
|
|
#define COLOR0 EPD_WHITE
|
|
#define COLOR1 EPD_BLACK
|
|
#define COLOR2 EPD_RED
|
|
|
|
|
|
const char* nightscoutUrl = "http://nightscout.chrispr.org:8082/api/v1/entries?count=20";
|
|
|
|
// TIME CONSTANTS
|
|
const long gmtOffset_sec = -18000;
|
|
const int daylightOffset_sec = 3600;
|
|
const char* ntpServer = "pool.ntp.org";
|
|
|
|
#define uS_TO_S_FACTOR 1000000 /* Conversion factor for micro seconds to seconds */
|
|
#define TIME_TO_SLEEP 5 /* Time ESP32 will go to sleep (in seconds) */
|
|
|
|
RTC_DATA_ATTR int sleepCount = 0;
|
|
|
|
void setup() {
|
|
// put your setup code here, to run once:
|
|
Serial.begin(115200);
|
|
while (!Serial) { delay(10); }
|
|
Serial.println("Nightscout CGM BS Monitor starting...");
|
|
|
|
//print_wakeup_reason();
|
|
|
|
ConnectToWifi();
|
|
|
|
struct tm timeinfo;
|
|
initTime();
|
|
|
|
if(!getLocalTime(&timeinfo)){
|
|
Serial.println("Failed to obtain time");
|
|
}
|
|
else {
|
|
Serial.println(&timeinfo, "%A, %B %d %Y %H:%M:%S");
|
|
}
|
|
|
|
//turn EPD on
|
|
pinMode(EPD_ENABLE, OUTPUT);
|
|
digitalWrite(EPD_ENABLE, HIGH);
|
|
|
|
int timeSinceLastSyncInSeconds = 0;
|
|
JsonArray jsonArray;
|
|
JsonVariant latestReading;
|
|
time_t lastSyncTime;
|
|
DynamicJsonDocument doc(8192);
|
|
|
|
while(timeSinceLastSyncInSeconds < (15 * 60))
|
|
{ //break out of the loop if we haven't received a reading in 15 minutes to conserve battery
|
|
String nightscoutResponse = httpGETRequest(nightscoutUrl);
|
|
//Serial.println(nightscoutResponse);
|
|
|
|
DeserializationError error = deserializeJson(doc, nightscoutResponse);
|
|
|
|
if (error) {
|
|
Serial.print(F("deserializeJson() failed: "));
|
|
Serial.println(error.f_str());
|
|
return;
|
|
}
|
|
|
|
//Serial.println(nightscoutResponse);
|
|
|
|
jsonArray = doc.as<JsonArray>();
|
|
latestReading = jsonArray[0];
|
|
Serial.printf("Received %d sgv records\n", jsonArray.size());
|
|
|
|
SleepIfCGMWarmup(latestReading);
|
|
|
|
unsigned long long dateInt = latestReading["date"].as<unsigned long long>();
|
|
int correctedDateInt = dateInt / 1000;
|
|
lastSyncTime = (time_t)correctedDateInt;
|
|
|
|
timeSinceLastSyncInSeconds = GetLastSyncTimeDifference(lastSyncTime);
|
|
Serial.printf("timeSinceLastSyncInSeconds %d\n", timeSinceLastSyncInSeconds);
|
|
|
|
if(timeSinceLastSyncInSeconds < 60)
|
|
break; //got the most recent reading, display it
|
|
|
|
if(timeSinceLastSyncInSeconds < 300 && timeSinceLastSyncInSeconds > 120)
|
|
break; //we missed it, or came in between readings, so sleep until the next one
|
|
|
|
//otherwise, try again in a few seconds
|
|
Serial.printf("timeSinceLastSyncInSeconds is %d, so trying again", timeSinceLastSyncInSeconds);
|
|
delay(1000);
|
|
}
|
|
display.begin();
|
|
display.clearBuffer();
|
|
|
|
RenderGraph(jsonArray);
|
|
RenderRightPane(latestReading);
|
|
RenderBatteryPercentage();
|
|
|
|
display.display();
|
|
|
|
SleepUntilNextReading(lastSyncTime);
|
|
}
|
|
|
|
void print_wakeup_reason(){
|
|
esp_sleep_wakeup_cause_t wakeup_reason;
|
|
|
|
wakeup_reason = esp_sleep_get_wakeup_cause();
|
|
|
|
switch(wakeup_reason)
|
|
{
|
|
case ESP_SLEEP_WAKEUP_EXT0 : Serial.println("Wakeup caused by external signal using RTC_IO"); break;
|
|
case ESP_SLEEP_WAKEUP_EXT1 : Serial.println("Wakeup caused by external signal using RTC_CNTL"); break;
|
|
case ESP_SLEEP_WAKEUP_TIMER : Serial.println("Wakeup caused by timer"); break;
|
|
case ESP_SLEEP_WAKEUP_TOUCHPAD : Serial.println("Wakeup caused by touchpad"); break;
|
|
case ESP_SLEEP_WAKEUP_ULP : Serial.println("Wakeup caused by ULP program"); break;
|
|
default : Serial.printf("Wakeup was not caused by deep sleep: %d\n",wakeup_reason); break;
|
|
}
|
|
}
|
|
|
|
void initTime()
|
|
{
|
|
struct tm timeinfo;
|
|
//if(!getLocalTime(&timeinfo))
|
|
if(sleepCount % 5 == 0) {
|
|
Serial.println("Syncing with NTP server..");
|
|
configTime(0, 0, ntpServer);
|
|
}
|
|
while(!getLocalTime(&timeinfo)){
|
|
Serial.println("Sleeping until time is set..");
|
|
delay(100);
|
|
}
|
|
//Setting EST timezone
|
|
setenv("TZ", "EST+5EDT,M3.2.0/2,M11.1.0/2",1);
|
|
tzset();
|
|
}
|
|
|
|
//Deprecated
|
|
int GetLastSyncTimeInSecondsWithinHour(String dateString) {
|
|
//"dateString":"2022-02-11T19:59:42.481-0500"
|
|
int idx = dateString.indexOf("T");
|
|
int minutesInTheHour = dateString.substring(idx+4, idx+6).toInt();
|
|
int secondsInTheHour = dateString.substring(idx+7, idx+9).toInt();
|
|
return (minutesInTheHour * 60) + secondsInTheHour;
|
|
}
|
|
|
|
void SleepIfCGMWarmup(JsonVariant latestReading) {
|
|
if(latestReading["direction"].as<String>().equalsIgnoreCase("NOT COMPUTABLE")) {
|
|
Serial.println("CGM warmup period. Sleeping 5");
|
|
SleepFiveMins();
|
|
}
|
|
}
|
|
|
|
int GetLastSyncTimeDifference(time_t lastReading) {
|
|
struct tm timeinfo;
|
|
time_t localTime;
|
|
|
|
if(!getLocalTime(&timeinfo)){
|
|
Serial.println("Error getting local time");
|
|
}
|
|
localTime = mktime(&timeinfo);
|
|
|
|
double timeDiff = difftime(localTime, lastReading);
|
|
return timeDiff;
|
|
}
|
|
void SleepUntilNextReading(time_t lastReading) {
|
|
struct tm timeinfo;
|
|
time_t localTime;
|
|
|
|
if(!getLocalTime(&timeinfo)){
|
|
Serial.println("Error getting local time");
|
|
}
|
|
localTime = mktime(&timeinfo);
|
|
|
|
double timeDiff = difftime(localTime, lastReading);
|
|
Serial.print(timeDiff);
|
|
Serial.println(" seconds diff between lastReading and current time");
|
|
int secondsToSleep = 300 - (int)timeDiff;
|
|
//Add one more to not jump the gun
|
|
secondsToSleep += 10;
|
|
sleepCount++;
|
|
if(secondsToSleep > 0 && secondsToSleep < 300) {
|
|
//secondsToSleep = secondsToSleep + 7;
|
|
Serial.print("Sleeping ");
|
|
Serial.println(secondsToSleep);
|
|
esp_sleep_enable_timer_wakeup(secondsToSleep * uS_TO_S_FACTOR);
|
|
Serial.flush();
|
|
TurnOffEPD();
|
|
esp_deep_sleep_start();
|
|
}
|
|
else {
|
|
SleepFiveMins();
|
|
}
|
|
}
|
|
|
|
unsigned long GetCurrentEpochTime() {
|
|
struct tm timeinfo;
|
|
time_t localTime;
|
|
|
|
if(!getLocalTime(&timeinfo)){
|
|
Serial.println("Error getting local time");
|
|
}
|
|
localTime = mktime(&timeinfo);
|
|
return (unsigned long)localTime;
|
|
}
|
|
|
|
void RenderBatteryPercentage() {
|
|
int batteryPinInput = analogRead(BATTERY_PIN);
|
|
Serial.printf("raw adc value %d", batteryPinInput);
|
|
//Vin = 3.3(read/4098)
|
|
float batteryVoltage = 3.3 * (float(batteryPinInput) / 4098.0);
|
|
//ESP32 A13 returns half the voltage, so double this reading
|
|
batteryVoltage = batteryVoltage * 2.0;
|
|
Serial.print("calc. voltage ");
|
|
Serial.println(batteryVoltage);
|
|
|
|
//Render on screen
|
|
display.setCursor(245, 110);
|
|
display.setTextSize(2);
|
|
if(batteryVoltage < 3.6)
|
|
display.setTextColor(COLOR2);
|
|
else
|
|
display.setTextColor(COLOR1);
|
|
display.print(batteryVoltage);
|
|
display.print("v");
|
|
}
|
|
|
|
void TurnOffEPD() {
|
|
digitalWrite(EPD_ENABLE, LOW);
|
|
delay(1000);
|
|
gpio_hold_en(GPIO_NUM_12);
|
|
gpio_deep_sleep_hold_en();
|
|
}
|
|
|
|
void SleepFiveMins() {
|
|
int secondsToSleep = 300;
|
|
Serial.print("Sleeping ");
|
|
Serial.println(secondsToSleep);
|
|
esp_sleep_enable_timer_wakeup(secondsToSleep * uS_TO_S_FACTOR);
|
|
Serial.flush();
|
|
TurnOffEPD();
|
|
esp_deep_sleep_start();
|
|
}
|
|
|
|
void RenderGraph(JsonArray sgvRecords) {
|
|
boolean redrawScreen = true;
|
|
double x = 0;
|
|
double y = 100;
|
|
int cnt = -45;
|
|
|
|
//Get 46 minutes into the past, to filter out older records
|
|
unsigned long timeCutoff = GetCurrentEpochTime() - (46 * 60);
|
|
|
|
for(int i=sgvRecords.size()-1; i >= 0; i--) {
|
|
|
|
JsonObject obj = sgvRecords[i].as<JsonObject>(); //v.as<JsonObject>();
|
|
|
|
if(&obj == NULL || obj["sgv"].as<String>() == NULL)
|
|
continue;
|
|
Serial.println("sgv");
|
|
Serial.println(obj["sgv"].as<String>());
|
|
Serial.println("device");
|
|
Serial.println(obj["device"].as<String>());
|
|
|
|
|
|
if(obj["device"].as<String>().indexOf("xDrip") == -1) {
|
|
continue;
|
|
}
|
|
if(cnt > 0)
|
|
continue;
|
|
unsigned long readingTime = (unsigned long)(obj["date"].as<unsigned long long>() / 1000);
|
|
|
|
Serial.printf("readingTime: %d\n", readingTime);
|
|
if(readingTime < timeCutoff)
|
|
continue;
|
|
|
|
Graph(display, cnt, obj["sgv"].as<double>(), 20, 110, 210, 105, -45,0, 5,60,180,10,"", "", "", COLOR1, COLOR1, COLOR2, COLOR1, COLOR0, redrawScreen);
|
|
Serial.printf("Graphing x: %+d y: %f\n", cnt, obj["sgv"].as<double>());
|
|
cnt += 5;
|
|
}
|
|
}
|
|
|
|
void RenderTrendArrow(String trend) {
|
|
int charSize = 4;
|
|
if(trend.equalsIgnoreCase("DoubleUp")) {
|
|
display.drawChar(235, 60, 0x18, COLOR2, COLOR0, charSize);
|
|
display.drawChar(255, 60, 0x18, COLOR2, COLOR0, charSize);
|
|
}
|
|
else if(trend.equalsIgnoreCase("SingleUp")) {
|
|
display.drawChar(235, 60, 0x18, COLOR2, COLOR0, charSize);
|
|
}
|
|
else if(trend.equalsIgnoreCase("FortyFiveUp")) {
|
|
display.drawChar(235, 60, 0xBA, COLOR1, COLOR0, charSize);
|
|
}
|
|
else if(trend.equalsIgnoreCase("Flat")) {
|
|
display.drawChar(235, 60, 0x1A, COLOR1, COLOR0, charSize);
|
|
}
|
|
else if (trend.equalsIgnoreCase("FortyFiveDown")) {
|
|
display.drawChar(235, 60, 0xC9, COLOR1, COLOR0, charSize);
|
|
}
|
|
else if( trend.equalsIgnoreCase("SingleDown")) {
|
|
display.drawChar(235, 60, 0x19, COLOR2, COLOR0, charSize);
|
|
}
|
|
else if (trend.equalsIgnoreCase("DoubleDown")) {
|
|
display.drawChar(235, 60, 0x19, COLOR2, COLOR0, charSize);
|
|
display.drawChar(255, 60, 0x19, COLOR2, COLOR0, charSize);
|
|
}
|
|
else
|
|
{
|
|
Serial.print("An unrecognized trend was received. The trend is ");
|
|
Serial.println(trend);
|
|
}
|
|
}
|
|
|
|
void RenderRightPane(JsonVariant sgvRecord) {
|
|
display.setCursor(235, 10);
|
|
display.setTextSize(3);
|
|
display.setTextColor(COLOR2);
|
|
display.print(sgvRecord["sgv"].as<String>());
|
|
|
|
display.setCursor(235, 35);
|
|
display.setTextSize(3);
|
|
display.setTextColor(COLOR1);
|
|
int delta = int(sgvRecord["delta"].as<double>());
|
|
if(delta > 0)
|
|
display.print("+");
|
|
|
|
display.print(delta);
|
|
|
|
RenderTrendArrow(sgvRecord["direction"].as<String>());
|
|
|
|
//time
|
|
String sysTime = sgvRecord["sysTime"].as<String>();
|
|
int idx = sysTime.indexOf("T");
|
|
String timePart = sysTime.substring(idx + 1, idx + 6);
|
|
|
|
display.setCursor(235, 90);
|
|
display.setTextSize(2);
|
|
display.setTextColor(COLOR1);
|
|
display.print(timePart);
|
|
}
|
|
|
|
void loop() {
|
|
// Because we deep sleep, this will _NEVER_ be run
|
|
|
|
}
|
|
|
|
String httpGETRequest(const char* url) {
|
|
HTTPClient http;
|
|
|
|
// Your IP address with path or Domain name with URL path
|
|
http.begin(url);
|
|
|
|
http.addHeader("Content-Type", "application/json");
|
|
http.addHeader("Accept", "application/json");
|
|
|
|
// Send HTTP POST request
|
|
int httpResponseCode = http.GET();
|
|
|
|
String payload = "{}";
|
|
|
|
if (httpResponseCode>0) {
|
|
Serial.print("HTTP Response code: ");
|
|
Serial.println(httpResponseCode);
|
|
payload = http.getString();
|
|
}
|
|
else {
|
|
Serial.print("Error code: ");
|
|
Serial.println(httpResponseCode);
|
|
}
|
|
// Free resources
|
|
http.end();
|
|
|
|
return payload;
|
|
}
|
|
|
|
void ConnectToWifi() {
|
|
//WiFi.mode(WIFI_STA);
|
|
//WiFi.disconnect();
|
|
WiFi.begin(ssid, password);
|
|
int cnt=0;
|
|
Serial.println("Connecting to WiFi...");
|
|
while(WiFi.status() != WL_CONNECTED) {
|
|
delay(500);
|
|
Serial.print('.');
|
|
//Serial.print("status:" );
|
|
//Serial.println(WiFi.status());
|
|
//Serial.println(WiFi.localIP());
|
|
cnt++;
|
|
if(cnt > 50){
|
|
Serial.println("Cycling wifi");
|
|
WiFi.disconnect();
|
|
WiFi.begin(ssid, password);
|
|
cnt = 0;
|
|
}
|
|
}
|
|
Serial.println("");
|
|
Serial.print("Connected to WiFi network with IP Address: ");
|
|
Serial.println(WiFi.localIP());
|
|
}
|
|
|
|
/*
|
|
|
|
function to draw a cartesian coordinate system and plot whatever data you want
|
|
just pass x and y and the graph will be drawn
|
|
|
|
huge arguement list
|
|
&d name of your display object
|
|
x = x data point
|
|
y = y datapont
|
|
gx = x graph location (lower left)
|
|
gy = y graph location (lower left)
|
|
w = width of graph
|
|
h = height of graph
|
|
xlo = lower bound of x axis
|
|
xhi = upper bound of x asis
|
|
xinc = division of x axis (distance not count)
|
|
ylo = lower bound of y axis
|
|
yhi = upper bound of y asis
|
|
yinc = division of y axis (distance not count)
|
|
title = title of graph
|
|
xlabel = x asis label
|
|
ylabel = y asis label
|
|
gcolor = graph line colors
|
|
acolor = axi ine colors
|
|
pcolor = color of your plotted data
|
|
tcolor = text color
|
|
bcolor = background color
|
|
&redraw = flag to redraw graph on fist call only
|
|
*/
|
|
|
|
double ox , oy ;
|
|
|
|
void Graph(ThinkInk_290_Tricolor_Z10 &d, double x, double y, double gx, double gy, double w, double h, double xlo, double xhi, double xinc, double ylo, double yhi, double yinc, String title, String xlabel, String ylabel, unsigned int gcolor, unsigned int acolor, unsigned int pcolor, unsigned int tcolor, unsigned int bcolor, boolean &redraw) {
|
|
|
|
double ydiv, xdiv;
|
|
// initialize old x and old y in order to draw the first point of the graph
|
|
// but save the transformed value
|
|
// note my transform funcition is the same as the map function, except the map uses long and we need doubles
|
|
//static double ox = (x - xlo) * ( w) / (xhi - xlo) + gx;
|
|
//static double oy = (y - ylo) * (gy - h - gy) / (yhi - ylo) + gy;
|
|
double i;
|
|
double temp;
|
|
int rot, newrot;
|
|
|
|
if (redraw == true) {
|
|
|
|
redraw = false;
|
|
ox = (x - xlo) * ( w) / (xhi - xlo) + gx;
|
|
oy = (y - ylo) * (gy - h - gy) / (yhi - ylo) + gy;
|
|
// draw y scale
|
|
for ( i = ylo; i <= yhi; i += yinc) {
|
|
// compute the transform
|
|
temp = (i - ylo) * (gy - h - gy) / (yhi - ylo) + gy;
|
|
|
|
if (i == 0) {
|
|
d.drawLine(gx, temp, gx + w, temp, acolor);
|
|
}
|
|
else {
|
|
d.drawLine(gx, temp, gx + w, temp, gcolor);
|
|
}
|
|
|
|
d.setTextSize(1);
|
|
d.setTextColor(tcolor, bcolor);
|
|
d.setCursor(gx - 20, temp);
|
|
// precision is default Arduino--this could really use some format control
|
|
d.println(int(i));
|
|
}
|
|
// draw x scale
|
|
for (i = xlo; i <= xhi; i += xinc) {
|
|
|
|
// compute the transform
|
|
|
|
temp = (i - xlo) * ( w) / (xhi - xlo) + gx;
|
|
if (i == 0) {
|
|
d.drawLine(temp, gy, temp, gy - h, acolor);
|
|
}
|
|
else {
|
|
d.drawLine(temp, gy, temp, gy - h, gcolor);
|
|
}
|
|
|
|
d.setTextSize(1);
|
|
d.setTextColor(tcolor, bcolor);
|
|
d.setCursor(temp, gy + 5);
|
|
// precision is default Arduino--this could really use some format control
|
|
d.println(abs(int(i)));
|
|
}
|
|
|
|
//now draw the labels
|
|
d.setTextSize(2);
|
|
d.setTextColor(tcolor, bcolor);
|
|
d.setCursor(gx , gy - h - 30);
|
|
d.println(title);
|
|
|
|
d.setTextSize(1);
|
|
d.setTextColor(acolor, bcolor);
|
|
d.setCursor(gx , gy + 20);
|
|
d.println(xlabel);
|
|
|
|
d.setTextSize(1);
|
|
d.setTextColor(acolor, bcolor);
|
|
d.setCursor(gx - 30, gy - h - 10);
|
|
d.println(ylabel);
|
|
|
|
|
|
}
|
|
|
|
//graph drawn now plot the data
|
|
// the entire plotting code are these few lines...
|
|
// recall that ox and oy are initialized as static above
|
|
x = (x - xlo) * ( w) / (xhi - xlo) + gx;
|
|
y = (y - ylo) * (gy - h - gy) / (yhi - ylo) + gy;
|
|
d.drawLine(ox, oy, x, y, pcolor);
|
|
d.drawLine(ox, oy + 1, x, y + 1, pcolor);
|
|
d.drawLine(ox, oy - 1, x, y - 1, pcolor);
|
|
ox = x;
|
|
oy = y;
|
|
|
|
}
|