Browse Source

init commit

Toby Chui 1 year ago
parent
commit
1da02be210
4 changed files with 552 additions and 0 deletions
  1. 105 0
      InkyDash/InkyDash.ino
  2. 203 0
      InkyDash/draw.ino
  3. 36 0
      InkyDash/time.ino
  4. 208 0
      InkyDash/weather.ino

+ 105 - 0
InkyDash/InkyDash.ino

@@ -0,0 +1,105 @@
+/*
+    InkyDash
+
+    An ESP8266 powered version of InkyCal project
+    for low power desktop dashboard
+
+    Author: tobychui
+
+
+*/
+
+/* Pin Mappings */
+//Wemos D1 mini
+// BUSY -> D2, RST -> D4, DC -> D3, CS -> D8, CLK -> D5, DIN -> D7, GND -> GND, 3.3V -> 3.3V
+
+// Generic ESP8266
+// BUSY -> GPIO4, RST -> GPIO2, DC -> GPIO0, CS -> GPIO15, CLK -> GPIO14, DIN -> GPIO13, GND -> GND, 3.3V -> 3.3V
+
+/* Display Libraries */
+#include <GxEPD.h>
+#include <GxGDEW075Z09/GxGDEW075Z09.h>    // 7.5" b/w/r
+#include <GxIO/GxIO_SPI/GxIO_SPI.h>
+#include <GxIO/GxIO.h>
+
+/* Fonts */
+#include <Fonts/FreeSans9pt7b.h>
+#include <Fonts/FreeSansBold9pt7b.h>
+#include <Fonts/FreeSans12pt7b.h>
+#include <Fonts/FreeSans18pt7b.h>
+#include <Fonts/FreeSans24pt7b.h>
+#include <Fonts/FreeSansBold24pt7b.h>
+
+/* WiFi and Connections */
+#include <ESP8266WiFi.h>
+#include <NTPClient.h>
+#include <WiFiUdp.h>
+#include <WiFiManager.h>
+#include <ArduinoJson.h>
+#include <TaskScheduler.h>
+
+/* WiFi and Connections */
+WiFiManager wifiManager;
+WiFiUDP UDPconn;
+NTPClient ntpClient(UDPconn, "pool.ntp.org");
+
+/* SPI Settings */
+GxIO_Class io(SPI, /*CS=D8*/ SS, /*DC=D3*/ 0, /*RST=D4*/ 2); // arbitrary selection of D3(=0), D4(=2), selected for default of GxEPD_Class
+GxEPD_Class display(io, /*RST=D4*/ 2, /*BUSY=D2*/ 4); // default selection of D4(=2), D2(=4)
+
+/* Time Const */
+const String weekDays[7] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
+const String weeksDaysFull[7] = { "SUNDAY", "MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY", "SATURDAY" };
+const String months[12] = {"JANUARY",  "FEBRUARY",  "MARCH",  "APRIL",  "MAY",  "JUNE",  "JULY",  "AUGUST",  "SEPTEMBER",  "OCTOBER",  "NOVEMBER",  "DECEMBER"};
+const String deviceName = "InkyDash v0.1";
+
+/* Forcast */
+int forcastTempMin[7] = {0,0,0,0,0,0,0};
+int forcastTempMax[7] = {25,25,25,25,25,25,25};
+int forcastRhMax[7] = {100,100,100,100,100,100,100};
+
+/* Global Variables */
+int currentTemp = 25;
+int currentHumd = 50;
+int currentRain = 0;
+bool currentIsRaining = false;
+bool renderForcast = true;
+
+/* Schedulers */
+/*
+  void datetimeUpdateCallback();
+  void todayWeatherUpdateCallback();
+  void weatherForcastCallback();
+  Task t_dt(10000, TASK_FOREVER, &datetimeUpdateCallback); //Date-time update task
+  Task t_wt(3600000, TASK_FOREVER, &todayWeatherUpdateCallback); //Weather update task
+  Task t_wf(3600000, TASK_FOREVER, &weatherForcastCallback); //Forcast update task
+  Scheduler runner;
+*/
+void setup() {
+  //Start debug serial
+  Serial.begin(115200);
+  Serial.println();
+  Serial.println("Starting up InkyDash");
+  display.init(115200);
+
+  //Draw starting frame
+  display.setRotation(3);
+  display.drawPaged(drawStartingFrame);
+  delay(100);
+
+  // Connect to Wi-Fi
+  wifiManager.setClass("invert");
+  wifiManager.autoConnect("InkyDash Setup");
+
+  ntpClient.begin();
+  ntpClient.setTimeOffset(28800);
+  
+  //Draw home page
+  Serial.println("WiFi Connected");
+  display.drawPaged(drawHomeFrame);
+}
+
+void loop() {
+  // put your main code here, to run repeatedly:
+
+}

