How to Build a Desktop Weather Station Using ESP32 and E-ink Display

Published  April 22, 2025   0
ESP32 Desktop Weather Station

In today’s fast-paced world, many of us spend long hours indoors—whether working from home, studying, or just managing our daily routines. Natural cues like changing skies, outdoor temperatures, or the feel of humidity are often lost behind four walls and closed windows. This can make it difficult to stay aware of what’s happening outside, especially when our focus is tied to digital devices. Having a dedicated, always-visible weather display on your desk adds a subtle but meaningful connection to the outside world. It’s a small touch that can help with planning your day, knowing when to grab a jacket before stepping out, or simply enjoying the gentle reminder that time and weather continue to change beyond the screen.

This project offers a practical way to build a clean and minimal DIY weather station using an ESP32 microcontroller and an E-Ink display. The use of E-Ink ensures that the display remains readable even without constant power, making it a perfect fit for battery-powered desktop use. Once connected to Wi-Fi, the station fetches real-time weather data such as temperature, humidity, and current conditions using the OpenWeatherMap API. It also monitors indoor temperature and humidity with the help of a built-in sensor and adds to the data displayed on the screen. The device automatically updates itself at fixed intervals and enters deep sleep to conserve energy. Once assembled, it becomes a quiet and elegant companion on your desk—keeping you informed, without the distractions of a smartphone or computer screen. If you are looking for something simple, you can also check out our IoT based Arduino weather monitoring system, which we have built previously. 

Desktop Weather Station Features

  • Multicolour E-Ink Display: A 4.2-inch multicolour E-Ink display that keeps content visible without constant power usage.

  • ESP32-S3 SoC: The ESP32-S3 microcontroller manages Wi-Fi communication, data fetching, and display updates.

  • Weather API Support: Uses OpenWeatherMap API to get weather details like outdoor temperature, condition, and location.

  • Indoor Monitoring: Dedicated low-power sensor for indoor temperature and humidity monitoring.

  • Low Power: Lasts for days in a single charge.

  • Auto Sleep: Goes into deep sleep after each update to conserve power.

  • Custom PCB Design: All components are neatly arranged on a custom PCB designed with KiCad.

Components Required

Here is the list of components we would require to build our Desktop Weather Station. The exact value of each component can be found in the schematics or the BOM.

  • ESP32-S3-WROOM-1-N16R8

  • 4.2” EPD display

  • HPP845E031R4 Temperature and Humidity sensor

  • ADP124ACPZ 3.3V LDO

  • MAX1898 Battery charger IC

  • MOSFETs, Diodes and LEDs

  • Other passive components

  • Switches and connectors

  • Custom PCB

  • 3D printed parts.

  • Other tools and consumables.

Desktop Weather Station Schematic Diagram

The complete circuit diagram for the Desktop Weather Station is shown below. It can also be downloaded in PDF format from the GitHub repo linked at the end.

Desktop Weather Station Schematics

We have divided the schematics into different sections based on the functionality. This will ensure it will be much easier to understand each section in depth. Let’s discuss each section one by one.

Desktop Weather Station USB Input and Power Management

In the first section, we have the USB input along with the power management circuit. The type-C USB input is used for charging the internal battery as well as for programming and debugging the onboard ESP32-S3 SoC. Two pull-down resistors are added to the CCx input, so that the board will work without any issues with a type C host. The USB  data lines are directly connected to the native USB pin of the ESP32-S3. Next, we have the power path management circuit built around an AO3401 P-channel MOSFET and an SS14 Schottky diode. This section ensures that the rest of the board can be powered from both the battery as well as the USB input without any problem. Next, an ADP124ACPZ ultra-low noise low-dropout voltage regulator from Analog Devices. This LDO is responsible for creating the 3.3V line required by the rest of the circuit.

Desktop Weather Station Power Switch and Battery Management

In the next section, we can see that the power switch is connected to the enable pin of the APD124 LDO. For charging the battery we have used the MAX1898 single-cell Lithium battery charging IC from Analog Devices. It can charge the internal battery with a maximum current of up to 500mA. Depending on the battery used, the charge current programming resistor must be changed to set the optimal charge current. As you can see, for the battery we have included two options, either the user can use an LIR2450, which can be inserted into the battery holder onboard or can attach a LiPo battery through the JST connector. A voltage divider is used to measure the battery voltage, which will be displayed on the E-Ink/EPD display.

Desktop Weather Station ESP32-S3 SoC

As already mentioned, the brain of the desktop weather station is an ESP32-S3 SoC. We have used the ESP32-S3-Wroom-1 module so that the assembling process will be easier, and we don’t need to worry about the RF design side, which will be a headache if we had used a bare ESP32-S3 chip. An ADM803 voltage supervisor chip is used for power monitoring and to ensure the proper startup of ESP32-S3. The USC connections are connected to the pins GPIO19 and GPIO20, which are the native USB pins of ESP32-S3. There are two buttons in the circuit, one is for boot selection and one is for manual reset. These buttons will be helpful while reprogramming the ESP32-S3 if the automatic flashing through the USB doesn’t work.

Desktop Weather Station EPD Display Driver

Then the E-Ink display section is fairly simple and as per the manufacturer's instructions. The Display module we have used is the E2417JS0D6 from Pervasive Displays, which is a 4.2”, tri-colour E-Ink display with a resolution of 400 x 300 pixels. The driver circuit is directly taken from the display module datasheet, and some components are replaced with an equivalent due to the unavailability of the recommended component.

Desktop Weather Station Temperature and Humidity Sensor

Last but not least, we have the HPP845E - high accuracy RH and Temperature Sensor from TE Connectivity. The HPP845E belong to their HTU21D series and it is code compatible with other HTU21D sensors. It can measure a wide temperature range of  -40 – 125 °C and relative humidity of 0-100%. It is interfaced with the ESP32-S3 through the I2C interface. This sensor is responsible for the indoor environmental monitoring in our desktop weather station.

Desktop Weather Station PCB

For this project, we have decided to make a custom multi-colour PCB. This will ensure that the final product is as compact as possible as well as easy to assemble and use. The PCB is designed with KiCad. All the design files are available to download from the GitHub repo linked below this article. The PCB has a dimension of each face is approximately 105mm x 90mm.

Here are the top and bottom layers of the PCB.

Desktop Weather Station PCB Layers

Once the PCB is ready and fully verified, we send it for manufacturing. Here are the Final PCBs from the PCB manufacturer.

Desktop Weather Station PCB Boards

Assembling the Desktop Weather Station PCB

For assembling the PCBs, the first step we have done is to sort all the required components as per the BOM. Once it's done we have placed them on the PCB and soldered them one by one. If you want to make this procedure easier, you can use an SMD stencil to apply the solder paste and then place the components on it prior to reflowing the PCB with an SMD rework station or a reflow oven. Here is the images of a fully assembled weather station PCB.

Desktop Weather Station PCB Boards Front

And here is the fully assembled PCB along with the display module.

ESP32 Desktop Weather Station Display and PCB

3D Printed Parts of Weather Station

We have designed a 3D-printed enclosure for the weather station so that it would be a cool gadget for your desk. The files for all  3D printed parts can be downloaded from the GitHub link provided at the end of the article, along with the Arduino sketch and bitmap file. Learn more about 3D printing and how to get started with it by following the link. You can download the 3D files from the project GitHub repo.

ESP32 Desktop Weather Station 3D Model

And here is the back side showing the stand and mounting holes.

Desktop Weather Station 3D back

And here are the printed parts. As you can see we have also used the threaded insert for the screws. The used screws are 6mm long  M2.5 screws.

Desktop Weather Station 3D Parts

Assembling the Desktop Weather Station 

Once all the parts are ready, you can start assembling the desktop weather station. Here is everything before the final assembly.

Assembling the Desktop Weather Station Components

To assemble the desktop weather station, first insert the display module into the 3D printed frame, followed by the fully assembled PCB. Carefully insert the display flex cable into the display connector and secure it. Later, connect the battery.

Backside of Desktop Weather Station with Assembled Parts

Then close the back panel, and use the 6mm M2.5 screws to secure it to the front frame. After that, you can attach the stand to the back panel using M2.5 screws. Here is the final result.

ESP32 Desktop Weather Station Fully Assembled

Desktop Weather Station Arduino Code

Now that we’ve understood how the circuit is set up, let’s take a look at the code that makes everything work. Even though there are multiple files included in the code, we will only be discussing the two most important files: config.h file which contains the user configurations and the main Arduino sketch, which contains the major functions and variables. The firmware part of this project builds upon the excellent work of David Bird. The code has been adapted from his original implementation, which you can find here: ESP32 Weather Paper Display. Before diving into the code, make sure your Arduino IDE is properly set up for the project. Install the ESP32 board manager, if not already installed,  and select the ESP32-S3 as the board. You'll also need to install a few libraries, including GxEPD2 for the E-Ink display, U8g2_for_Adafruit_GFX for additional graphics support, and ArduinoJson for handling JSON data. Once the board and libraries are in place, you're ready to make changes or to compile the code.
At firs, let’s look at the config.h file. As the name indicates, this file contains the user configurations for WiFi and the OpenWeather Map API.

// Change to your WiFi credentials
const char* ssid     = "circuitdigest";
const char* password = "12345678";
// Use your own API key by signing up for a free developer account at https://openweathermap.org/
String apikey       = "ad3exxxxxxxxxxxxxxxxxxxxxxxxxxxx";                      // openweathermap API
const char server[] = "api.openweathermap.org";
//Set your location according to OWM locations
String LAT              = "11.0110382";                     //Latitude
String LON              = "77.0130247";                     //Longitude
String City             = "Coimbatore";                    
String Country          = "IN";                           
String Language         = "EN";                            // Language
String Hemisphere       = "north";                         // or "south" 
String Units            = "M";                             // Use 'M' for Metric or I for Imperial
const char* Timezone    = "IST-5:30";                     //Time Zone
const char* ntpServer   = "pool.ntp.org";                 //ntp server                                                          
int   gmtOffset_sec     = 19800;                          // +5.30
int  daylightOffset_sec = 0;

In this section of the code, you need to enter your own Wi-Fi credentials by replacing the values of ssid and password with the name and password of your Wi-Fi network to allow the ESP32 to connect to the internet. The project fetches live weather data from OpenWeatherMap, so you must sign up at openweathermap.org to get a free API key and replace the placeholder value of apikey with your own. The server domain api.openweathermap.org is specified to make HTTP requests for weather data. Additionally, set your exact geographic location by updating the LAT and LON variables with your latitude and longitude coordinates. For clarity in the weather display, specify the city and country using the City and Country variables, set your preferred language for the weather description in Language, and define your hemisphere (either "north" or "south"). 

The Units variable allows you to choose between metric (M) or imperial (I) units for temperature and wind speed. Time-related settings are also configured here: set your timezone using the Timezone string (for example, "IST-5:30" for Indian Standard Time), specify an NTP server (ntpServer) to synchronise the real-time clock with internet time, and define the gmtOffset_sec and daylightOffset_sec values to adjust the time according to your local GMT offset and daylight saving settings. All these parameters ensure that your ESP32 connects successfully, fetches accurate weather data for your location, and displays it in your preferred format and language (Currently the code only supports English).

As you are familiar with the user configurations, let’s look at the main code.

#include "config.h"  // See 'config.h' file and enter your OWM API key and set the Wifi SSID and PASSWORD
#include <ArduinoJson.h>       // https://github.com/bblanchon/ArduinoJson
#include <WiFi.h>              // Built-in
#include "time.h"              // Built-in
#include <SPI.h>               // Built-in
#define  ENABLE_GxEPD2_display 0
#include <GxEPD2_BW.h>         // GxEPD2 v1.6.3
#include <GxEPD2_3C.h>
#include <U8g2_for_Adafruit_GFX.h>
#include "epaper_fonts.h"
#include "forecast_record.h"
#include "lang.h"
#include <Wire.h>
#define HTU21D_ADDRESS 0x40   // I2C address of HTU21D
#define TEMP_MEASURE_NO_HOLD  0xF3
#define HUMID_MEASURE_NO_HOLD 0xF5
#define SOFT_RESET            0xFE
#define USER_REGISTER_READ  0xE7
#define USER_REGISTER_WRITE 0xE6
#define SCREEN_WIDTH  400.0    // Set for landscape mode, don't remove the decimal place!
#define SCREEN_HEIGHT 300.0
enum alignment {LEFT, RIGHT, CENTER};
// Connections for the EPD Display
static const uint8_t EPD_BUSY = 13;  // to EPD BUSY
static const uint8_t EPD_CS   = 10;  // to EPD CS
static const uint8_t EPD_RST  = 14; // to EPD RST
static const uint8_t EPD_DC   = 15; // to EPD DC
static const uint8_t EPD_SCK  = 12; // to EPD CLK
static const uint8_t EPD_MISO = -1; // Master-In Slave-Out not used, as no data from display
static const uint8_t EPD_MOSI = 11; // to EPD DIN
GxEPD2_3C<GxEPD2_420c, GxEPD2_420c::HEIGHT> display(GxEPD2_420c(/*CS=5*/ EPD_CS, /*DC=*/ EPD_DC, /*RST=*/ EPD_RST, /*BUSY=*/ EPD_BUSY)); //
U8G2_FOR_ADAFRUIT_GFX u8g2Fonts;  // Select u8g2 font from here: https://github.com/olikraus/u8g2/wiki/fntlistall
// Using fonts:
// u8g2_font_helvB08_tf
// u8g2_font_helvB10_tf
// u8g2_font_helvB12_tf
// u8g2_font_helvB14_tf
// u8g2_font_helvB18_tf
// u8g2_font_helvB24_tf
boolean LargeIcon = true, SmallIcon = false;
#define Large  11           // For icon drawing, needs to be odd number for best effect
#define Small  5            // For icon drawing, needs to be odd number for best effect
String  time_str, date_str; // strings to hold time and received weather data
int     wifi_signal, CurrentHour = 0, CurrentMin = 0, CurrentSec = 0;
long    StartTime = 0;
//################ PROGRAM VARIABLES and OBJECTS ################
#define max_readings 24
Forecast_record_type  WxConditions[1];
Forecast_record_type  WxForecast[max_readings];
#include <common.h>
#define autoscale_on  true
#define autoscale_off false
#define barchart_on   true
#define barchart_off  false
float pressure_readings[max_readings]    = {0};
float temperature_readings[max_readings] = {0};
float humidity_readings[max_readings]    = {0};
float rain_readings[max_readings]        = {0};
float snow_readings[max_readings]        = {0};
long SleepDuration = 15; // Sleep time in minutes, aligned to the nearest minute boundary, so if 30 will always update at 00 or 30 past the hour

