
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.
Table of Contents
- Desktop Weather Station Features
- Components Required
- Desktop Weather Station Schematic Diagram
- Desktop Weather Station PCB
- Assembling the Desktop Weather Station PCB
- 3D Printed Parts of Weather Station
- Assembling the Desktop Weather Station
- Desktop Weather Station Arduino Code
- GitHub Repository with Code and Circuit
- Projects using ESP32-S3
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.

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.

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.

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.

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.

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.

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.

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

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.

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

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.

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

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.

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.

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.

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.

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
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
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
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
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
}