+ 203 - 0
InkyDash/draw.ino

@@ -0,0 +1,203 @@
+/*
+   drawutils.ino
+
+   This files contains utilities for drawing to the display
+
+
+*/
+
+//Draw the frame for the home information panel
+void drawHomeFrame() {
+  //Update the render information
+  ntpClient.update();
+  //Reset the screen with white
+  display.fillScreen(GxEPD_WHITE);
+  //Draw elements
+  drawDate(10, 10); //Draw the date
+  //drawWeatherPallete(display.width() - 110, 10);
+  drawDayProgressBar(200, 10, GxEPD_BLACK);
+  drawCalender(250);
+  drawLastUpdateTimestamp();
+}
+
+//Draw a timestamp at the corner to show the last update time
+void drawLastUpdateTimestamp() {
+  display.setFont(&FreeSans9pt7b);
+  display.setTextSize(0.5);
+  time_t epochTime = ntpClient.getEpochTime();
+  struct tm *ptm = gmtime ((time_t *)&epochTime);
+  String currentDate = String(ptm->tm_mday) + "/" + String(ptm->tm_mon + 1) + "/" + String(ptm->tm_year + 1900) + " " + timeZeroPad(ntpClient.getHours()) + ":" + timeZeroPad(ntpClient.getMinutes()) + ":" + timeZeroPad(ntpClient.getSeconds());
+  drawHighlighedText("Update: " + currentDate, 10, display.height() - 22, 5, GxEPD_WHITE, GxEPD_BLACK);
+  display.setTextSize(1);
+}
+
+//Draw a day progress bar to show time inaccurately
+void drawDayProgressBar(int y, int padding, uint16_t color) {
+  display.fillRect(padding, y, display.width() - 2 * padding, 3, color);
+
+  //Calculate how many percentage today has passed
+  time_t epochTime = ntpClient.getEpochTime();
+  int secondsSinceMidnight = epochTime % 86400; // 86400 seconds in a day (24 hours * 60 minutes * 60 seconds)
+  float percentageOfDayPassed = float(secondsSinceMidnight) / 86400.0;
+  int progressBarBlockSize = 3;
+  //Calculate how many no of blocks needed to be rendered
+  int totalNumberOfBlocks = (display.width() - 2 * padding) / (progressBarBlockSize * 2) + 1;
+  totalNumberOfBlocks = int(float(totalNumberOfBlocks) * percentageOfDayPassed);
+  display.setTextColor(GxEPD_RED);
+  for (int i = 0; i < totalNumberOfBlocks; i++) {
+    display.fillRect(padding + (i * (progressBarBlockSize * 2)), y + 5, progressBarBlockSize, 8, GxEPD_BLACK);
+  }
+  display.setTextColor(GxEPD_BLACK);
+}
+
+
+//Draw the date information at x,y (upper left corner point)
+void drawDate(int x, int y) {
+  /* Display Settings */
+  display.setFont(&FreeSans24pt7b);
+  display.setTextColor(GxEPD_BLACK);
+
+  int16_t tbx, tby; uint16_t tbw, tbh;
+  time_t epochTime = ntpClient.getEpochTime();
+  struct tm *ptm = gmtime((time_t *)&epochTime);
+
+  //Draw the month
+  String currentMonth = months[ptm->tm_mon];
+  display.getTextBounds(currentMonth, 0, 0, &tbx, &tby, &tbw, &tbh);
+  display.setCursor(x, y + tbh);
+  display.print(currentMonth);
+
+  //Offset the Y axis downward
+  y += y + tbh + 10;
+
+  //Draw the day
+  display.setTextSize(2);
+  display.getTextBounds(String(ptm->tm_mday), 0, 0, &tbx, &tby, &tbw, &tbh);
+  display.setCursor(x, y + tbh);
+  display.print(String(ptm->tm_mday));
+  display.setTextSize(1);
+
+
+  //Draw weekday
+  String weekDay = weeksDaysFull[ntpClient.getDay()];
+  display.setFont(&FreeSans12pt7b);
+  display.getTextBounds(weekDay, 0, 0, &tbx, &tby, &tbw, &tbh);
+  //Offset the Y axis again before appending
+  y += y + tbh + 10;
+  display.setCursor(x, y + tbh);
+  display.print(weekDay);
+
+}
+
+//Draw the calender
+void drawCalender(int y) {
+  // Display Settings
+  display.setFont(&FreeSansBold9pt7b);
+  display.setTextColor(GxEPD_BLACK);
+
+  // Dimension definations
+  int horizontalPadding = 10;
+  int verticalPadding = 10;
+  int gridWidth = int((display.width() - 2 * horizontalPadding) / 7.0);
+  int gridHeight = gridWidth;
+  int16_t tbx, tby; uint16_t tbw, tbh;
+
+  // Time Calculations
+  time_t epochTime = ntpClient.getEpochTime();
+  struct tm *ptm = gmtime((time_t *)&epochTime);
+  int dayInMonth = getNumberOfDayByMonth(ptm->tm_mon);
+  int firstDayOfWeek = getFirstDayDayOfWeek() + 1;
+  int dayCounter = (1 - firstDayOfWeek);
+
+  //Render the day of week line
+  display.fillRect(horizontalPadding, y - 12, display.width() - 2 * horizontalPadding, 30, GxEPD_BLACK);
+  display.setTextColor(GxEPD_WHITE);
+  for (int dx = 0; dx < 7; dx++) {
+    display.getTextBounds(weekDays[dx], 0, 0, &tbx, &tby, &tbw, &tbh);
+    display.setCursor(dx * gridWidth + horizontalPadding + int(tbw / 2.0), y + verticalPadding);
+    display.print(weekDays[dx]);
+  }
+  display.setTextColor(GxEPD_BLACK);
+
+  //Offset the y downward by 1.5 line height
+  y += int(gridHeight * 1.5);
+
+  //Render the calender dates
+  display.setFont(&FreeSans12pt7b);
+  int cx, cy, tcx, tcy;
+  for (int dy = 0; dy < 6; dy++) {
+    for (int dx = 0; dx < 7; dx++) {
+      if (dayCounter > dayInMonth) {
+        //End of this month
+        break;
+      }
+      if (dayCounter <= 0) {
+        //Shifting in first day of month
+        dayCounter++;
+        continue;
+      }
+
+      //Get the center of the grid
+      cx = dx * gridWidth + horizontalPadding + int(float(gridWidth) / 2.0);
+      cy = y + dy * gridHeight + verticalPadding - int(float(gridHeight) / 2.0);
+
+      //Calculate the height and width of the text and center it at the center of the grid
+      display.getTextBounds(String(dayCounter), 0, 0, &tbx, &tby, &tbw, &tbh);
+      tcx = cx - (tbw / 2.0);
+      tcy = cy + (tbh / 2.0);
+
+      //Draw the text to screen
+      if (dayCounter == ptm->tm_mday) {
+        //Today
+        //display.fillRoundRect(dx * gridWidth + horizontalPadding, y + dy * gridHeight + verticalPadding - gridHeight, gridWidth, gridHeight, 3, GxEPD_RED);
+        display.fillCircle(cx, cy, gridWidth / 2, GxEPD_RED);
+        display.setCursor(tcx, tcy);
+        display.setTextColor(GxEPD_WHITE);
+        display.print(String(dayCounter));
+        display.setTextColor(GxEPD_BLACK);
+      } else {
+        display.setCursor(tcx, tcy);
+        display.print(String(dayCounter));
+      }
+
+      dayCounter++;
+    }
+  }
+}
+
+//Draw the loading frame for the WiFi connection and settings
+void drawStartingFrame() {
+  //Print initializing text
+  display.fillScreen(GxEPD_WHITE);
+  int y_offset = -20; //Move the icon up by 20px
+
+  //Draw imus font logo
+  display.setFont(&FreeSans12pt7b);
+  display.setTextColor(GxEPD_BLACK);
+  int16_t tbx, tby; uint16_t tbw, tbh;
+  display.getTextBounds("imuslab", 0, 0, &tbx, &tby, &tbw, &tbh);
+  uint16_t x = ((display.width() - tbw) / 2) - tbx;
+  uint16_t y = ((display.height() - tbh) / 2) - tby + y_offset;
+  display.setCursor(x, y);
+  display.print("imuslab");
+
+
+  //Draw initializing text
+  display.setFont(&FreeSans9pt7b);
+  String waitingText = "Initializing Dashboard";
+  display.getTextBounds(waitingText, 0, 0, &tbx, &tby, &tbw, &tbh);
+  x = 20;
+  y = display.height() - 30;
+  drawHighlighedText(waitingText, x, y, 5, GxEPD_WHITE, GxEPD_BLACK);
+}
+
+
+//Draw a text in highlighed rectangle
+void drawHighlighedText(String text, int x, int y, int padding, uint16_t text_color, uint16_t bg_color ) {
+  int16_t tbx, tby; uint16_t tbw, tbh;
+  display.getTextBounds(text, 0, 0, &tbx, &tby, &tbw, &tbh);
+  display.fillRect(uint16_t(x - padding - 2), uint16_t(y - padding - 2), uint16_t(tbw + 2 * padding + 4), uint16_t(tbh + 2 * padding), bg_color);
+  display.setTextColor(text_color);
+  display.setCursor(x, y + 2 * padding);
+  display.print(text);
+}