The code starts with including all the necessary libraries, configuration files, display setup, sensor definitions, and global variables required to run the weather station project. It begins by including the config.h file, where the user is expected to enter their OpenWeatherMap API key along with Wi-Fi credentials and other user configurations. Core libraries like ArduinoJson, WiFi, time, and SPI are included to handle internet connectivity, time synchronisation, and communication. The GxEPD2 library is used to drive the e-paper display, along with the U8g2_for_Adafruit_GFX library to enable high-quality font rendering. Several custom headers, such as epaper_fonts.h, forecast_record.h, and lang.h are included for managing fonts, weather data structures, and language settings

The HPP845E031R4(HTU21D series) temperature and humidity sensor is initialised over I2C with its standard address and command constants. Display dimensions are defined for landscape mode, and connection pins are mapped to interface the ESP32 with the e-paper display. A 3-color e-paper display object is initialised, and font objects are set up for rendering text. Several font options are listed for flexibility in display design. Flags and values are declared to manage display icon sizes, hold time and weather data strings and manage time variables. Arrays for storing readings of pressure, temperature, humidity, rain, and snow are defined, which are useful for generating trends or forecasts. The Forecast_record_type arrays store current and forecasted weather data. A variable for setting the deep sleep duration is also defined, allowing the device to wake up at regular intervals (e.g., every 15 minutes) to fetch and display new weather data. If you want to adjust the display update frequency, change this variable accordingly.

void setup() {
StartTime = millis();
pinMode(16, OUTPUT);
digitalWrite(16, LOW);
pinMode(42, OUTPUT);
digitalWrite(42, HIGH);
delay(100);
Serial.begin(115200);
Wire.begin();  // Initialize I2C
softReset();  // Reset the sensor before starting
disableHeater(); // Ensure the heater is turned off
if (StartWiFi() == WL_CONNECTED && SetupTime() == true) {
  InitialiseDisplay(); // Give screen time to initialise by getting weather data!
  byte Attempts = 1;
  bool RxWeather = false, RxForecast = false;
  WiFiClient client;   // wifi client object
  while ((RxWeather == false || RxForecast == false) && Attempts <= 2) { // Try up-to 2 time for Weather and Forecast data
    if (RxWeather  == false) RxWeather  = obtain_wx_data(client, "weather");
    if (RxForecast == false) RxForecast = obtain_wx_data(client, "forecast");
    Attempts++;
  }
  if (RxWeather && RxForecast) { // Only if received both Weather or Forecast proceed
     StopWiFi(); // Reduces power consumption
    DisplayWeather();
    display.display(false); // Full screen update mode
  }
}
BeginSleep();
}
void loop() { // this will never run!
yield();
}

In the setup function, the ESP32 initialises essential components and performs the initial weather data fetch. GPIO 16 is configured as an output to control power to the E-Ink display, and it’s set LOW to enable power. GPIO 42 is connected to an LED, which is turned ON by setting it HIGH. After that, the serial communication is started for debugging, and the I2C bus is initialised for the temperature and humidity sensor. A soft reset is performed on the sensor, and its internal heater is disabled to ensure accurate readings. The code then attempts to connect to Wi-Fi and synchronise the system time. If both operations succeed, the E-Ink display is initialised, and weather data is requested using a Wi-Fi client object. It tries up to two times to retrieve both current weather and forecast data. If successful, Wi-Fi is turned off to save power, the weather data is displayed, and a full screen refresh is triggered on the display. Finally, the system enters deep sleep mode using BeginSleep to conserve power between updates. There is not much in the loop function, as all operations are handled within the setup function.

void BeginSleep() {
display.powerOff();
digitalWrite(16, HIGH);
digitalWrite(42, LOW);
long SleepTimer = SleepDuration * 60; // theoretical sleep duration
long offset = (CurrentMin % SleepDuration) * 60 + CurrentSec; // number of seconds elapsed after last theoretical wake-up time point
if (offset > SleepDuration/2 * 60){ // waking up too early will cause <offset> too large
  offset -= SleepDuration * 60; // then we should make it negative, so as to extend this coming sleep duration
}
esp_sleep_enable_timer_wakeup((SleepTimer - offset) * 1000000LL); // do compensation to cover ESP32 RTC timer source inaccuracies
Serial.println("Entering " + String(SleepTimer) + "-secs of sleep time");
Serial.println("Awake for : " + String((millis() - StartTime) / 1000.0, 3) + "-secs");
Serial.println("Starting deep-sleep period...");
esp_deep_sleep_start();      // Sleep for e.g. 30 minutes
}

The BeginSleep function is responsible for putting the ESP32 into deep sleep mode to conserve power. It powers off the E-Ink display, disables GPIOs connected to the display and LED, calculates the exact sleep duration aligned with the intended wake-up interval, and initiates deep sleep using esp_deep_sleep_start. This helps in periodic weather updates while maintaining low power consumption.

void DisplayWeather() {                 // 4.2" e-paper display is 400x300 resolution
DrawHeadingSection();                 // Top line of the display
DrawMainWeatherSection(172, 70);      // Centre section of display for Location, temperature, Weather report, current Wx Symbol and wind direction
DrawForecastSection(233, 15);         // 3hr forecast boxes
DisplayPrecipitationSection(233, 82); // Precipitation sectio
if (WxConditions[0].Visibility > 0) Visibility(335, 100, String(WxConditions[0].Visibility) + "M");
if (WxConditions[0].Cloudcover > 0) CloudCover(350, 125, WxConditions[0].Cloudcover);
DrawAstronomySection(233, 74);        // Astronomy section Sun rise/set, Moon phase and Moon icon
}

The DisplayWeather function handles the complete layout of the E-Ink display by calling various drawing functions. It includes the header, main weather section, forecast, precipitation, visibility, cloud cover, and astronomy sections, each placed strategically on the screen for clarity.

void DrawHeadingSection() {
u8g2Fonts.setFont(u8g2_font_helvB08_tf);
display.setTextColor(GxEPD_RED);
drawString(SCREEN_WIDTH / 2, 2, City, CENTER);
display.setTextColor(GxEPD_BLACK);
drawString(4, 2, date_str, LEFT);
drawString(120, 2, time_str, LEFT);
DrawBattery(SCREEN_WIDTH-70, 14);
display.drawLine(0, 14, SCREEN_WIDTH, 14, GxEPD_BLACK);
}
void DrawMainWeatherSection(int x, int y) {
Display_HPP845E_Data(x - 120, y + 58);
DisplayDisplayWindSection(x - 115, y - 3, WxConditions[0].Winddir, WxConditions[0].Windspeed, 40);
DisplayWXicon(x + 5, y - 5, WxConditions[0].Icon, LargeIcon);
u8g2Fonts.setFont(u8g2_font_helvB10_tf);
u8g2Fonts.setFont(u8g2_font_helvB12_tf);
String Wx_Description = WxConditions[0].Forecast0;
if (WxConditions[0].Forecast1 != "") Wx_Description += " & " +  WxConditions[0].Forecast1;
if (WxConditions[0].Forecast2 != "" && WxConditions[0].Forecast1 != WxConditions[0].Forecast2) Wx_Description += " & " +  WxConditions[0].Forecast2;
drawStringMaxWidth(x - 170, y + 83, 28, TitleCase(Wx_Description), LEFT);
DrawMainWx(x, y + 60);
display.drawRect(0, y + 68, 232, 48, GxEPD_BLACK);
}

The DrawHeadingSection draws the top section of the display, including the city name, current date, time, battery level, and a horizontal separator line. And the DrawMainWeatherSection creates the central area, showing the weather icon, temperature, humidity, and a brief forecast description. It also draws a bounding box around this information.

void DrawForecastSection(int x, int y) {
u8g2Fonts.setFont(u8g2_font_helvB10_tf);
DrawForecastWeather(x, y, 0);
DrawForecastWeather(x + 56, y, 1);
DrawForecastWeather(x + 112, y, 2);
//       (x,y,width,height,MinValue, MaxValue, Title, Data Array, AutoScale, ChartMode)
for (int r = 0; r < max_readings; r++) {
  if (Units == "I") {
    pressure_readings[r] = WxForecast[r].Pressure * 0.02953;
    rain_readings[r]     = WxForecast[r].Rainfall * 0.0393701;
  }
  else {
    pressure_readings[r] = WxForecast[r].Pressure;
    rain_readings[r]     = WxForecast[r].Rainfall;
  }
  temperature_readings[r] = WxForecast[r].Temperature;
}
display.drawLine(0, y + 172, SCREEN_WIDTH, y + 172, GxEPD_BLACK);
u8g2Fonts.setFont(u8g2_font_helvB12_tf);
drawString(SCREEN_WIDTH / 2, y + 180, TXT_FORECAST_VALUES, CENTER);
u8g2Fonts.setFont(u8g2_font_helvB10_tf);
DrawGraph(SCREEN_WIDTH / 400 * 30,  SCREEN_HEIGHT / 300 * 221, SCREEN_WIDTH / 4, SCREEN_HEIGHT / 5, 900, 1050, Units == "M" ? TXT_PRESSURE_HPA : TXT_PRESSURE_IN, pressure_readings, max_readings, autoscale_on, barchart_off);
DrawGraph(SCREEN_WIDTH / 400 * 158, SCREEN_HEIGHT / 300 * 221, SCREEN_WIDTH / 4, SCREEN_HEIGHT / 5, 10, 30, Units == "M" ? TXT_TEMPERATURE_C : TXT_TEMPERATURE_F, temperature_readings, max_readings, autoscale_on, barchart_off);
DrawGraph(SCREEN_WIDTH / 400 * 288, SCREEN_HEIGHT / 300 * 221, SCREEN_WIDTH / 4, SCREEN_HEIGHT / 5, 0, 30, Units == "M" ? TXT_RAINFALL_MM : TXT_RAINFALL_IN, rain_readings, max_readings, autoscale_on, barchart_on);
}
void DrawForecastWeather(int x, int y, int index) {
u8g2Fonts.setFont(u8g2_font_helvB08_tf);
display.drawRect(x, y, 55, 65, GxEPD_BLACK);
display.drawLine(x + 1, y + 13, x + 54, y + 13, GxEPD_BLACK);
DisplayWXicon(x + 28, y + 35, WxForecast[index].Icon, SmallIcon);
drawString(x + 31, y + 3, String(ConvertUnixTime(WxForecast[index].Dt + WxConditions[0].Timezone).substring(0,5)), CENTER);
drawString(x + 41, y + 52, String(WxForecast[index].High, 0) + "° / " + String(WxForecast[index].Low, 0) + "°", CENTER);
}

The DrawForecastSection displays short-term forecast data with three individual weather boxes and corresponding graphs for pressure, temperature, and rainfall trends. The DrawForecastWeather function is used to create each of those forecast boxes with time, icon, and temperature range.

void DrawMainWx(int x, int y) {
u8g2Fonts.setFont(u8g2_font_helvB14_tf);
drawString(x - 25, y - 22, String(WxConditions[0].Temperature, 1) + "°" + (Units == "M" ? "C" : "F"), CENTER); // Show current Temperature
u8g2Fonts.setFont(u8g2_font_helvB12_tf);
drawString(x - 15, y - 3, String(WxConditions[0].High, 0) + "° | " + String(WxConditions[0].Low, 0) + "°", CENTER); // Show forecast high and Low
drawString(x + 30, y - 22, String(WxConditions[0].Humidity, 0) + "%", CENTER);
u8g2Fonts.setFont(u8g2_font_helvB10_tf);
drawString(x + 32, y - 3, "RH", CENTER);
}
void DisplayDisplayWindSection(int x, int y, float angle, float windspeed, int Cradius) {
arrow(x, y, Cradius - 7, angle, 12, 18); // Show wind direction on outer circle of width and length
u8g2Fonts.setFont(u8g2_font_helvB08_tf);
int dxo, dyo, dxi, dyi;
display.drawLine(0, 15, 0, y + Cradius + 30, GxEPD_RED);
display.drawCircle(x, y, Cradius, GxEPD_RED);     // Draw compass circle
display.drawCircle(x, y, Cradius + 1, GxEPD_RED); // Draw compass circle
display.drawCircle(x, y, Cradius * 0.7, GxEPD_RED); // Draw compass inner circle
for (float a = 0; a < 360; a = a + 22.5) {
  dxo = Cradius * cos((a - 90) * PI / 180);
  dyo = Cradius * sin((a - 90) * PI / 180);
  if (a == 45)  drawString(dxo + x + 10, dyo + y - 10, TXT_NE, CENTER);
  if (a == 135) drawString(dxo + x + 7,  dyo + y + 5,  TXT_SE, CENTER);
  if (a == 225) drawString(dxo + x - 15, dyo + y,      TXT_SW, CENTER);
  if (a == 315) drawString(dxo + x - 15, dyo + y - 10, TXT_NW, CENTER);
  dxi = dxo * 0.9;
  dyi = dyo * 0.9;
  display.drawLine(dxo + x, dyo + y, dxi + x, dyi + y, GxEPD_RED);
  dxo = dxo * 0.7;
  dyo = dyo * 0.7;
  dxi = dxo * 0.9;
  dyi = dyo * 0.9;
  display.drawLine(dxo + x, dyo + y, dxi + x, dyi + y, GxEPD_RED);
}
drawString(x, y - Cradius - 10,     TXT_N, CENTER);
drawString(x, y + Cradius + 5,      TXT_S, CENTER);
drawString(x - Cradius - 10, y - 3, TXT_W, CENTER);
drawString(x + Cradius + 8,  y - 3, TXT_E, CENTER);
drawString(x - 2, y - 20, WindDegToDirection(angle), CENTER);
drawString(x + 3, y + 12, String(angle, 0) + "°", CENTER);
drawString(x + 3, y - 3, String(windspeed, 1) + (Units == "M" ? "m/s" : "mph"), CENTER);
}
String WindDegToDirection(float winddirection) {
int dir = int((winddirection / 22.5) + 0.5);
String Ord_direction[16] = {TXT_N, TXT_NNE, TXT_NE, TXT_ENE, TXT_E, TXT_ESE, TXT_SE, TXT_SSE, TXT_S, TXT_SSW, TXT_SW, TXT_WSW, TXT_W, TXT_WNW, TXT_NW, TXT_NNW};
return Ord_direction[(dir % 16)];
}

The DrawMainWx function prints the current temperature, humidity, and high/low values. DisplayDisplayWindSection draws a compass-like wind direction indicator along with wind speed and cardinal directions. The WindDegToDirection function helps convert wind angle into compass direction text.

void Display_HPP845E_Data(int x, int y) {
display.fillRect(x-45, y-10 , 24, 18, GxEPD_RED);
display.fillTriangle(x - 47, y - 10, x - 33, y - 20, x - 20, y - 10, GxEPD_RED);
//display.fillRect(x - 30, y + 2, 6, 6, GxEPD_WHITE);
display.drawRect(x - 39, y , 5, 5, GxEPD_WHITE);
display.drawRect(x - 33, y , 5, 5, GxEPD_WHITE);
display.drawRect(x - 39, y - 6, 5, 5, GxEPD_WHITE);
display.drawRect(x - 33, y - 6, 5, 5, GxEPD_WHITE);
float temperature = readTemperature();
float humidity = readHumidity();
drawString(x+30, y-2, String(temperature)+"° | "+String(humidity)+"%", CENTER);
}
void DisplayPrecipitationSection(int x, int y) {
display.drawRect(x, y - 1, 167, 56, GxEPD_BLACK); // precipitation outline
u8g2Fonts.setFont(u8g2_font_helvB10_tf);
if (WxForecast[1].Rainfall > 0.005) { // Ignore small amounts
  drawString(x + 5, y + 15, String(WxForecast[1].Rainfall, 2) + (Units == "M" ? "mm" : "in"), LEFT); // Only display rainfall total today if > 0
  addraindrop(x + 65 - (Units == "I" ? 10 : 0), y + 16, 7);
}
if (WxForecast[1].Snowfall > 0.005)  // Ignore small amounts
  drawString(x + 5, y + 35, String(WxForecast[1].Snowfall, 2) + (Units == "M" ? "mm" : "in") + " * *", LEFT); // Only display snowfall total today if > 0
}

Display_HPP845E_Data function is used to show temperature and humidity from the HTU21D sensor in a small panel on the display. DisplayPrecipitationSection shows rainfall and snowfall amounts with simple icons if the values are above a threshold.

void DrawAstronomySection(int x, int y) {
u8g2Fonts.setFont(u8g2_font_helvB08_tf);
display.drawRect(x, y + 64, 167, 48, GxEPD_BLACK);
drawString(x + 7, y + 70, ConvertUnixTime(WxConditions[0].Sunrise + WxConditions[0].Timezone).substring(0, (Units == "M" ? 5 : 7)) + " " + TXT_SUNRISE, LEFT);
drawString(x + 7, y + 85, ConvertUnixTime(WxConditions[0].Sunset + WxConditions[0].Timezone).substring(0, (Units == "M" ? 5 : 7)) + " " + TXT_SUNSET, LEFT);
time_t now = time(NULL);
struct tm * now_utc = gmtime(&now);
const int day_utc   = now_utc->tm_mday;
const int month_utc = now_utc->tm_mon + 1;
const int year_utc  = now_utc->tm_year + 1900;
drawString(x + 7, y + 100, MoonPhase(day_utc, month_utc, year_utc), LEFT);
DrawMoon(x + 105, y + 50, day_utc, month_utc, year_utc, Hemisphere);
}
void DrawMoon(int x, int y, int dd, int mm, int yy, String hemisphere) {
const int diameter = 38;
double Phase = NormalizedMoonPhase(dd, mm, yy);
hemisphere.toLowerCase();
if (hemisphere == "south") Phase = 1 - Phase;
// Draw dark part of moon
display.fillCircle(x + diameter - 1, y + diameter, diameter / 2 + 1, GxEPD_RED);
const int number_of_lines = 90;
for (double Ypos = 0; Ypos <= 45; Ypos++) {
  double Xpos = sqrt(45 * 45 - Ypos * Ypos);
  // Determine the edges of the lighted part of the moon
  double Rpos = 2 * Xpos;
  double Xpos1, Xpos2;
  if (Phase < 0.5) {
    Xpos1 = - Xpos;
    Xpos2 = (Rpos - 2 * Phase * Rpos - Xpos);
  }
  else {
    Xpos1 = Xpos;
    Xpos2 = (Xpos - 2 * Phase * Rpos + Rpos);
  }
  // Draw light part of moon
  double pW1x = (Xpos1 + number_of_lines) / number_of_lines * diameter + x;
  double pW1y = (number_of_lines - Ypos)  / number_of_lines * diameter + y;
  double pW2x = (Xpos2 + number_of_lines) / number_of_lines * diameter + x;
  double pW2y = (number_of_lines - Ypos)  / number_of_lines * diameter + y;
  double pW3x = (Xpos1 + number_of_lines) / number_of_lines * diameter + x;
  double pW3y = (Ypos + number_of_lines)  / number_of_lines * diameter + y;
  double pW4x = (Xpos2 + number_of_lines) / number_of_lines * diameter + x;
  double pW4y = (Ypos + number_of_lines)  / number_of_lines * diameter + y;
  display.drawLine(pW1x, pW1y, pW2x, pW2y, GxEPD_WHITE);
  display.drawLine(pW3x, pW3y, pW4x, pW4y, GxEPD_WHITE);
}
display.drawCircle(x + diameter - 1, y + diameter, diameter / 2 + 1, GxEPD_RED);
}
String MoonPhase(int d, int m, int y) {
int c, e;
double jd;
int b;
if (m < 3) {
  y--;
  m += 12;
}
++m;
c   = 365.25 * y;
e   = 30.6  * m;
jd  = c + e + d - 694039.09;     /* jd is total days elapsed */
jd /= 29.53059;                  /* divide by the moon cycle (29.53 days) */
b   = jd;                        /* int(jd) -> b, take integer part of jd */
jd -= b;                         /* subtract integer part to leave fractional part of original jd */
b   = jd * 8 + 0.5;              /* scale fraction from 0-8 and round by adding 0.5 */
b   = b & 7;                     /* 0 and 8 are the same phase so modulo 8 for 0 */
Hemisphere.toLowerCase();
if (Hemisphere == "south") b = 7 - b;
if (b == 0) return TXT_MOON_NEW;              // New;              0%  illuminated
if (b == 1) return TXT_MOON_WAXING_CRESCENT;  // Waxing crescent; 25%  illuminated
if (b == 2) return TXT_MOON_FIRST_QUARTER;    // First quarter;   50%  illuminated
if (b == 3) return TXT_MOON_WAXING_GIBBOUS;   // Waxing gibbous;  75%  illuminated
if (b == 4) return TXT_MOON_FULL;             // Full;            100% illuminated
if (b == 5) return TXT_MOON_WANING_GIBBOUS;   // Waning gibbous;  75%  illuminated
if (b == 6) return TXT_MOON_THIRD_QUARTER;    // Third quarter;   50%  illuminated
if (b == 7) return TXT_MOON_WANING_CRESCENT;  // Waning crescent; 25%  illuminated
return "";
}

The DrawAstronomySection displays sunrise, sunset, moon phase text, and calls DrawMoon to draw the visual representation of the moon. The moon phase is calculated using the MoonPhase function, which uses date-based math to determine the current phase and return the appropriate label.