+ 36 - 0
InkyDash/time.ino

@@ -0,0 +1,36 @@
+/*
+   time.ino
+
+   This script handle date time related features
+*/
+//Pad any time string with 0
+String timeZeroPad(int input) {
+  if (input < 10) {
+    return "0" + String(input);
+  }
+  return String(input);
+}
+
+//Get the day of week of the first day of this month
+int getFirstDayDayOfWeek(){
+  time_t epochTime = ntpClient.getEpochTime();
+  struct tm *ptm = gmtime((time_t *)&epochTime);
+  return (ntpClient.getDay() - (ptm->tm_mday % 7) + 7) % 7;
+}
+
+//Get the number of days of a month
+int getNumberOfDayByMonth(int monthId) {
+  if (monthId < 0 || monthId > 11) {
+    return -1; // Invalid monthId
+  }
+  int daysInMonth[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
+  return daysInMonth[monthId];
+}
+
+//Return if the current time is night
+bool isNight() {
+  int currentHour = ntpClient.getHours();
+  // Night is defined from 7:00 PM to 7:00 AM (19:00 to 7:00 in 24-hour format)
+  // Change this to fit your life style
+  return (currentHour >= 19 || currentHour < 7);
+}

+ 208 - 0
InkyDash/weather.ino

@@ -0,0 +1,208 @@
+/*
+    Weather Access API
+    Information provided by the Hong Kong Observatory open data API
+
+*/
+
+
+//Draw weather pallete, by default it align right
+void drawWeatherPallete(int x, int y) {
+  display.setFont(&FreeSans24pt7b);
+  uint16_t textColor;
+  int16_t tbx, tby; uint16_t tbw, tbh;
+  if (isNight()) {
+    textColor = GxEPD_BLACK;
+  } else {
+    textColor = GxEPD_RED;
+  }
+  display.setTextColor(textColor);
+  int lineHeight = 44;
+
+  //Print current temperature and humd
+  display.getTextBounds(String(currentTemp) + "C", 0, 0, &tbx, &tby, &tbw, &tbh);
+  display.setCursor(display.width() - tbw - 10, y + tbh);
+  display.print(String(currentTemp) + "C");
+
+  y += tbh + 10;
+  display.getTextBounds(String(currentHumd) + "%", 0, 0, &tbx, &tby, &tbw, &tbh);
+  display.setCursor(display.width() - tbw - 10, y + tbh);
+  display.print(String(currentHumd) + "%");
+  y += tbh + 10;
+  display.setFont(&FreeSans9pt7b);
+  lineHeight = 18;
+
+  if (renderForcast) {
+    //Render the forcast for coming 5 days
+    //if data grab failed, skip the render
+    String forcast;
+    for (int i = 0; i < 5; i++) {
+      y += lineHeight;
+      forcast = String(forcastTempMin[i]) + "-" + String(forcastTempMax[i]) + "C " + String(forcastRhMax[i]) + "%";
+      display.getTextBounds(forcast, 0, 0, &tbx, &tby, &tbw, &tbh);
+      display.setCursor(display.width() - tbw - 10, y);
+      display.print(forcast);
+    }
+  }
+
+}
+
+
+//Current weather info
+const char* rhurl = "https://data.weather.gov.hk/weatherAPI/opendata/weather.php?dataType=rhrread&lang=en";
+//Weather forcast info
+const char* wfurl = "https://data.weather.gov.hk/weatherAPI/opendata/weather.php?dataType=fnd&lang=en";
+
+/*
+   Weather Forcast Loaders
+*/
+
+bool updateAndRenderForcastInfo() {
+  Serial.println("Updating weather forcast info...");
+  WiFiClientSecure httpsClient;  // Use WiFiClientSecure for HTTPS
+  httpsClient.setInsecure();     // Ignore SSL certificate verification (for simplicity)
+
+  if (httpsClient.connect("data.weather.gov.hk", 443)) {
+    Serial.println("Downloading forcast info");
+    httpsClient.print(String("GET ") + wfurl + " HTTP/1.1\r\n" +
+                      "Host: data.weather.gov.hk\r\n" +
+                      "Connection: close\r\n\r\n");
+
+    String response = "";
+    //Skip through the headers
+    while (httpsClient.connected()) {
+      String line = httpsClient.readStringUntil('\n');
+      if (line == "\r") {
+        break;
+      }
+    }
+
+    //Read the JSON body
+    String line = "";
+    while (httpsClient.available()) {
+      line = httpsClient.readStringUntil('\n');  //Read Line by Line
+      response += line;
+    }
+
+    //Trim the first unwannted byte and the last
+    int startTrimPos = response.indexOf("{");
+    int lastTrimPos = response.lastIndexOf("}");
+    response = response.substring(startTrimPos, lastTrimPos + 1);
+
+    //Serial.println(response);
+    if (response.length() < 10){
+      //TODO HANDLE ERROR
+      return false;
+    }
+    
+    // Build the filter
+    StaticJsonDocument<1024> filter;
+    for (int i = 0; i < 7; i++) {
+      filter["weatherForecast"][i]["week"] = true;
+      filter["weatherForecast"][i]["forecastMintemp"]["value"] = true;
+      filter["weatherForecast"][i]["forecastMaxtemp"]["value"] = true;
+      filter["weatherForecast"][i]["forecastMaxrh"]["value"] = true;
+      filter["weatherForecast"][i]["ForecastIcon"] = true;
+    }
+
+    // Parse JSON data
+    StaticJsonDocument<5120> jsonDoc;
+    auto error = deserializeJson(jsonDoc, response, DeserializationOption::Filter(filter));
+    if (error) {
+      Serial.print(F("deserializeJson() failed with code "));
+      Serial.println(error.c_str());
+      return false;
+    }
+
+    //serializeJsonPretty(jsonDoc, Serial);
+
+    //Render the weather forcast onto the HMI
+    for (int i = 0; i < 7; i++) {
+      //Get day of week
+      String dow = jsonDoc["weatherForecast"][i]["week"];
+      dow = dow.substring(0, 3);
+
+      //Get min-max temp
+      int minTemp = jsonDoc["weatherForecast"][i]["forecastMintemp"]["value"];
+      int maxTemp = jsonDoc["weatherForecast"][i]["forecastMaxtemp"]["value"];
+      int maxRh = jsonDoc["weatherForecast"][i]["forecastMaxrh"]["value"];
+      //Get forcast icon
+      int forcastIcon = jsonDoc["weatherForecast"][i]["ForecastIcon"];
+      //TODO: Implement this
+      //renderForcastField(String(i), dow, minTemp, maxTemp, maxRh, forcastIcon);
+      forcastTempMin[i] = minTemp;
+      forcastTempMax[i] = maxTemp;
+      forcastRhMax[i] = maxRh;
+    }
+  }
+  return true;
+}
+
+/*
+   Current Weather Loaders
+*/
+
+void updateCurrentWeatherInfo() {
+  WiFiClientSecure httpsClient;  // Use WiFiClientSecure for HTTPS
+  httpsClient.setInsecure();     // Ignore SSL certificate verification (for simplicity)
+  
+  if (httpsClient.connect("data.weather.gov.hk", 443)) {
+    Serial.println("Downloading current weather info");
+    httpsClient.print(String("GET ") + rhurl + " HTTP/1.1\r\n" +
+                      "Host: data.weather.gov.hk\r\n" +
+                      "Connection: close\r\n\r\n");
+
+    String response = "";
+    //Skip through the headers
+    while (httpsClient.connected()) {
+      String line = httpsClient.readStringUntil('\n');
+      if (line == "\r") {
+        break;
+      }
+    }
+
+    //Read the JSON body
+    String line;
+    while (httpsClient.available()) {
+      line = httpsClient.readStringUntil('\n');  //Read Line by Line
+      response += line;
+    }
+
+
+    //Trim the first unwannted byte and the last
+    int startTrimPos = response.indexOf("{");
+    int lastTrimPos = response.lastIndexOf("}");
+    response = response.substring(startTrimPos, lastTrimPos + 1);
+
+    //Serial.println(response);
+    
+    // Build the filter
+    StaticJsonDocument<1024> filter;
+    filter["temperature"]["data"][0]["value"] = true;
+    filter["humidity"]["data"][0]["value"] = true;
+    filter["rainfall"]["data"][0]["max"] = true;
+    filter["rainfall"]["data"][0]["main"] = true;
+    
+    // Parse JSON data
+    StaticJsonDocument<4096> jsonDoc;
+    auto error = deserializeJson(jsonDoc, response, DeserializationOption::Filter(filter));
+    if (error) {
+      Serial.print(F("deserializeJson() failed with code "));
+      Serial.println(error.c_str());
+      return;
+    }
+    
+    // Access and return the temperature value
+    currentTemp = jsonDoc["temperature"]["data"][19]["value"];
+    currentHumd = jsonDoc["humidity"]["data"][0]["value"];
+    currentRain = jsonDoc["rainfall"]["data"][16]["max"];
+    currentIsRaining = String(jsonDoc["rainfall"]["data"][16]["main"]) == "TRUE";
+
+    // Serial debug prints
+    Serial.println(int(jsonDoc["temperature"]["data"][19]["value"]));
+    Serial.println(int(jsonDoc["humidity"]["data"][0]["value"]));
+    Serial.println(int(jsonDoc["rainfall"]["data"][16]["max"]));
+    Serial.println(String(jsonDoc["rainfall"]["data"][16]["main"]));
+  } else {
+    Serial.println("Failed to connect to server");
+  }
+}