void DisplayWXicon(int x, int y, String IconName, bool IconSize) {
Serial.println(IconName);
if      (IconName == "01d" || IconName == "01n")  Sunny(x, y, IconSize, IconName);
else if (IconName == "02d" || IconName == "02n")  MostlySunny(x, y, IconSize, IconName);
else if (IconName == "03d" || IconName == "03n")  Cloudy(x, y, IconSize, IconName);
else if (IconName == "04d" || IconName == "04n")  MostlyCloudy(x, y, IconSize, IconName);
else if (IconName == "09d" || IconName == "09n")  ChanceRain(x, y, IconSize, IconName);
else if (IconName == "10d" || IconName == "10n")  Rain(x, y, IconSize, IconName);
else if (IconName == "11d" || IconName == "11n")  Tstorms(x, y, IconSize, IconName);
else if (IconName == "13d" || IconName == "13n")  Snow(x, y, IconSize, IconName);
else if (IconName == "50d")                       Haze(x, y, IconSize, IconName);
else if (IconName == "50n")                       Fog(x, y, IconSize, IconName);
else                                              Nodata(x, y, IconSize, IconName);
}
void addcloud(int x, int y, int scale, int linesize) {
//Draw cloud outer
display.fillCircle(x - scale * 3, y, scale, GxEPD_RED);                      // Left most circle
display.fillCircle(x + scale * 3, y, scale, GxEPD_RED);                      // Right most circle
display.fillCircle(x - scale, y - scale, scale * 1.4, GxEPD_RED);            // left middle upper circle
display.fillCircle(x + scale * 1.5, y - scale * 1.3, scale * 1.75, GxEPD_RED); // Right middle upper circle
display.fillRect(x - scale * 3 - 1, y - scale, scale * 6, scale * 2 + 1, GxEPD_RED); // Upper and lower lines
//Clear cloud inner
display.fillCircle(x - scale * 3, y, scale - linesize, GxEPD_WHITE);           // Clear left most circle
display.fillCircle(x + scale * 3, y, scale - linesize, GxEPD_WHITE);           // Clear right most circle
display.fillCircle(x - scale, y - scale, scale * 1.4 - linesize, GxEPD_WHITE); // left middle upper circle
display.fillCircle(x + scale * 1.5, y - scale * 1.3, scale * 1.75 - linesize, GxEPD_WHITE); // Right middle upper circle
display.fillRect(x - scale * 3 + 2, y - scale + linesize - 1, scale * 5.9, scale * 2 - linesize * 2 + 2, GxEPD_WHITE); // Upper and lower lines
}
void addraindrop(int x, int y, int scale) {
display.fillCircle(x, y, scale / 2, GxEPD_RED);
display.fillTriangle(x - scale / 2, y, x, y - scale * 1.2, x + scale / 2, y , GxEPD_RED);
x = x + scale * 1.6; y = y + scale / 3;
display.fillCircle(x, y, scale / 2, GxEPD_RED);
display.fillTriangle(x - scale / 2, y, x, y - scale * 1.2, x + scale / 2, y , GxEPD_RED);
}
void addrain(int x, int y, int scale, bool IconSize) {
if (IconSize == SmallIcon) scale *= 1.34;
for (int d = 0; d < 4; d++) {
  addraindrop(x + scale * (7.8 - d * 1.95) - scale * 5.2, y + scale * 2.1 - scale / 6, scale / 1.6);
}
}
void addsnow(int x, int y, int scale, bool IconSize) {
int dxo, dyo, dxi, dyi;
for (int flakes = 0; flakes < 5; flakes++) {
  for (int i = 0; i < 360; i = i + 45) {
    dxo = 0.5 * scale * cos((i - 90) * 3.14 / 180); dxi = dxo * 0.1;
    dyo = 0.5 * scale * sin((i - 90) * 3.14 / 180); dyi = dyo * 0.1;
    display.drawLine(dxo + x + flakes * 1.5 * scale - scale * 3, dyo + y + scale * 2, dxi + x + 0 + flakes * 1.5 * scale - scale * 3, dyi + y + scale * 2, GxEPD_RED);
  }
}
}
void addtstorm(int x, int y, int scale) {
y = y + scale / 2;
for (int i = 0; i < 5; i++) {
  display.drawLine(x - scale * 4 + scale * i * 1.5 + 0, y + scale * 1.5, x - scale * 3.5 + scale * i * 1.5 + 0, y + scale, GxEPD_RED);
  if (scale != Small) {
    display.drawLine(x - scale * 4 + scale * i * 1.5 + 1, y + scale * 1.5, x - scale * 3.5 + scale * i * 1.5 + 1, y + scale, GxEPD_RED);
    display.drawLine(x - scale * 4 + scale * i * 1.5 + 2, y + scale * 1.5, x - scale * 3.5 + scale * i * 1.5 + 2, y + scale, GxEPD_RED);
  }
  display.drawLine(x - scale * 4 + scale * i * 1.5, y + scale * 1.5 + 0, x - scale * 3 + scale * i * 1.5 + 0, y + scale * 1.5 + 0, GxEPD_RED);
  if (scale != Small) {
    display.drawLine(x - scale * 4 + scale * i * 1.5, y + scale * 1.5 + 1, x - scale * 3 + scale * i * 1.5 + 0, y + scale * 1.5 + 1, GxEPD_RED);
    display.drawLine(x - scale * 4 + scale * i * 1.5, y + scale * 1.5 + 2, x - scale * 3 + scale * i * 1.5 + 0, y + scale * 1.5 + 2, GxEPD_RED);
  }
  display.drawLine(x - scale * 3.5 + scale * i * 1.4 + 0, y + scale * 2.5, x - scale * 3 + scale * i * 1.5 + 0, y + scale * 1.5, GxEPD_RED);
  if (scale != Small) {
    display.drawLine(x - scale * 3.5 + scale * i * 1.4 + 1, y + scale * 2.5, x - scale * 3 + scale * i * 1.5 + 1, y + scale * 1.5, GxEPD_RED);
    display.drawLine(x - scale * 3.5 + scale * i * 1.4 + 2, y + scale * 2.5, x - scale * 3 + scale * i * 1.5 + 2, y + scale * 1.5, GxEPD_RED);
  }
}
}
void addsun(int x, int y, int scale, bool IconSize) {
int linesize = 3;
if (IconSize == SmallIcon) linesize = 1;
display.fillRect(x - scale * 2, y, scale * 4, linesize, GxEPD_BLACK);
display.fillRect(x, y - scale * 2, linesize, scale * 4, GxEPD_BLACK);
display.drawLine(x - scale * 1.3, y - scale * 1.3, x + scale * 1.3, y + scale * 1.3, GxEPD_BLACK);
display.drawLine(x - scale * 1.3, y + scale * 1.3, x + scale * 1.3, y - scale * 1.3, GxEPD_BLACK);
if (IconSize == LargeIcon) {
  display.drawLine(1 + x - scale * 1.3, y - scale * 1.3, 1 + x + scale * 1.3, y + scale * 1.3, GxEPD_BLACK);
  display.drawLine(2 + x - scale * 1.3, y - scale * 1.3, 2 + x + scale * 1.3, y + scale * 1.3, GxEPD_BLACK);
  display.drawLine(3 + x - scale * 1.3, y - scale * 1.3, 3 + x + scale * 1.3, y + scale * 1.3, GxEPD_BLACK);
  display.drawLine(1 + x - scale * 1.3, y + scale * 1.3, 1 + x + scale * 1.3, y - scale * 1.3, GxEPD_BLACK);
  display.drawLine(2 + x - scale * 1.3, y + scale * 1.3, 2 + x + scale * 1.3, y - scale * 1.3, GxEPD_BLACK);
  display.drawLine(3 + x - scale * 1.3, y + scale * 1.3, 3 + x + scale * 1.3, y - scale * 1.3, GxEPD_BLACK);
}
display.fillCircle(x, y, scale * 1.3, GxEPD_WHITE);
display.fillCircle(x, y, scale, GxEPD_BLACK);
display.fillCircle(x, y, scale - linesize, GxEPD_WHITE);
}
void addfog(int x, int y, int scale, int linesize, bool IconSize) {
if (IconSize == SmallIcon) {
  y -= 10;
  linesize = 1;
}
for (int i = 0; i < 6; i++) {
  display.fillRect(x - scale * 3, y + scale * 1.5, scale * 6, linesize, GxEPD_RED);
  display.fillRect(x - scale * 3, y + scale * 2.0, scale * 6, linesize, GxEPD_RED);
  display.fillRect(x - scale * 3, y + scale * 2.5, scale * 6, linesize, GxEPD_RED);
}
}
void Sunny(int x, int y, bool IconSize, String IconName) {
int scale = Small, offset = 3;
if (IconSize == LargeIcon) {
  scale = Large;
  y = y - 8;
  offset = 18;
} else y = y - 3; // Shift up small sun icon
if (IconName.endsWith("n")) addmoon(x, y + offset, scale, IconSize);
scale = scale * 1.6;
addsun(x, y, scale, IconSize);
}
void MostlySunny(int x, int y, bool IconSize, String IconName) {
int scale = Small, linesize = 3, offset = 3;
if (IconSize == LargeIcon) {
  scale = Large;
  offset = 10;
} else linesize = 1;
if (IconName.endsWith("n")) addmoon(x, y + offset, scale, IconSize);
addcloud(x, y + offset, scale, linesize);
addsun(x - scale * 1.8, y - scale * 1.8 + offset, scale, IconSize);
}
void MostlyCloudy(int x, int y, bool IconSize, String IconName) {
int scale = Large, linesize = 3;
if (IconSize == SmallIcon) {
  scale = Small;
  linesize = 1;
}
if (IconName.endsWith("n")) addmoon(x, y, scale, IconSize);
addcloud(x, y, scale, linesize);
addsun(x - scale * 1.8, y - scale * 1.8, scale, IconSize);
addcloud(x, y, scale, linesize);
}
void Cloudy(int x, int y, bool IconSize, String IconName) {
int scale = Large, linesize = 3;
if (IconSize == SmallIcon) {
  scale = Small;
  if (IconName.endsWith("n")) addmoon(x, y, scale, IconSize);
  linesize = 1;
  addcloud(x, y, scale, linesize);
}
else {
  y += 10;
  if (IconName.endsWith("n")) addmoon(x, y, scale, IconSize);
  addcloud(x + 30, y - 35, 5, linesize); // Cloud top right
  addcloud(x - 20, y - 25, 7, linesize); // Cloud top left
  addcloud(x, y, scale, linesize);       // Main cloud
}
}
void Rain(int x, int y, bool IconSize, String IconName) {
int scale = Large, linesize = 3;
if (IconSize == SmallIcon) {
  scale = Small;
  linesize = 1;
}
if (IconName.endsWith("n")) addmoon(x, y + 10, scale, IconSize);
addcloud(x, y, scale, linesize);
addrain(x, y, scale, IconSize);
}
void ExpectRain(int x, int y, bool IconSize, String IconName) {
int scale = Large, linesize = 3;
if (IconSize == SmallIcon) {
  scale = Small;
  linesize = 1;
}
if (IconName.endsWith("n")) addmoon(x, y, scale, IconSize);
addsun(x - scale * 1.8, y - scale * 1.8, scale, IconSize);
addcloud(x, y, scale, linesize);
addrain(x, y, scale, IconSize);
}
void ChanceRain(int x, int y, bool IconSize, String IconName) {
int scale = Large, linesize = 3;
if (IconSize == SmallIcon) {
  scale = Small;
  linesize = 1;
}
if (IconName.endsWith("n")) addmoon(x, y, scale, IconSize);
addsun(x - scale * 1.8, y - scale * 1.8, scale, IconSize);
addcloud(x, y, scale, linesize);
addrain(x, y, scale, IconSize);
}
void Tstorms(int x, int y, bool IconSize, String IconName) {
int scale = Large, linesize = 3;
if (IconSize == SmallIcon) {
  scale = Small;
  linesize = 1;
}
if (IconName.endsWith("n")) addmoon(x, y, scale, IconSize);
addcloud(x, y, scale, linesize);
addtstorm(x, y, scale);
}
void Snow(int x, int y, bool IconSize, String IconName) {
int scale = Large, linesize = 3;
if (IconSize == SmallIcon) {
  scale = Small;
  linesize = 1;
}
if (IconName.endsWith("n")) addmoon(x, y + 15, scale, IconSize);
addcloud(x, y, scale, linesize);
addsnow(x, y, scale, IconSize);
}
void Fog(int x, int y, bool IconSize, String IconName) {
int linesize = 3, scale = Large;
if (IconSize == SmallIcon) {
  scale = Small;
  linesize = 1;
}
if (IconName.endsWith("n")) addmoon(x, y, scale, IconSize);
addcloud(x, y - 5, scale, linesize);
addfog(x, y - 5, scale, linesize, IconSize);
}
void Haze(int x, int y, bool IconSize, String IconName) {
int linesize = 3, scale = Large;
if (IconSize == SmallIcon) {
  scale = Small;
  linesize = 1;
}
if (IconName.endsWith("n")) addmoon(x, y, scale, IconSize);
addsun(x, y - 5, scale * 1.4, IconSize);
addfog(x, y - 5, scale * 1.4, linesize, IconSize);
}
void CloudCover(int x, int y, int CCover) {
addcloud(x - 9, y - 3, Small * 0.5, 2); // Cloud top left
addcloud(x + 3, y - 3, Small * 0.5, 2); // Cloud top right
addcloud(x, y,         Small * 0.5, 2); // Main cloud
u8g2Fonts.setFont(u8g2_font_helvB08_tf);
drawString(x + 15, y - 5, String(CCover) + "%", LEFT);
}
void Visibility(int x, int y, String Visi) {
y = y - 3; //
float start_angle = 0.52, end_angle = 2.61;
int r = 10;
for (float i = start_angle; i < end_angle; i = i + 0.05) {
  display.drawPixel(x + r * cos(i), y - r / 2 + r * sin(i), GxEPD_RED);
  display.drawPixel(x + r * cos(i), 1 + y - r / 2 + r * sin(i), GxEPD_RED);
}
start_angle = 3.61; end_angle = 5.78;
for (float i = start_angle; i < end_angle; i = i + 0.05) {
  display.drawPixel(x + r * cos(i), y + r / 2 + r * sin(i), GxEPD_RED);
  display.drawPixel(x + r * cos(i), 1 + y + r / 2 + r * sin(i), GxEPD_RED);
}
display.fillCircle(x, y, r / 4, GxEPD_RED);
u8g2Fonts.setFont(u8g2_font_helvB08_tf);
drawString(x + 12, y - 3, Visi, LEFT);
}
void addmoon(int x, int y, int scale, bool IconSize) {
if (IconSize == LargeIcon) {
  x = x + 12; y = y + 12;
  display.fillCircle(x - 50, y - 55, scale, GxEPD_RED);
  display.fillCircle(x - 35, y - 55, scale * 1.6, GxEPD_WHITE);
}
else
{
  display.fillCircle(x - 20, y - 12, scale, GxEPD_RED);
  display.fillCircle(x - 15, y - 12, scale * 1.6, GxEPD_WHITE);
}
}
void Nodata(int x, int y, bool IconSize, String IconName) {
if (IconSize == LargeIcon) u8g2Fonts.setFont(u8g2_font_helvB24_tf); else u8g2Fonts.setFont(u8g2_font_helvB10_tf);
drawString(x - 3, y - 8, "?", CENTER);
u8g2Fonts.setFont(u8g2_font_helvB08_tf);
}
void DrawBattery(int x, int y) {
uint8_t percentage = 100;
float voltage = analogRead(3) / 4096.0 * 7.46;
if (voltage > 1 ) { // Only display if there is a valid reading
  Serial.println("Voltage = " + String(voltage));
  percentage = 2836.9625 * pow(voltage, 4) - 43987.4889 * pow(voltage, 3) + 255233.8134 * pow(voltage, 2) - 656689.7123 * voltage + 632041.7303;
  if (voltage >= 4.20) percentage = 100;
  if (voltage <= 3.50) percentage = 0;
  display.drawRect(x + 15, y - 12, 19, 10, GxEPD_BLACK);
  display.fillRect(x + 34, y - 10, 2, 5, GxEPD_BLACK);
  display.fillRect(x + 17, y - 10, 15 * percentage / 100.0, 6, GxEPD_RED);
  drawString(x + 65, y - 11, String(percentage) + "%", RIGHT);
  //drawString(x + 13, y + 5,  String(voltage, 2) + "v", CENTER);
}
}
void DrawGraph(int x_pos, int y_pos, int gwidth, int gheight, float Y1Min, float Y1Max, String title, float DataArray[], int readings, boolean auto_scale, boolean barchart_mode) {
#define auto_scale_margin 0 // Sets the autoscale increment, so axis steps up in units of e.g. 3
#define y_minor_axis 5      // 5 y-axis division markers
float maxYscale = -10000;
float minYscale =  10000;
int last_x, last_y;
float x1, y1, x2, y2;
if (auto_scale == true) {
  for (int i = 1; i < readings; i++ ) {
    if (DataArray[i] >= maxYscale) maxYscale = DataArray[i];
    if (DataArray[i] <= minYscale) minYscale = DataArray[i];
  }
  maxYscale = round(maxYscale + auto_scale_margin); // Auto scale the graph and round to the nearest value defined, default was Y1Max
  Y1Max = round(maxYscale + 0.5);
  if (minYscale != 0) minYscale = round(minYscale - auto_scale_margin); // Auto scale the graph and round to the nearest value defined, default was Y1Min
  Y1Min = round(minYscale);
}
// Draw the graph
last_x = x_pos + 1;
last_y = y_pos + (Y1Max - constrain(DataArray[1], Y1Min, Y1Max)) / (Y1Max - Y1Min) * gheight;
display.drawRect(x_pos, y_pos, gwidth + 3, gheight + 2, GxEPD_BLACK);
u8g2Fonts.setFont(u8g2_font_helvB08_tf);
drawString(x_pos + gwidth / 2, y_pos - 12, title, CENTER);
// Draw the graph
last_x = x_pos;
last_y = y_pos + (Y1Max - constrain(DataArray[1], Y1Min, Y1Max)) / (Y1Max - Y1Min) * gheight;
display.drawRect(x_pos, y_pos, gwidth + 3, gheight + 2, GxEPD_BLACK);
drawString(x_pos + gwidth / 2, y_pos - 13, title, CENTER);
// Draw the data
for (int gx = 0; gx < readings; gx++) {
  y2 = y_pos + (Y1Max - constrain(DataArray[gx], Y1Min, Y1Max)) / (Y1Max - Y1Min) * gheight + 1;
  if (barchart_mode) {
    x2 = x_pos + gx * (gwidth / readings) + 2;
    display.fillRect(x2, y2, (gwidth / readings) - 2, y_pos + gheight - y2 + 2, GxEPD_RED);
  }
  else
  {
    x2 = x_pos + gx * gwidth / (readings - 1) + 1; // max_readings is the global variable that sets the maximum data that can be plotted
    display.drawLine(last_x, last_y, x2, y2, GxEPD_RED);
  }
  last_x = x2;
  last_y = y2;
}
//Draw the Y-axis scale
#define number_of_dashes 15
for (int spacing = 0; spacing <= y_minor_axis; spacing++) {
  for (int j = 0; j < number_of_dashes; j++) { // Draw dashed graph grid lines
    if (spacing < y_minor_axis) display.drawFastHLine((x_pos + 3 + j * gwidth / number_of_dashes), y_pos + (gheight * spacing / y_minor_axis), gwidth / (2 * number_of_dashes), GxEPD_BLACK);
  }
  if ((Y1Max - (float)(Y1Max - Y1Min) / y_minor_axis * spacing) < 5 || title == TXT_PRESSURE_IN) {
    drawString(x_pos, y_pos + gheight * spacing / y_minor_axis - 5, String((Y1Max - (float)(Y1Max - Y1Min) / y_minor_axis * spacing + 0.01), 1), RIGHT);
  }
  else
  {
    if (Y1Min < 1 && Y1Max < 10)
      drawString(x_pos - 3, y_pos + gheight * spacing / y_minor_axis - 5, String((Y1Max - (float)(Y1Max - Y1Min) / y_minor_axis * spacing + 0.01), 1), RIGHT);
    else
      drawString(x_pos - 3, y_pos + gheight * spacing / y_minor_axis - 5, String((Y1Max - (float)(Y1Max - Y1Min) / y_minor_axis * spacing + 0.01), 0), RIGHT);
  }
}
for (int i = 0; i <= 2; i++) {
  drawString(15 + x_pos + gwidth / 3 * i, y_pos + gheight + 3, String(i), LEFT);
}
drawString(x_pos + gwidth / 2, y_pos + gheight + 10, TXT_DAYS, CENTER);
}

The DisplayWXicon function selects the appropriate drawing routine based on the weather icon code from OpenWeatherMap. It maps icon strings like "01d" or "09n" to corresponding symbol-drawing functions such as Sunny, Rain, Fog and other functions mentioned above. These functions are used to draw different icons using just basic graphic primitives, depending on the weather data. While the drawBattery displays the battery status while the drawGrapgh function is responsible for the forecast graphs. 

void StopWiFi() {
WiFi.disconnect();
WiFi.mode(WIFI_OFF);
}
//#########################################################################################
boolean SetupTime() {
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer, "time.nist.gov"); //(gmtOffset_sec, daylightOffset_sec, ntpServer)
setenv("TZ", Timezone, 1);  //setenv()adds the "TZ" variable to the environment with a value TimeZone, only used if set to 1, 0 means no change
tzset(); // Set the TZ environment variable
delay(100);
bool TimeStatus = UpdateLocalTime();
return TimeStatus;
}
boolean UpdateLocalTime() {
struct tm timeinfo;
char   time_output[30], day_output[30], update_time[30];
while (!getLocalTime(&timeinfo, 10000)) { // Wait for 5-sec for time to synchronise
  Serial.println("Failed to obtain time");
  return false;
}
CurrentHour = timeinfo.tm_hour;
CurrentMin  = timeinfo.tm_min;
CurrentSec  = timeinfo.tm_sec;
2017 14:05:49
if (Units == "M") {
  sprintf(day_output, "%s  %02u-%s-%04u", weekday_D[timeinfo.tm_wday], timeinfo.tm_mday, month_M[timeinfo.tm_mon], (timeinfo.tm_year) + 1900);
  strftime(update_time, sizeof(update_time), "%H:%M", &timeinfo);  // Creates: '@ 14:05:49'   and change from 30 to 8 <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
  sprintf(time_output, "%s", update_time);
}
else
{
  strftime(day_output, sizeof(day_output), "%a  %b-%d-%Y", &timeinfo); // Creates  'Sat May-31-2019'
  strftime(update_time, sizeof(update_time), "%r", &timeinfo);         // Creates: '@ 02:05:49pm'
  sprintf(time_output, "%s", update_time);
}
date_str = day_output;
time_str = time_output;
return true;
}

The StartWiFi function connects the ESP32 to the configured Wi-Fi network and checks the status within a 15-second timeout. If successful, it records the signal strength. The StopWiFi function disconnects and powers down the Wi-Fi module to save energy. And the SetupTime function configures the ESP32 to get the correct local time using NTP servers. It sets the timezone and calls UpdateLocalTime to retrieve and format the current time and date, storing them in global variables for display.

void drawString(int x, int y, String text, alignment align) {
int16_t  x1, y1; //the bounds of x,y and w and h of the variable 'text' in pixels.
uint16_t w, h;
display.setTextWrap(false);
display.getTextBounds(text, x, y, &x1, &y1, &w, &h);
if (align == RIGHT)  x = x - w;
if (align == CENTER) x = x - w / 2;
u8g2Fonts.setCursor(x, y + h);
u8g2Fonts.print(text);
}
void drawStringMaxWidth(int x, int y, unsigned int text_width, String text, alignment align) {
int16_t  x1, y1; //the bounds of x,y and w and h of the variable 'text' in pixels.
uint16_t w, h;
display.getTextBounds(text, x, y, &x1, &y1, &w, &h);
if (align == RIGHT)  x = x - w;
if (align == CENTER) x = x - w / 2;
u8g2Fonts.setCursor(x, y);
if (text.length() > text_width * 2) {
  u8g2Fonts.setFont(u8g2_font_helvB10_tf);
  text_width = 42;
  y = y - 3;
}
u8g2Fonts.println(text.substring(0, text_width));
if (text.length() > text_width) {
  u8g2Fonts.setCursor(x, y + h + 15);
  String secondLine = text.substring(text_width);
  secondLine.trim(); // Remove any leading spaces
  u8g2Fonts.println(secondLine);
}
}
void InitialiseDisplay() {
display.init(115200, true, 2, false);
// display.init(); for older Waveshare HAT's
SPI.end();
SPI.begin(EPD_SCK, EPD_MISO, EPD_MOSI, EPD_CS);
u8g2Fonts.begin(display); // connect u8g2 procedures to Adafruit GFX
u8g2Fonts.setFontMode(1);                  // use u8g2 transparent mode (this is default)
u8g2Fonts.setFontDirection(0);             // left to right (this is default)
u8g2Fonts.setForegroundColor(GxEPD_BLACK); // apply Adafruit GFX color
u8g2Fonts.setBackgroundColor(GxEPD_WHITE); // apply Adafruit GFX color
u8g2Fonts.setFont(u8g2_font_helvB10_tf);   // select u8g2 font from here: https://github.com/olikraus/u8g2/wiki/fntlistall
display.fillScreen(GxEPD_WHITE);
display.setFullWindow();
}

InitialiseDisplay sets up the E-Ink display with the required SPI configuration and initialises the font rendering engine. It clears the screen and sets the display to full-window mode for updates. The drawString and drawStringMaxWidth functions handle different text elements in the UI.

void softReset() {
  Wire.beginTransmission(HTU21D_ADDRESS);
  Wire.write(SOFT_RESET);
  Wire.endTransmission();
  delay(15);  // Wait for reset to complete
}
void disableHeater() {
  // Read the user register
  Wire.beginTransmission(HTU21D_ADDRESS);
  Wire.write(USER_REGISTER_READ);
  Wire.endTransmission();
 
  Wire.requestFrom(HTU21D_ADDRESS, 1);
  if (Wire.available()) {
      uint8_t userReg = Wire.read();  // Read the user register value
      // Clear bit 2 to disable the heater
      userReg &= ~(1 << 2);
      // Write the updated value back to the user register
      Wire.beginTransmission(HTU21D_ADDRESS);
      Wire.write(USER_REGISTER_WRITE);
      Wire.write(userReg);
      Wire.endTransmission();
  }
}
// Function to read temperature
float readTemperature() {
  Wire.beginTransmission(HTU21D_ADDRESS);
  Wire.write(TEMP_MEASURE_NO_HOLD); // Send temperature measurement command
  Wire.endTransmission();
  delay(50); // Wait for conversion (50ms for temperature)
  Wire.requestFrom(HTU21D_ADDRESS, 3); // Read 3 bytes (2 data + 1 CRC)
  if (Wire.available() == 3) {
      uint16_t rawTemp = (Wire.read() << 8) | Wire.read();
      Wire.read();  // Read and discard CRC
      rawTemp &= 0xFFFC; // Clear status bits
      Serial.print("T: ");
      Serial.print(rawTemp);
      Serial.println(" °C");
      return -46.85 + (175.72 * rawTemp / 65536.0);  // Convert to Celsius
  }
  return -999.0; // Error value
}
// Function to read humidity
float readHumidity() {
  Wire.beginTransmission(HTU21D_ADDRESS);
  Wire.write(HUMID_MEASURE_NO_HOLD); // Send humidity measurement command
  Wire.endTransmission();
  delay(16); // Wait for conversion (16ms for humidity)
  Wire.requestFrom(HTU21D_ADDRESS, 3); // Read 3 bytes (2 data + 1 CRC)
  if (Wire.available() == 3) {
      uint16_t rawHumidity = (Wire.read() << 8) | Wire.read();
      Wire.read();  // Read and discard CRC
      rawHumidity &= 0xFFFC; // Clear status bits
      Serial.print("H: ");
      Serial.print(rawHumidity);
      Serial.println(" °C");
      return -6.0 + (125.0 * rawHumidity / 65536.0);  // Convert to %RH
  }
  return -999.0; // Error value
}

The remaining functions are responsible for the indoor temperature and humidity sensor management. The softReset sends a soft reset command to the HTU21D sensor to ensure it's ready before use. disableHeater reads and modifies the sensor’s configuration register to make sure the internal heater is turned off, improving measurement accuracy. The readTemperature and readHumidity functions communicate with the HTU21D sensor over I2C to get raw temperature and humidity values, convert them into human-readable format, and return them for display.

GitHub Repository with Code and Circuit

Code and Schematics of Desktop Weather StationCode and Schematics of Desktop Weather Station Zip File

Projects using ESP32-S3

If you are interested in more ESP32 projects, such as projects featuring ESP32-S3, environmental sensors or Eink displays, check out the following recommendations

Build a Smart Digital Game Board Using Multicolour PCB

Build a Smart Digital Game Board Using Multicolour PCB

Learn how to build a smart digital Ludo game board using a custom multicolor PCB, ESP32-S3 microcontroller, and interactive components. It combines touch-based controls, RGB LEDs, and a central IPS LCD display to recreate the classic game with a modern, engaging twist.

 Interfacing 1.54-inch E-Paper Display with Arduino UNO

Interfacing 1.54-inch E-Paper Display with Arduino UNO

Explore how to interface a 1.54-inch E-Paper display with an Arduino UNO using SPI communication. It covers wiring, code implementation, and showcases how to display text and images on the screen.

Interfacing DHT11 Humidity & Temperature Sensor with Arduino

Interfacing DHT11 Humidity & Temperature Sensor with Arduino

Explains how to connect and use the DHT11 temperature and humidity sensor with an Arduino UNO. Learn the process of wiring the sensor, installing libraries, and writing the code to read and display the sensor's data on the Arduino.

Complete Project Code

#include "config.h"  // See 'config.h' file and enter your OWM API key and set the Wifi SSID and PASSWORD
#include <ArduinoJson.h>       // https://github.com/bblanchon/ArduinoJson
#include <WiFi.h>              // Built-in
#include "time.h"              // Built-in
#include <SPI.h>               // Built-in
#define  ENABLE_GxEPD2_display 0
#include <GxEPD2_BW.h>         // GxEPD2 v1.6.3
#include <GxEPD2_3C.h>
#include <U8g2_for_Adafruit_GFX.h>
#include "epaper_fonts.h"
#include "forecast_record.h"
#include "lang.h"
#include <Wire.h>
#define HTU21D_ADDRESS 0x40   // I2C address of HTU21D
#define TEMP_MEASURE_NO_HOLD  0xF3
#define HUMID_MEASURE_NO_HOLD 0xF5
#define SOFT_RESET            0xFE
#define USER_REGISTER_READ  0xE7
#define USER_REGISTER_WRITE 0xE6
#define SCREEN_WIDTH  400.0    // Set for landscape mode, don't remove the decimal place!
#define SCREEN_HEIGHT 300.0
enum alignment {LEFT, RIGHT, CENTER};
// Connections for the EPD Display
static const uint8_t EPD_BUSY = 13;  // to EPD BUSY
static const uint8_t EPD_CS   = 10;  // to EPD CS
static const uint8_t EPD_RST  = 14; // to EPD RST
static const uint8_t EPD_DC   = 15; // to EPD DC
static const uint8_t EPD_SCK  = 12; // to EPD CLK
static const uint8_t EPD_MISO = -1; // Master-In Slave-Out not used, as no data from display
static const uint8_t EPD_MOSI = 11; // to EPD DIN

GxEPD2_3C<GxEPD2_420c, GxEPD2_420c::HEIGHT> display(GxEPD2_420c(/*CS=5*/ EPD_CS, /*DC=*/ EPD_DC, /*RST=*/ EPD_RST, /*BUSY=*/ EPD_BUSY)); //
U8G2_FOR_ADAFRUIT_GFX u8g2Fonts;  // Select u8g2 font from here: https://github.com/olikraus/u8g2/wiki/fntlistall
// Using fonts:
// u8g2_font_helvB08_tf
// u8g2_font_helvB10_tf
// u8g2_font_helvB12_tf
// u8g2_font_helvB14_tf
// u8g2_font_helvB18_tf
// u8g2_font_helvB24_tf
boolean LargeIcon = true, SmallIcon = false;
#define Large  11           // For icon drawing, needs to be odd number for best effect
#define Small  5            // For icon drawing, needs to be odd number for best effect
String  time_str, date_str; // strings to hold time and received weather data
int     wifi_signal, CurrentHour = 0, CurrentMin = 0, CurrentSec = 0;
long    StartTime = 0;
//################ PROGRAM VARIABLES and OBJECTS ################
#define max_readings 24
Forecast_record_type  WxConditions[1];
Forecast_record_type  WxForecast[max_readings];
#include <common.h>
#define autoscale_on  true
#define autoscale_off false
#define barchart_on   true
#define barchart_off  false
float pressure_readings[max_readings]    = {0};
float temperature_readings[max_readings] = {0};
float humidity_readings[max_readings]    = {0};
float rain_readings[max_readings]        = {0};
float snow_readings[max_readings]        = {0};
long SleepDuration = 15; // Sleep time in minutes, aligned to the nearest minute boundary, so if 30 will always update at 00 or 30 past the hour
//#########################################################################################
void setup() {
 StartTime = millis();
 pinMode(16, OUTPUT);
 digitalWrite(16, LOW);
 pinMode(42, OUTPUT); 
 digitalWrite(42, HIGH);
 delay(100);
 Serial.begin(115200);
 Wire.begin();  // Initialize I2C
 softReset();  // Reset the sensor before starting
 disableHeater(); // Ensure the heater is turned off
 if (StartWiFi() == WL_CONNECTED && SetupTime() == true) {
   InitialiseDisplay(); // Give screen time to initialise by getting weather data!
   byte Attempts = 1;
   bool RxWeather = false, RxForecast = false;
   WiFiClient client;   // wifi client object
   while ((RxWeather == false || RxForecast == false) && Attempts <= 2) { // Try up-to 2 time for Weather and Forecast data
     if (RxWeather  == false) RxWeather  = obtain_wx_data(client, "weather");
     if (RxForecast == false) RxForecast = obtain_wx_data(client, "forecast");
     Attempts++;
   }
   if (RxWeather && RxForecast) { // Only if received both Weather or Forecast proceed
      StopWiFi(); // Reduces power consumption
     DisplayWeather();
     display.display(false); // Full screen update mode
   }
 }
 BeginSleep();
}
//#########################################################################################
void loop() { // this will never run!
yield();
}
//#########################################################################################
void BeginSleep() {
 display.powerOff();
 digitalWrite(16, HIGH);
 digitalWrite(42, LOW);
 long SleepTimer = SleepDuration * 60; // theoretical sleep duration
 long offset = (CurrentMin % SleepDuration) * 60 + CurrentSec; // number of seconds elapsed after last theoretical wake-up time point
 if (offset > SleepDuration/2 * 60){ // waking up too early will cause <offset> too large
   offset -= SleepDuration * 60; // then we should make it negative, so as to extend this coming sleep duration
 }
 esp_sleep_enable_timer_wakeup((SleepTimer - offset) * 1000000LL); // do compensation to cover ESP32 RTC timer source inaccuracies
 Serial.println("Entering " + String(SleepTimer) + "-secs of sleep time");
 Serial.println("Awake for : " + String((millis() - StartTime) / 1000.0, 3) + "-secs");
 Serial.println("Starting deep-sleep period...");
 esp_deep_sleep_start();      // Sleep for e.g. 30 minutes
}
//#########################################################################################
void DisplayWeather() {                 // 4.2" e-paper display is 400x300 resolution
 DrawHeadingSection();                 // Top line of the display
 DrawMainWeatherSection(172, 70);      // Centre section of display for Location, temperature, Weather report, current Wx Symbol and wind direction
 DrawForecastSection(233, 15);         // 3hr forecast boxes
 DisplayPrecipitationSection(233, 82); // Precipitation sectio
 if (WxConditions[0].Visibility > 0) Visibility(335, 100, String(WxConditions[0].Visibility) + "M");
 if (WxConditions[0].Cloudcover > 0) CloudCover(350, 125, WxConditions[0].Cloudcover);
 DrawAstronomySection(233, 74);        // Astronomy section Sun rise/set, Moon phase and Moon icon
}
//#########################################################################################
void DrawHeadingSection() {
 u8g2Fonts.setFont(u8g2_font_helvB08_tf);
 display.setTextColor(GxEPD_RED);
 drawString(SCREEN_WIDTH / 2, 2, City, CENTER);
 display.setTextColor(GxEPD_BLACK);
 drawString(4, 2, date_str, LEFT);
 drawString(120, 2, time_str, LEFT);
 DrawBattery(SCREEN_WIDTH-70, 14);
 display.drawLine(0, 14, SCREEN_WIDTH, 14, GxEPD_BLACK);
}
//#########################################################################################
void DrawMainWeatherSection(int x, int y) { 
 Display_HPP845E_Data(x - 120, y + 58);
 DisplayDisplayWindSection(x - 115, y - 3, WxConditions[0].Winddir, WxConditions[0].Windspeed, 40);
 DisplayWXicon(x + 5, y - 5, WxConditions[0].Icon, LargeIcon);
 u8g2Fonts.setFont(u8g2_font_helvB10_tf);
 u8g2Fonts.setFont(u8g2_font_helvB12_tf);
 String Wx_Description = WxConditions[0].Forecast0;
 if (WxConditions[0].Forecast1 != "") Wx_Description += " & " +  WxConditions[0].Forecast1;
 if (WxConditions[0].Forecast2 != "" && WxConditions[0].Forecast1 != WxConditions[0].Forecast2) Wx_Description += " & " +  WxConditions[0].Forecast2;
 drawStringMaxWidth(x - 170, y + 83, 28, TitleCase(Wx_Description), LEFT);
 DrawMainWx(x, y + 60);
 display.drawRect(0, y + 68, 232, 48, GxEPD_BLACK);
}
//#########################################################################################
void DrawForecastSection(int x, int y) {
 u8g2Fonts.setFont(u8g2_font_helvB10_tf);
 DrawForecastWeather(x, y, 0);
 DrawForecastWeather(x + 56, y, 1);
 DrawForecastWeather(x + 112, y, 2);
 //       (x,y,width,height,MinValue, MaxValue, Title, Data Array, AutoScale, ChartMode)
 for (int r = 0; r < max_readings; r++) {
   if (Units == "I") {
     pressure_readings[r] = WxForecast[r].Pressure * 0.02953;
     rain_readings[r]     = WxForecast[r].Rainfall * 0.0393701;
   }
   else {
     pressure_readings[r] = WxForecast[r].Pressure;
     rain_readings[r]     = WxForecast[r].Rainfall;
   }
   temperature_readings[r] = WxForecast[r].Temperature;
 }
 display.drawLine(0, y + 172, SCREEN_WIDTH, y + 172, GxEPD_BLACK);
 u8g2Fonts.setFont(u8g2_font_helvB12_tf);
 drawString(SCREEN_WIDTH / 2, y + 180, TXT_FORECAST_VALUES, CENTER);
 u8g2Fonts.setFont(u8g2_font_helvB10_tf);
 DrawGraph(SCREEN_WIDTH / 400 * 30,  SCREEN_HEIGHT / 300 * 221, SCREEN_WIDTH / 4, SCREEN_HEIGHT / 5, 900, 1050, Units == "M" ? TXT_PRESSURE_HPA : TXT_PRESSURE_IN, pressure_readings, max_readings, autoscale_on, barchart_off);
 DrawGraph(SCREEN_WIDTH / 400 * 158, SCREEN_HEIGHT / 300 * 221, SCREEN_WIDTH / 4, SCREEN_HEIGHT / 5, 10, 30, Units == "M" ? TXT_TEMPERATURE_C : TXT_TEMPERATURE_F, temperature_readings, max_readings, autoscale_on, barchart_off);
 DrawGraph(SCREEN_WIDTH / 400 * 288, SCREEN_HEIGHT / 300 * 221, SCREEN_WIDTH / 4, SCREEN_HEIGHT / 5, 0, 30, Units == "M" ? TXT_RAINFALL_MM : TXT_RAINFALL_IN, rain_readings, max_readings, autoscale_on, barchart_on);
}
//#########################################################################################
void DrawForecastWeather(int x, int y, int index) {
 u8g2Fonts.setFont(u8g2_font_helvB08_tf);
 display.drawRect(x, y, 55, 65, GxEPD_BLACK);
 display.drawLine(x + 1, y + 13, x + 54, y + 13, GxEPD_BLACK);
 DisplayWXicon(x + 28, y + 35, WxForecast[index].Icon, SmallIcon);
 drawString(x + 31, y + 3, String(ConvertUnixTime(WxForecast[index].Dt + WxConditions[0].Timezone).substring(0,5)), CENTER);
 drawString(x + 41, y + 52, String(WxForecast[index].High, 0) + "° / " + String(WxForecast[index].Low, 0) + "°", CENTER);
}
//#########################################################################################
void DrawMainWx(int x, int y) {
 u8g2Fonts.setFont(u8g2_font_helvB14_tf);
 drawString(x - 25, y - 22, String(WxConditions[0].Temperature, 1) + "°" + (Units == "M" ? "C" : "F"), CENTER); // Show current Temperature
 u8g2Fonts.setFont(u8g2_font_helvB12_tf);
 drawString(x - 15, y - 3, String(WxConditions[0].High, 0) + "° | " + String(WxConditions[0].Low, 0) + "°", CENTER); // Show forecast high and Low
 drawString(x + 30, y - 22, String(WxConditions[0].Humidity, 0) + "%", CENTER);
 u8g2Fonts.setFont(u8g2_font_helvB10_tf);
 drawString(x + 32, y - 3, "RH", CENTER);
}
//#########################################################################################
void DisplayDisplayWindSection(int x, int y, float angle, float windspeed, int Cradius) {
 arrow(x, y, Cradius - 7, angle, 12, 18); // Show wind direction on outer circle of width and length
 u8g2Fonts.setFont(u8g2_font_helvB08_tf);
 int dxo, dyo, dxi, dyi;
 display.drawLine(0, 15, 0, y + Cradius + 30, GxEPD_RED);
 display.drawCircle(x, y, Cradius, GxEPD_RED);     // Draw compass circle
 display.drawCircle(x, y, Cradius + 1, GxEPD_RED); // Draw compass circle
 display.drawCircle(x, y, Cradius * 0.7, GxEPD_RED); // Draw compass inner circle
 for (float a = 0; a < 360; a = a + 22.5) {
   dxo = Cradius * cos((a - 90) * PI / 180);
   dyo = Cradius * sin((a - 90) * PI / 180);
   if (a == 45)  drawString(dxo + x + 10, dyo + y - 10, TXT_NE, CENTER);
   if (a == 135) drawString(dxo + x + 7,  dyo + y + 5,  TXT_SE, CENTER);
   if (a == 225) drawString(dxo + x - 15, dyo + y,      TXT_SW, CENTER);
   if (a == 315) drawString(dxo + x - 15, dyo + y - 10, TXT_NW, CENTER);
   dxi = dxo * 0.9;
   dyi = dyo * 0.9;
   display.drawLine(dxo + x, dyo + y, dxi + x, dyi + y, GxEPD_RED);
   dxo = dxo * 0.7;
   dyo = dyo * 0.7;
   dxi = dxo * 0.9;
   dyi = dyo * 0.9;
   display.drawLine(dxo + x, dyo + y, dxi + x, dyi + y, GxEPD_RED);
 }
 drawString(x, y - Cradius - 10,     TXT_N, CENTER);
 drawString(x, y + Cradius + 5,      TXT_S, CENTER);
 drawString(x - Cradius - 10, y - 3, TXT_W, CENTER);
 drawString(x + Cradius + 8,  y - 3, TXT_E, CENTER);
 drawString(x - 2, y - 20, WindDegToDirection(angle), CENTER);
 drawString(x + 3, y + 12, String(angle, 0) + "°", CENTER);
 drawString(x + 3, y - 3, String(windspeed, 1) + (Units == "M" ? "m/s" : "mph"), CENTER);
}
//#########################################################################################
String WindDegToDirection(float winddirection) {
 int dir = int((winddirection / 22.5) + 0.5);
 String Ord_direction[16] = {TXT_N, TXT_NNE, TXT_NE, TXT_ENE, TXT_E, TXT_ESE, TXT_SE, TXT_SSE, TXT_S, TXT_SSW, TXT_SW, TXT_WSW, TXT_W, TXT_WNW, TXT_NW, TXT_NNW};
 return Ord_direction[(dir % 16)];
}
//#########################################################################################
void Display_HPP845E_Data(int x, int y) {
 display.fillRect(x-45, y-10 , 24, 18, GxEPD_RED);
 display.fillTriangle(x - 47, y - 10, x - 33, y - 20, x - 20, y - 10, GxEPD_RED);
 //display.fillRect(x - 30, y + 2, 6, 6, GxEPD_WHITE);
 display.drawRect(x - 39, y , 5, 5, GxEPD_WHITE);
 display.drawRect(x - 33, y , 5, 5, GxEPD_WHITE);
 display.drawRect(x - 39, y - 6, 5, 5, GxEPD_WHITE);
 display.drawRect(x - 33, y - 6, 5, 5, GxEPD_WHITE);
 float temperature = readTemperature();
 float humidity = readHumidity();
 drawString(x+30, y-2, String(temperature)+"° | "+String(humidity)+"%", CENTER);
 
}
//#########################################################################################
void DisplayPrecipitationSection(int x, int y) {
 display.drawRect(x, y - 1, 167, 56, GxEPD_BLACK); // precipitation outline
 u8g2Fonts.setFont(u8g2_font_helvB10_tf);
 if (WxForecast[1].Rainfall > 0.005) { // Ignore small amounts
   drawString(x + 5, y + 15, String(WxForecast[1].Rainfall, 2) + (Units == "M" ? "mm" : "in"), LEFT); // Only display rainfall total today if > 0
   addraindrop(x + 65 - (Units == "I" ? 10 : 0), y + 16, 7);
 }
 if (WxForecast[1].Snowfall > 0.005)  // Ignore small amounts
   drawString(x + 5, y + 35, String(WxForecast[1].Snowfall, 2) + (Units == "M" ? "mm" : "in") + " * *", LEFT); // Only display snowfall total today if > 0
}
//#########################################################################################
void DrawAstronomySection(int x, int y) {
 u8g2Fonts.setFont(u8g2_font_helvB08_tf);
 display.drawRect(x, y + 64, 167, 48, GxEPD_BLACK);
 drawString(x + 7, y + 70, ConvertUnixTime(WxConditions[0].Sunrise + WxConditions[0].Timezone).substring(0, (Units == "M" ? 5 : 7)) + " " + TXT_SUNRISE, LEFT);
 drawString(x + 7, y + 85, ConvertUnixTime(WxConditions[0].Sunset + WxConditions[0].Timezone).substring(0, (Units == "M" ? 5 : 7)) + " " + TXT_SUNSET, LEFT);
 time_t now = time(NULL);
 struct tm * now_utc = gmtime(&now);
 const int day_utc   = now_utc->tm_mday;
 const int month_utc = now_utc->tm_mon + 1;
 const int year_utc  = now_utc->tm_year + 1900;
 drawString(x + 7, y + 100, MoonPhase(day_utc, month_utc, year_utc), LEFT);
 DrawMoon(x + 105, y + 50, day_utc, month_utc, year_utc, Hemisphere);
}
//#########################################################################################
void DrawMoon(int x, int y, int dd, int mm, int yy, String hemisphere) {
 const int diameter = 38;
 double Phase = NormalizedMoonPhase(dd, mm, yy);
 hemisphere.toLowerCase();
 if (hemisphere == "south") Phase = 1 - Phase;
 // Draw dark part of moon
 display.fillCircle(x + diameter - 1, y + diameter, diameter / 2 + 1, GxEPD_RED);
 const int number_of_lines = 90;
 for (double Ypos = 0; Ypos <= 45; Ypos++) {
   double Xpos = sqrt(45 * 45 - Ypos * Ypos);
   // Determine the edges of the lighted part of the moon
   double Rpos = 2 * Xpos;
   double Xpos1, Xpos2;
   if (Phase < 0.5) {
     Xpos1 = - Xpos;
     Xpos2 = (Rpos - 2 * Phase * Rpos - Xpos);
   }
   else {
     Xpos1 = Xpos;
     Xpos2 = (Xpos - 2 * Phase * Rpos + Rpos);
   }
   // Draw light part of moon
   double pW1x = (Xpos1 + number_of_lines) / number_of_lines * diameter + x;
   double pW1y = (number_of_lines - Ypos)  / number_of_lines * diameter + y;
   double pW2x = (Xpos2 + number_of_lines) / number_of_lines * diameter + x;
   double pW2y = (number_of_lines - Ypos)  / number_of_lines * diameter + y;
   double pW3x = (Xpos1 + number_of_lines) / number_of_lines * diameter + x;
   double pW3y = (Ypos + number_of_lines)  / number_of_lines * diameter + y;
   double pW4x = (Xpos2 + number_of_lines) / number_of_lines * diameter + x;
   double pW4y = (Ypos + number_of_lines)  / number_of_lines * diameter + y;
   display.drawLine(pW1x, pW1y, pW2x, pW2y, GxEPD_WHITE);
   display.drawLine(pW3x, pW3y, pW4x, pW4y, GxEPD_WHITE);
 }
 display.drawCircle(x + diameter - 1, y + diameter, diameter / 2 + 1, GxEPD_RED);
}
//#########################################################################################
String MoonPhase(int d, int m, int y) {
 int c, e;
 double jd;
 int b;
 if (m < 3) {
   y--;
   m += 12;
 }
 ++m;
 c   = 365.25 * y;
 e   = 30.6  * m;
 jd  = c + e + d - 694039.09;     /* jd is total days elapsed */
 jd /= 29.53059;                  /* divide by the moon cycle (29.53 days) */
 b   = jd;                        /* int(jd) -> b, take integer part of jd */
 jd -= b;                         /* subtract integer part to leave fractional part of original jd */
 b   = jd * 8 + 0.5;              /* scale fraction from 0-8 and round by adding 0.5 */
 b   = b & 7;                     /* 0 and 8 are the same phase so modulo 8 for 0 */
 Hemisphere.toLowerCase();
 if (Hemisphere == "south") b = 7 - b;
 if (b == 0) return TXT_MOON_NEW;              // New;              0%  illuminated
 if (b == 1) return TXT_MOON_WAXING_CRESCENT;  // Waxing crescent; 25%  illuminated
 if (b == 2) return TXT_MOON_FIRST_QUARTER;    // First quarter;   50%  illuminated
 if (b == 3) return TXT_MOON_WAXING_GIBBOUS;   // Waxing gibbous;  75%  illuminated
 if (b == 4) return TXT_MOON_FULL;             // Full;            100% illuminated
 if (b == 5) return TXT_MOON_WANING_GIBBOUS;   // Waning gibbous;  75%  illuminated
 if (b == 6) return TXT_MOON_THIRD_QUARTER;    // Third quarter;   50%  illuminated
 if (b == 7) return TXT_MOON_WANING_CRESCENT;  // Waning crescent; 25%  illuminated
 return "";
}
//#########################################################################################
void arrow(int x, int y, int asize, float aangle, int pwidth, int plength) {
 float dx = (asize + 28) * cos((aangle - 90) * PI / 180) + x; // calculate X position
 float dy = (asize + 28) * sin((aangle - 90) * PI / 180) + y; // calculate Y position
 float x1 = 0;           float y1 = plength;
 float x2 = pwidth / 2;  float y2 = pwidth / 2;
 float x3 = -pwidth / 2; float y3 = pwidth / 2;
 float angle = aangle * PI / 180;
 float xx1 = x1 * cos(angle) - y1 * sin(angle) + dx;
 float yy1 = y1 * cos(angle) + x1 * sin(angle) + dy;
 float xx2 = x2 * cos(angle) - y2 * sin(angle) + dx;
 float yy2 = y2 * cos(angle) + x2 * sin(angle) + dy;
 float xx3 = x3 * cos(angle) - y3 * sin(angle) + dx;
 float yy3 = y3 * cos(angle) + x3 * sin(angle) + dy;
 display.fillTriangle(xx1, yy1, xx3, yy3, xx2, yy2, GxEPD_BLACK);
}
//#########################################################################################
void DisplayWXicon(int x, int y, String IconName, bool IconSize) {
 Serial.println(IconName);
 if      (IconName == "01d" || IconName == "01n")  Sunny(x, y, IconSize, IconName);
 else if (IconName == "02d" || IconName == "02n")  MostlySunny(x, y, IconSize, IconName);
 else if (IconName == "03d" || IconName == "03n")  Cloudy(x, y, IconSize, IconName);
 else if (IconName == "04d" || IconName == "04n")  MostlyCloudy(x, y, IconSize, IconName);
 else if (IconName == "09d" || IconName == "09n")  ChanceRain(x, y, IconSize, IconName);
 else if (IconName == "10d" || IconName == "10n")  Rain(x, y, IconSize, IconName);
 else if (IconName == "11d" || IconName == "11n")  Tstorms(x, y, IconSize, IconName);
 else if (IconName == "13d" || IconName == "13n")  Snow(x, y, IconSize, IconName);
 else if (IconName == "50d")                       Haze(x, y, IconSize, IconName);
 else if (IconName == "50n")                       Fog(x, y, IconSize, IconName);
 else                                              Nodata(x, y, IconSize, IconName);
}
//#########################################################################################
uint8_t StartWiFi() {
 Serial.print("\r\nConnecting to: "); Serial.println(String(ssid));
 IPAddress dns(8, 8, 8, 8); // Google DNS
 WiFi.disconnect();
 WiFi.mode(WIFI_STA); // switch off AP
 WiFi.begin(ssid, password);
 unsigned long start = millis();
 uint8_t connectionStatus;
 bool AttemptConnection = true;
 while (AttemptConnection) {
   connectionStatus = WiFi.status();
   if (millis() > start + 15000) { // Wait 15-secs maximum
     AttemptConnection = false;
   }
   if (connectionStatus == WL_CONNECTED || connectionStatus == WL_CONNECT_FAILED) {
     AttemptConnection = false;
   }
   delay(50);
 }
 if (connectionStatus == WL_CONNECTED) {
   wifi_signal = WiFi.RSSI(); // Get Wifi Signal strength now, because the WiFi will be turned off to save power!
   Serial.println("WiFi connected at: " + WiFi.localIP().toString());
 }
 else Serial.println("WiFi connection *** FAILED ***");
 return connectionStatus;
}
//#########################################################################################
void StopWiFi() {
 WiFi.disconnect();
 WiFi.mode(WIFI_OFF);
}
//#########################################################################################
boolean SetupTime() {
 configTime(gmtOffset_sec, daylightOffset_sec, ntpServer, "time.nist.gov"); //(gmtOffset_sec, daylightOffset_sec, ntpServer)
 setenv("TZ", Timezone, 1);  //setenv()adds the "TZ" variable to the environment with a value TimeZone, only used if set to 1, 0 means no change
 tzset(); // Set the TZ environment variable
 delay(100);
 bool TimeStatus = UpdateLocalTime();
 return TimeStatus;
}
//#########################################################################################
boolean UpdateLocalTime() {
 struct tm timeinfo;
 char   time_output[30], day_output[30], update_time[30];
 while (!getLocalTime(&timeinfo, 10000)) { // Wait for 5-sec for time to synchronise
   Serial.println("Failed to obtain time");
   return false;
 }
 CurrentHour = timeinfo.tm_hour;
 CurrentMin  = timeinfo.tm_min;
 CurrentSec  = timeinfo.tm_sec;
 //See http://www.cplusplus.com/reference/ctime/strftime/
 //Serial.println(&timeinfo, "%a %b %d %Y   %H:%M:%S"); // Displays: Saturday, June 24 2017 14:05:49
 if (Units == "M") {
   sprintf(day_output, "%s  %02u-%s-%04u", weekday_D[timeinfo.tm_wday], timeinfo.tm_mday, month_M[timeinfo.tm_mon], (timeinfo.tm_year) + 1900);
   strftime(update_time, sizeof(update_time), "%H:%M", &timeinfo);  // Creates: '@ 14:05:49'   and change from 30 to 8 <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
   sprintf(time_output, "%s", update_time);
 }
 else
 {
   strftime(day_output, sizeof(day_output), "%a  %b-%d-%Y", &timeinfo); // Creates  'Sat May-31-2019'
   strftime(update_time, sizeof(update_time), "%r", &timeinfo);         // Creates: '@ 02:05:49pm'
   sprintf(time_output, "%s", update_time);
 }
 date_str = day_output;
 time_str = time_output;
 return true;
}
//#########################################################################################
// Symbols are drawn on a relative 10x10grid and 1 scale unit = 1 drawing unit
void addcloud(int x, int y, int scale, int linesize) {
 //Draw cloud outer
 display.fillCircle(x - scale * 3, y, scale, GxEPD_RED);                      // Left most circle
 display.fillCircle(x + scale * 3, y, scale, GxEPD_RED);                      // Right most circle
 display.fillCircle(x - scale, y - scale, scale * 1.4, GxEPD_RED);            // left middle upper circle
 display.fillCircle(x + scale * 1.5, y - scale * 1.3, scale * 1.75, GxEPD_RED); // Right middle upper circle
 display.fillRect(x - scale * 3 - 1, y - scale, scale * 6, scale * 2 + 1, GxEPD_RED); // Upper and lower lines
 //Clear cloud inner
 display.fillCircle(x - scale * 3, y, scale - linesize, GxEPD_WHITE);           // Clear left most circle
 display.fillCircle(x + scale * 3, y, scale - linesize, GxEPD_WHITE);           // Clear right most circle
 display.fillCircle(x - scale, y - scale, scale * 1.4 - linesize, GxEPD_WHITE); // left middle upper circle
 display.fillCircle(x + scale * 1.5, y - scale * 1.3, scale * 1.75 - linesize, GxEPD_WHITE); // Right middle upper circle
 display.fillRect(x - scale * 3 + 2, y - scale + linesize - 1, scale * 5.9, scale * 2 - linesize * 2 + 2, GxEPD_WHITE); // Upper and lower lines
}
//#########################################################################################
void addraindrop(int x, int y, int scale) {
 display.fillCircle(x, y, scale / 2, GxEPD_RED);
 display.fillTriangle(x - scale / 2, y, x, y - scale * 1.2, x + scale / 2, y , GxEPD_RED);
 x = x + scale * 1.6; y = y + scale / 3;
 display.fillCircle(x, y, scale / 2, GxEPD_RED);
 display.fillTriangle(x - scale / 2, y, x, y - scale * 1.2, x + scale / 2, y , GxEPD_RED);
}
//#########################################################################################
void addrain(int x, int y, int scale, bool IconSize) {
 if (IconSize == SmallIcon) scale *= 1.34;
 for (int d = 0; d < 4; d++) {
   addraindrop(x + scale * (7.8 - d * 1.95) - scale * 5.2, y + scale * 2.1 - scale / 6, scale / 1.6);
 }
}
//#########################################################################################
void addsnow(int x, int y, int scale, bool IconSize) {
 int dxo, dyo, dxi, dyi;
 for (int flakes = 0; flakes < 5; flakes++) {
   for (int i = 0; i < 360; i = i + 45) {
     dxo = 0.5 * scale * cos((i - 90) * 3.14 / 180); dxi = dxo * 0.1;
     dyo = 0.5 * scale * sin((i - 90) * 3.14 / 180); dyi = dyo * 0.1;
     display.drawLine(dxo + x + flakes * 1.5 * scale - scale * 3, dyo + y + scale * 2, dxi + x + 0 + flakes * 1.5 * scale - scale * 3, dyi + y + scale * 2, GxEPD_RED);
   }
 }
}
//#########################################################################################
void addtstorm(int x, int y, int scale) {
 y = y + scale / 2;
 for (int i = 0; i < 5; i++) {
   display.drawLine(x - scale * 4 + scale * i * 1.5 + 0, y + scale * 1.5, x - scale * 3.5 + scale * i * 1.5 + 0, y + scale, GxEPD_RED);
   if (scale != Small) {
     display.drawLine(x - scale * 4 + scale * i * 1.5 + 1, y + scale * 1.5, x - scale * 3.5 + scale * i * 1.5 + 1, y + scale, GxEPD_RED);
     display.drawLine(x - scale * 4 + scale * i * 1.5 + 2, y + scale * 1.5, x - scale * 3.5 + scale * i * 1.5 + 2, y + scale, GxEPD_RED);
   }
   display.drawLine(x - scale * 4 + scale * i * 1.5, y + scale * 1.5 + 0, x - scale * 3 + scale * i * 1.5 + 0, y + scale * 1.5 + 0, GxEPD_RED);
   if (scale != Small) {
     display.drawLine(x - scale * 4 + scale * i * 1.5, y + scale * 1.5 + 1, x - scale * 3 + scale * i * 1.5 + 0, y + scale * 1.5 + 1, GxEPD_RED);
     display.drawLine(x - scale * 4 + scale * i * 1.5, y + scale * 1.5 + 2, x - scale * 3 + scale * i * 1.5 + 0, y + scale * 1.5 + 2, GxEPD_RED);
   }
   display.drawLine(x - scale * 3.5 + scale * i * 1.4 + 0, y + scale * 2.5, x - scale * 3 + scale * i * 1.5 + 0, y + scale * 1.5, GxEPD_RED);
   if (scale != Small) {
     display.drawLine(x - scale * 3.5 + scale * i * 1.4 + 1, y + scale * 2.5, x - scale * 3 + scale * i * 1.5 + 1, y + scale * 1.5, GxEPD_RED);
     display.drawLine(x - scale * 3.5 + scale * i * 1.4 + 2, y + scale * 2.5, x - scale * 3 + scale * i * 1.5 + 2, y + scale * 1.5, GxEPD_RED);
   }
 }
}
//#########################################################################################
void addsun(int x, int y, int scale, bool IconSize) {
 int linesize = 3;
 if (IconSize == SmallIcon) linesize = 1;
 display.fillRect(x - scale * 2, y, scale * 4, linesize, GxEPD_BLACK);
 display.fillRect(x, y - scale * 2, linesize, scale * 4, GxEPD_BLACK);
 display.drawLine(x - scale * 1.3, y - scale * 1.3, x + scale * 1.3, y + scale * 1.3, GxEPD_BLACK);
 display.drawLine(x - scale * 1.3, y + scale * 1.3, x + scale * 1.3, y - scale * 1.3, GxEPD_BLACK);
 if (IconSize == LargeIcon) {
   display.drawLine(1 + x - scale * 1.3, y - scale * 1.3, 1 + x + scale * 1.3, y + scale * 1.3, GxEPD_BLACK);
   display.drawLine(2 + x - scale * 1.3, y - scale * 1.3, 2 + x + scale * 1.3, y + scale * 1.3, GxEPD_BLACK);
   display.drawLine(3 + x - scale * 1.3, y - scale * 1.3, 3 + x + scale * 1.3, y + scale * 1.3, GxEPD_BLACK);
   display.drawLine(1 + x - scale * 1.3, y + scale * 1.3, 1 + x + scale * 1.3, y - scale * 1.3, GxEPD_BLACK);
   display.drawLine(2 + x - scale * 1.3, y + scale * 1.3, 2 + x + scale * 1.3, y - scale * 1.3, GxEPD_BLACK);
   display.drawLine(3 + x - scale * 1.3, y + scale * 1.3, 3 + x + scale * 1.3, y - scale * 1.3, GxEPD_BLACK);
 }
 display.fillCircle(x, y, scale * 1.3, GxEPD_WHITE);
 display.fillCircle(x, y, scale, GxEPD_BLACK);
 display.fillCircle(x, y, scale - linesize, GxEPD_WHITE);
}
//#########################################################################################
void addfog(int x, int y, int scale, int linesize, bool IconSize) {
 if (IconSize == SmallIcon) {
   y -= 10;
   linesize = 1;
 }
 for (int i = 0; i < 6; i++) {
   display.fillRect(x - scale * 3, y + scale * 1.5, scale * 6, linesize, GxEPD_RED);
   display.fillRect(x - scale * 3, y + scale * 2.0, scale * 6, linesize, GxEPD_RED);
   display.fillRect(x - scale * 3, y + scale * 2.5, scale * 6, linesize, GxEPD_RED);
 }
}
//#########################################################################################
void Sunny(int x, int y, bool IconSize, String IconName) {
 int scale = Small, offset = 3;
 if (IconSize == LargeIcon) {
   scale = Large;
   y = y - 8;
   offset = 18;
 } else y = y - 3; // Shift up small sun icon
 if (IconName.endsWith("n")) addmoon(x, y + offset, scale, IconSize);
 scale = scale * 1.6;
 addsun(x, y, scale, IconSize);
}
//#########################################################################################
void MostlySunny(int x, int y, bool IconSize, String IconName) {
 int scale = Small, linesize = 3, offset = 3;
 if (IconSize == LargeIcon) {
   scale = Large;
   offset = 10;
 } else linesize = 1;
 if (IconName.endsWith("n")) addmoon(x, y + offset, scale, IconSize);
 addcloud(x, y + offset, scale, linesize);
 addsun(x - scale * 1.8, y - scale * 1.8 + offset, scale, IconSize);
}
//#########################################################################################
void MostlyCloudy(int x, int y, bool IconSize, String IconName) {
 int scale = Large, linesize = 3;
 if (IconSize == SmallIcon) {
   scale = Small;
   linesize = 1;
 }
 if (IconName.endsWith("n")) addmoon(x, y, scale, IconSize);
 addcloud(x, y, scale, linesize);
 addsun(x - scale * 1.8, y - scale * 1.8, scale, IconSize);
 addcloud(x, y, scale, linesize);
}
//#########################################################################################
void Cloudy(int x, int y, bool IconSize, String IconName) {
 int scale = Large, linesize = 3;
 if (IconSize == SmallIcon) {
   scale = Small;
   if (IconName.endsWith("n")) addmoon(x, y, scale, IconSize);
   linesize = 1;
   addcloud(x, y, scale, linesize);
 }
 else {
   y += 10;
   if (IconName.endsWith("n")) addmoon(x, y, scale, IconSize);
   addcloud(x + 30, y - 35, 5, linesize); // Cloud top right
   addcloud(x - 20, y - 25, 7, linesize); // Cloud top left
   addcloud(x, y, scale, linesize);       // Main cloud
 }
}
//#########################################################################################
void Rain(int x, int y, bool IconSize, String IconName) {
 int scale = Large, linesize = 3;
 if (IconSize == SmallIcon) {
   scale = Small;
   linesize = 1;
 }
 if (IconName.endsWith("n")) addmoon(x, y + 10, scale, IconSize);
 addcloud(x, y, scale, linesize);
 addrain(x, y, scale, IconSize);
}
//#########################################################################################
void ExpectRain(int x, int y, bool IconSize, String IconName) {
 int scale = Large, linesize = 3;
 if (IconSize == SmallIcon) {
   scale = Small;
   linesize = 1;
 }
 if (IconName.endsWith("n")) addmoon(x, y, scale, IconSize);
 addsun(x - scale * 1.8, y - scale * 1.8, scale, IconSize);
 addcloud(x, y, scale, linesize);
 addrain(x, y, scale, IconSize);
}
//#########################################################################################
void ChanceRain(int x, int y, bool IconSize, String IconName) {
 int scale = Large, linesize = 3;
 if (IconSize == SmallIcon) {
   scale = Small;
   linesize = 1;
 }
 if (IconName.endsWith("n")) addmoon(x, y, scale, IconSize);
 addsun(x - scale * 1.8, y - scale * 1.8, scale, IconSize);
 addcloud(x, y, scale, linesize);
 addrain(x, y, scale, IconSize);
}
//#########################################################################################
void Tstorms(int x, int y, bool IconSize, String IconName) {
 int scale = Large, linesize = 3;
 if (IconSize == SmallIcon) {
   scale = Small;
   linesize = 1;
 }
 if (IconName.endsWith("n")) addmoon(x, y, scale, IconSize);
 addcloud(x, y, scale, linesize);
 addtstorm(x, y, scale);
}
//#########################################################################################
void Snow(int x, int y, bool IconSize, String IconName) {
 int scale = Large, linesize = 3;
 if (IconSize == SmallIcon) {
   scale = Small;
   linesize = 1;
 }
 if (IconName.endsWith("n")) addmoon(x, y + 15, scale, IconSize);
 addcloud(x, y, scale, linesize);
 addsnow(x, y, scale, IconSize);
}
//#########################################################################################
void Fog(int x, int y, bool IconSize, String IconName) {
 int linesize = 3, scale = Large;
 if (IconSize == SmallIcon) {
   scale = Small;
   linesize = 1;
 }
 if (IconName.endsWith("n")) addmoon(x, y, scale, IconSize);
 addcloud(x, y - 5, scale, linesize);
 addfog(x, y - 5, scale, linesize, IconSize);
}
//#########################################################################################
void Haze(int x, int y, bool IconSize, String IconName) {
 int linesize = 3, scale = Large;
 if (IconSize == SmallIcon) {
   scale = Small;
   linesize = 1;
 }
 if (IconName.endsWith("n")) addmoon(x, y, scale, IconSize);
 addsun(x, y - 5, scale * 1.4, IconSize);
 addfog(x, y - 5, scale * 1.4, linesize, IconSize);
}
//#########################################################################################
void CloudCover(int x, int y, int CCover) {
 addcloud(x - 9, y - 3, Small * 0.5, 2); // Cloud top left
 addcloud(x + 3, y - 3, Small * 0.5, 2); // Cloud top right
 addcloud(x, y,         Small * 0.5, 2); // Main cloud
 u8g2Fonts.setFont(u8g2_font_helvB08_tf);
 drawString(x + 15, y - 5, String(CCover) + "%", LEFT);
}
//#########################################################################################
void Visibility(int x, int y, String Visi) {
 y = y - 3; //
 float start_angle = 0.52, end_angle = 2.61;
 int r = 10;
 for (float i = start_angle; i < end_angle; i = i + 0.05) {
   display.drawPixel(x + r * cos(i), y - r / 2 + r * sin(i), GxEPD_RED);
   display.drawPixel(x + r * cos(i), 1 + y - r / 2 + r * sin(i), GxEPD_RED);
 }
 start_angle = 3.61; end_angle = 5.78;
 for (float i = start_angle; i < end_angle; i = i + 0.05) {
   display.drawPixel(x + r * cos(i), y + r / 2 + r * sin(i), GxEPD_RED);
   display.drawPixel(x + r * cos(i), 1 + y + r / 2 + r * sin(i), GxEPD_RED);
 }
 display.fillCircle(x, y, r / 4, GxEPD_RED);
 u8g2Fonts.setFont(u8g2_font_helvB08_tf);
 drawString(x + 12, y - 3, Visi, LEFT);
}
//#########################################################################################
void addmoon(int x, int y, int scale, bool IconSize) {
 if (IconSize == LargeIcon) {
   x = x + 12; y = y + 12;
   display.fillCircle(x - 50, y - 55, scale, GxEPD_RED);
   display.fillCircle(x - 35, y - 55, scale * 1.6, GxEPD_WHITE);
 }
 else
 {
   display.fillCircle(x - 20, y - 12, scale, GxEPD_RED);
   display.fillCircle(x - 15, y - 12, scale * 1.6, GxEPD_WHITE);
 }
}
//#########################################################################################
void Nodata(int x, int y, bool IconSize, String IconName) {
 if (IconSize == LargeIcon) u8g2Fonts.setFont(u8g2_font_helvB24_tf); else u8g2Fonts.setFont(u8g2_font_helvB10_tf);
 drawString(x - 3, y - 8, "?", CENTER);
 u8g2Fonts.setFont(u8g2_font_helvB08_tf);
}
//#########################################################################################
void DrawBattery(int x, int y) {
 uint8_t percentage = 100;
 float voltage = analogRead(3) / 4096.0 * 7.46;
 if (voltage > 1 ) { // Only display if there is a valid reading
   Serial.println("Voltage = " + String(voltage));
   percentage = 2836.9625 * pow(voltage, 4) - 43987.4889 * pow(voltage, 3) + 255233.8134 * pow(voltage, 2) - 656689.7123 * voltage + 632041.7303;
   if (voltage >= 4.20) percentage = 100;
   if (voltage <= 3.50) percentage = 0;
   display.drawRect(x + 15, y - 12, 19, 10, GxEPD_BLACK);
   display.fillRect(x + 34, y - 10, 2, 5, GxEPD_BLACK);
   display.fillRect(x + 17, y - 10, 15 * percentage / 100.0, 6, GxEPD_RED);
   drawString(x + 65, y - 11, String(percentage) + "%", RIGHT);
   //drawString(x + 13, y + 5,  String(voltage, 2) + "v", CENTER);
 }
}
void DrawGraph(int x_pos, int y_pos, int gwidth, int gheight, float Y1Min, float Y1Max, String title, float DataArray[], int readings, boolean auto_scale, boolean barchart_mode) {
#define auto_scale_margin 0 // Sets the autoscale increment, so axis steps up in units of e.g. 3
#define y_minor_axis 5      // 5 y-axis division markers
 float maxYscale = -10000;
 float minYscale =  10000;
 int last_x, last_y;
 float x1, y1, x2, y2;
 if (auto_scale == true) {
   for (int i = 1; i < readings; i++ ) {
     if (DataArray[i] >= maxYscale) maxYscale = DataArray[i];
     if (DataArray[i] <= minYscale) minYscale = DataArray[i];
   }
   maxYscale = round(maxYscale + auto_scale_margin); // Auto scale the graph and round to the nearest value defined, default was Y1Max
   Y1Max = round(maxYscale + 0.5);
   if (minYscale != 0) minYscale = round(minYscale - auto_scale_margin); // Auto scale the graph and round to the nearest value defined, default was Y1Min
   Y1Min = round(minYscale);
 }
 // Draw the graph
 last_x = x_pos + 1;
 last_y = y_pos + (Y1Max - constrain(DataArray[1], Y1Min, Y1Max)) / (Y1Max - Y1Min) * gheight;
 display.drawRect(x_pos, y_pos, gwidth + 3, gheight + 2, GxEPD_BLACK);
 u8g2Fonts.setFont(u8g2_font_helvB08_tf);
 drawString(x_pos + gwidth / 2, y_pos - 12, title, CENTER);
 // Draw the graph
 last_x = x_pos;
 last_y = y_pos + (Y1Max - constrain(DataArray[1], Y1Min, Y1Max)) / (Y1Max - Y1Min) * gheight;
 display.drawRect(x_pos, y_pos, gwidth + 3, gheight + 2, GxEPD_BLACK);
 drawString(x_pos + gwidth / 2, y_pos - 13, title, CENTER);
 // Draw the data
 for (int gx = 0; gx < readings; gx++) {
   y2 = y_pos + (Y1Max - constrain(DataArray[gx], Y1Min, Y1Max)) / (Y1Max - Y1Min) * gheight + 1;
   if (barchart_mode) {
     x2 = x_pos + gx * (gwidth / readings) + 2;
     display.fillRect(x2, y2, (gwidth / readings) - 2, y_pos + gheight - y2 + 2, GxEPD_RED);
   } 
   else
   {
     x2 = x_pos + gx * gwidth / (readings - 1) + 1; // max_readings is the global variable that sets the maximum data that can be plotted
     display.drawLine(last_x, last_y, x2, y2, GxEPD_RED);
   }
   last_x = x2;
   last_y = y2;
 }
 //Draw the Y-axis scale
#define number_of_dashes 15
 for (int spacing = 0; spacing <= y_minor_axis; spacing++) {
   for (int j = 0; j < number_of_dashes; j++) { // Draw dashed graph grid lines
     if (spacing < y_minor_axis) display.drawFastHLine((x_pos + 3 + j * gwidth / number_of_dashes), y_pos + (gheight * spacing / y_minor_axis), gwidth / (2 * number_of_dashes), GxEPD_BLACK);
   }
   if ((Y1Max - (float)(Y1Max - Y1Min) / y_minor_axis * spacing) < 5 || title == TXT_PRESSURE_IN) {
     drawString(x_pos, y_pos + gheight * spacing / y_minor_axis - 5, String((Y1Max - (float)(Y1Max - Y1Min) / y_minor_axis * spacing + 0.01), 1), RIGHT);
   }
   else
   {
     if (Y1Min < 1 && Y1Max < 10)
       drawString(x_pos - 3, y_pos + gheight * spacing / y_minor_axis - 5, String((Y1Max - (float)(Y1Max - Y1Min) / y_minor_axis * spacing + 0.01), 1), RIGHT);
     else
       drawString(x_pos - 3, y_pos + gheight * spacing / y_minor_axis - 5, String((Y1Max - (float)(Y1Max - Y1Min) / y_minor_axis * spacing + 0.01), 0), RIGHT);
   }
 }
 for (int i = 0; i <= 2; i++) {
   drawString(15 + x_pos + gwidth / 3 * i, y_pos + gheight + 3, String(i), LEFT);
 }
 drawString(x_pos + gwidth / 2, y_pos + gheight + 10, TXT_DAYS, CENTER);
}
//#########################################################################################
void drawString(int x, int y, String text, alignment align) {
 int16_t  x1, y1; //the bounds of x,y and w and h of the variable 'text' in pixels.
 uint16_t w, h;
 display.setTextWrap(false);
 display.getTextBounds(text, x, y, &x1, &y1, &w, &h);
 if (align == RIGHT)  x = x - w;
 if (align == CENTER) x = x - w / 2;
 u8g2Fonts.setCursor(x, y + h);
 u8g2Fonts.print(text);
}
//#########################################################################################
void drawStringMaxWidth(int x, int y, unsigned int text_width, String text, alignment align) {
 int16_t  x1, y1; //the bounds of x,y and w and h of the variable 'text' in pixels.
 uint16_t w, h;
 display.getTextBounds(text, x, y, &x1, &y1, &w, &h);
 if (align == RIGHT)  x = x - w;
 if (align == CENTER) x = x - w / 2;
 u8g2Fonts.setCursor(x, y);
 if (text.length() > text_width * 2) {
   u8g2Fonts.setFont(u8g2_font_helvB10_tf);
   text_width = 42;
   y = y - 3;
 }
 u8g2Fonts.println(text.substring(0, text_width));
 if (text.length() > text_width) {
   u8g2Fonts.setCursor(x, y + h + 15);
   String secondLine = text.substring(text_width);
   secondLine.trim(); // Remove any leading spaces
   u8g2Fonts.println(secondLine);
 }
}
//#########################################################################################
void InitialiseDisplay() {
 display.init(115200, true, 2, false);
 // display.init(); for older Waveshare HAT's
 SPI.end();
 SPI.begin(EPD_SCK, EPD_MISO, EPD_MOSI, EPD_CS);
 u8g2Fonts.begin(display); // connect u8g2 procedures to Adafruit GFX
 u8g2Fonts.setFontMode(1);                  // use u8g2 transparent mode (this is default)
 u8g2Fonts.setFontDirection(0);             // left to right (this is default)
 u8g2Fonts.setForegroundColor(GxEPD_BLACK); // apply Adafruit GFX color
 u8g2Fonts.setBackgroundColor(GxEPD_WHITE); // apply Adafruit GFX color
 u8g2Fonts.setFont(u8g2_font_helvB10_tf);   // select u8g2 font from here: https://github.com/olikraus/u8g2/wiki/fntlistall
 display.fillScreen(GxEPD_WHITE);
 display.setFullWindow();
}
// Function to perform a soft reset
void softReset() {
   Wire.beginTransmission(HTU21D_ADDRESS);
   Wire.write(SOFT_RESET);
   Wire.endTransmission();
   delay(15);  // Wait for reset to complete
}
void disableHeater() {
   // Read the user register
   Wire.beginTransmission(HTU21D_ADDRESS);
   Wire.write(USER_REGISTER_READ);
   Wire.endTransmission();
   Wire.requestFrom(HTU21D_ADDRESS, 1);
   if (Wire.available()) {
       uint8_t userReg = Wire.read();  // Read the user register value
       // Clear bit 2 to disable the heater
       userReg &= ~(1 << 2);
       // Write the updated value back to the user register
       Wire.beginTransmission(HTU21D_ADDRESS);
       Wire.write(USER_REGISTER_WRITE);
       Wire.write(userReg);
       Wire.endTransmission();
   }
}
// Function to read temperature
float readTemperature() {
   Wire.beginTransmission(HTU21D_ADDRESS);
   Wire.write(TEMP_MEASURE_NO_HOLD); // Send temperature measurement command
   Wire.endTransmission();
   delay(50); // Wait for conversion (50ms for temperature)
   Wire.requestFrom(HTU21D_ADDRESS, 3); // Read 3 bytes (2 data + 1 CRC)
   if (Wire.available() == 3) {
       uint16_t rawTemp = (Wire.read() << 8) | Wire.read();
       Wire.read();  // Read and discard CRC
       rawTemp &= 0xFFFC; // Clear status bits
       Serial.print("T: ");
       Serial.print(rawTemp);
       Serial.println(" °C");
       return -46.85 + (175.72 * rawTemp / 65536.0);  // Convert to Celsius
   }
   return -999.0; // Error value
}
// Function to read humidity
float readHumidity() {
   Wire.beginTransmission(HTU21D_ADDRESS);
   Wire.write(HUMID_MEASURE_NO_HOLD); // Send humidity measurement command
   Wire.endTransmission();
   delay(16); // Wait for conversion (16ms for humidity)
   Wire.requestFrom(HTU21D_ADDRESS, 3); // Read 3 bytes (2 data + 1 CRC)
   if (Wire.available() == 3) {
       uint16_t rawHumidity = (Wire.read() << 8) | Wire.read();
       Wire.read();  // Read and discard CRC
       rawHumidity &= 0xFFFC; // Clear status bits
       Serial.print("H: ");
       Serial.print(rawHumidity);
       Serial.println(" °C");
       return -6.0 + (125.0 * rawHumidity / 65536.0);  // Convert to %RH
   }
   return -999.0; // Error value
}
Video

Have any question realated to this Article?

Ask Our Community Members