Programming the reTerminal E1002 under Arduino IDE

5
(1)

Seeed Studio has just introduced the reTerminal E1002, a ready-to-use device featuring a sturdy metal casing and a 7.3″ color e-paper display with an 800×480 pixel resolution. Based on an ESP32-S3 microcontroller, it integrates several hardware features: three buttons, a buzzer, a status LED (in addition to the power LED), a temperature and pressure sensor, a microphone, and a MicroSD card reader. The whole system is powered by an internal 2000 mAh battery that provides up to three months of battery life.

This device is particularly well-suited for displaying data from the Zigbee sensor I developed in my tutorial, “Creating a Battery-Powered Zigbee Temperature Sensor with an ESP32C6,” which I integrated into Home Assistant.

The reTerminal E1002 can be configured and customized using three distinct approaches:

It is this last approach that I will use in this article, because it has the advantage of not imposing the limitations of the other two approaches.

Configuring the Arduino IDE for the reTerminal E1002

First, you need to integrate support for ESP32 microcontrollers into the Arduino IDE by adding the following URL to the Additional Boards Manager URLs field in the File > Preferences menu:

Plaintext
https://espressif.github.io/arduino-esp32/package_esp32_index.json

You then need to install the ESP32 package via the Tools > Board > Boards Manager menu by searching for “esp32” and installing the esp32 by Espressif Systems package (available on espressif github).

Then download the GxEPD2 library in its ZIP format:

And add it to the Arduino IDE via the menu Sketch > Include Library > Add .ZIP Library…

And finally, install this GxEPD2 library by Jean-Marc Zingg via the Tools > Manage libraries menu.

First “Hello World” program

Here is the code that displays the famous “Hello World” on the screen of the reTerminal E1002, which must be installed on the device after selecting the XIAO_ESP32S3 board via the Tools > Board > ESP32 Arduino menu and choosing the port to which the reTerminal is connected.

C++
/**
 * @file hello_world.ino
 * @brief Display "Hello World!" on Seeed reTerminal E1002 e-paper.
 * 
 *   https://tutoduino.fr/
 */
 #include <GxEPD2_7C.h> // Include library for 7-color ePaper display management
#include <Fonts/FreeMonoBold12pt7b.h> // Include a bold monospaced font at 12pt (for text display)

// Define SPI connection pins between the microcontroller and the ePaper display
#define EPD_SCK_PIN  7     // SPI clock pin
#define EPD_MOSI_PIN 9     // SPI data pin (MOSI)
#define EPD_CS_PIN   10    // SPI Chip Select for the ePaper display
#define EPD_DC_PIN   11    // Data/Command control pin
#define EPD_RES_PIN  12    // Display Reset pin
#define EPD_BUSY_PIN 13    // Display Busy status pin

// Define the ePaper display driver and class (for a 7.3" color model)
#define GxEPD2_DISPLAY_CLASS GxEPD2_7C
#define GxEPD2_DRIVER_CLASS GxEPD2_730c_GDEP073E01 // Specific to the 7.3" Color display

// Set up a maximum display buffer size for RAM management
#define MAX_DISPLAY_BUFFER_SIZE 16000

// Macro to compute the maximum height, given display size and RAM constraints
#define MAX_HEIGHT(EPD) \
    (EPD::HEIGHT <= MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8) \
         ? EPD::HEIGHT \
         : MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8))

// Instantiate the ePaper display object with the chosen driver and pinouts
GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)>
    display(GxEPD2_DRIVER_CLASS(/*CS=*/EPD_CS_PIN, /*DC=*/EPD_DC_PIN,
                                /*RST=*/EPD_RES_PIN, /*BUSY=*/EPD_BUSY_PIN));

// Create a dedicated SPI bus object for display communication
SPIClass hspi(HSPI);

/**
 * @brief Draws "Hello World!" in red at coordinates (100, 100) on the ePaper display.
 */
void helloWorld()
{
  display.setRotation(0);  // No rotation, natural orientation
  display.setFont(&FreeMonoBold12pt7b); // Use the bold 12pt monospace font
  display.setTextColor(GxEPD_RED);      // Set text color to red (one of supported display colors)
  display.setFullWindow();              // Use the full display area for drawing
  display.firstPage();                  // Start first drawing page (paged drawing saves RAM)
  do
  {
    display.fillScreen(GxEPD_WHITE);    // Fill the screen with white (background)
    display.setCursor(100, 100);        // Set the cursor to (100, 100) for text position
    display.print("Hello World!");      // Print "Hello World!" on the screen
  }
  while (display.nextPage());           // Continue until all pages are rendered (for paged displays)
}

/**
 * @brief Arduino setup function. Initializes the display and shows "Hello World!".
 */
void setup()
{
  pinMode(EPD_RES_PIN, OUTPUT);      // Configure reset pin as output
  pinMode(EPD_DC_PIN, OUTPUT);       // Configure data/command pin as output
  pinMode(EPD_CS_PIN, OUTPUT);       // Configure chip-select pin as output

  // Initialize SPI bus for display with specified pins, speed, and settings
  hspi.begin(EPD_SCK_PIN, -1, EPD_MOSI_PIN, -1);
  display.epd2.selectSPI(hspi, SPISettings(2000000, MSBFIRST, SPI_MODE0));

  // Initialize the ePaper display hardware and driver
  display.init(0);

  // Display "Hello World!" message
  helloWorld();

  // Put the display into low-power (hibernate) mode after rendering
  display.hibernate();
}

// No loop code needed. The display only updates once at startup in this demo.
void loop() {};

Here is the result after upload on the reTerminal:

Famous “Hello World!” displayed on reTerminal E1002 programmed on Arduino IDE

Fetching data from the Home Assistant server

One goal of this tutorial is to display on the reTerminal E1002 the temperature from the sensor that is available on the Home Assistant server. This data will be retrieved using the Home Assistant server’s RESTful API, accessible via the URL http://HA_IP_ADDRESS:8123/api/. This API only accepts and returns objects encoded in JSON. For this purpose, we will use the ArduinoJson library by Benoit Blanchon, which is installed from the Tools > Manage Libraries menu in the Arduino IDE.

You need to obtain, from the Home Assistant web page, the entity ID of the sensor you want to display on the reTerminal. For example, the temperature sensor corresponds to the entity with the ID: sensor.tutoduino_esp32c6tempsensor_temperature.

Home Assistant entity ID to retrieve

To retrieve data from Home Assistant, our program will need a Home Assistant token to query it via its API. This must be done from your Home Assistant server’s web page, by going to your profile page and clicking the Create Token button from the Security menu in your profile page.

Creating a Token in Home Assistant

Once the token is created, simply enter it in your program using the Arduino IDE. I always recommend storing your secrets (wifi password, home assistant token, etc.) in a secret.h file that will be included in your C program. This prevents them from being leaked on Github after an accidental copy-paste, for example.

Storing the Home Assistant token in a secret.h file in the Arduino IDE

Here’s an example of a program that will retrieve the temperature from our sensor via the Home Assistant API and display it on the reTerminal E1002 screen. Note that the reading on the Home Assistant server is done every 10 minutes and that between two readings, the reTerminal goes into deep sleep to save its battery. However, it is possible to wake up the reTerminal at any time by clicking on its green button.

C++
/**
 * @file TemperatureDisplay.ino
 * @brief Fetches temperature data from a Home Assistant server and displays it on a reTerminal E1002 e-paper screen.
 *        The device can enter deep sleep and wake up via a button press or timer.
 *        https://tutoduino.fr/
 */

#include <WiFi.h>           // Wi-Fi library for ESP32
#include <HTTPClient.h>     // HTTP client for making requests
#include <ArduinoJson.h>    // JSON parsing library
#include <GxEPD2_7C.h>      // E-paper display library for 7-color displays
#include <Fonts/FreeMonoBold12pt7b.h>  // Custom font for the display
#include "secrets.h"       // Custom file containing sensitive data (Wi-Fi credentials, API tokens)

// --- Pin Definitions ---
// Serial1 communication pins for debugging
#define SERIAL_RX 44
#define SERIAL_TX 43
// SPI pins for e-paper display communication
#define EPD_SCK_PIN  7   // SPI Clock pin
#define EPD_MOSI_PIN 9   // MOSI (Master Out Slave In) pin
#define EPD_CS_PIN   10  // Chip Select pin
#define EPD_DC_PIN   11  // Data/Command pin
#define EPD_RES_PIN  12  // Reset pin
#define EPD_BUSY_PIN 13  // Busy pin
// Button pin definitions according to the hardware schematic
#define GREEN_BUTTON 3   // KEY0 - GPIO3, connected to the green button

// --- E-paper Display Configuration ---
// Display class for 7.3" color e-paper
#define GxEPD2_DISPLAY_CLASS GxEPD2_7C
#define GxEPD2_DRIVER_CLASS GxEPD2_730c_GDEP073E01
// Maximum display buffer size in bytes (limits memory usage)
#define MAX_DISPLAY_BUFFER_SIZE 16000
// Calculate the maximum display height based on the buffer size
#define MAX_HEIGHT(EPD) \
  (EPD::HEIGHT <= (MAX_DISPLAY_BUFFER_SIZE * 8) / EPD::WIDTH ? EPD::HEIGHT : (MAX_DISPLAY_BUFFER_SIZE * 8) / EPD::WIDTH)

// --- Device Initialization ---
// Initialize SPI interface for the display (HSPI = Host SPI)
SPIClass hspi(HSPI);
// Initialize the display object with the specified driver and buffer size
GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)>
  display(GxEPD2_DRIVER_CLASS(EPD_CS_PIN, EPD_DC_PIN, EPD_RES_PIN, EPD_BUSY_PIN));

/**
 * @brief Displays the temperature on the e-paper screen.
 * @param temperature A string representing the temperature to display.
 */
void displayTemperature(const char* temperature) {
  Serial1.print("Temperature to display: ");
  Serial1.println(temperature);

  // Configure display settings
  display.setRotation(0);  // Set display orientation to default (0 degrees)
  display.setFont(&FreeMonoBold12pt7b);  // Set font to FreeMonoBold12pt7b
  display.setTextColor(GxEPD_RED);  // Set text color to red
  display.setFullWindow();  // Use the full display window for rendering

  // Start the display update process
  display.firstPage();
  do {
    display.fillScreen(GxEPD_WHITE);  // Clear the screen with white background
    display.setCursor(100, 100);  // Set cursor position to (100, 100)
    display.print("Temperature = ");
    display.print(temperature);  // Print the temperature value
    display.print(" C");  // Print the degree symbol and 'C' for Celsius
  } while (display.nextPage());  // Repeat until the display update is complete
}

/**
 * @brief Fetches the temperature from the Home Assistant server.
 * @return A dynamically allocated string containing the temperature or "Error" on failure.
 * @note The caller is responsible for freeing the returned string using free().
 */
char* getTemperature() {
  // Check if Wi-Fi is connected
  if (WiFi.status() != WL_CONNECTED) {
    Serial1.println("ERROR: Wi-Fi not connected.");
    return strdup("Error");  // Return an error message
  }

  // Initialize HTTP client and configure the request
  HTTPClient http;
  http.begin(ha_url);  // Set the target URL from secrets.h
  http.addHeader("Authorization", "Bearer " + String(ha_token));  // Add authorization header
  http.addHeader("Content-Type", "application/json");  // Set content type to JSON

  // Send a GET request to the Home Assistant API
  int httpCode = http.GET();
  if (httpCode != HTTP_CODE_OK) {
    Serial1.print("HTTP Error: ");
    Serial1.println(httpCode);
    Serial1.println(http.getString());  // Print the error response
    http.end();  // Close the connection
    return strdup("Error");  // Return an error message
  }

  // Read the server response
  String payload = http.getString();
  Serial1.println("Server response:");
  Serial1.println(payload);

  // Parse the JSON response
  DynamicJsonDocument doc(1024);  // Create a JSON document with a capacity of 1024 bytes
  DeserializationError error = deserializeJson(doc, payload);
  http.end();  // Close the HTTP connection

  if (error) {
    Serial1.print("JSON parsing error: ");
    Serial1.println(error.c_str());
    return strdup("Error");  // Return an error message if JSON parsing fails
  }

  // Check if the "state" field exists in the JSON response
  if (!doc.containsKey("state")) {
    Serial1.println("ERROR: 'state' field missing in JSON response.");
    return strdup("Error");  // Return an error message if "state" is missing
  }

  const char* state = doc["state"];  // Extract the temperature value from the JSON
  Serial1.print("Temperature = ");
  Serial1.println(state);

  // Return a dynamically allocated copy of the temperature string
  return strdup(state);
}

/**
 * @brief Prints the reason for waking up from deep sleep.
 */
void printWakeupReason() {
  esp_sleep_wakeup_cause_t wakeupReason = esp_sleep_get_wakeup_cause();
  switch (wakeupReason) {
    case ESP_SLEEP_WAKEUP_EXT0:
      Serial1.println("Wakeup caused by external signal (EXT0 - button press)");
      break;
    case ESP_SLEEP_WAKEUP_EXT1:
      Serial1.println("Wakeup caused by external signal (EXT1)");
      break;
    case ESP_SLEEP_WAKEUP_TIMER:
      Serial1.println("Wakeup caused by timer");
      break;
    case ESP_SLEEP_WAKEUP_TOUCHPAD:
      Serial1.println("Wakeup caused by touchpad");
      break;
    default:
      Serial1.printf("Wakeup not caused by deep sleep: %d\n", wakeupReason);
      break;
  }
}

/**
 * @brief Initial system setup. Runs once at startup.
 */
void setup() {
  // Initialize Serial1 for debugging
  Serial1.begin(115200, SERIAL_8N1, SERIAL_RX, SERIAL_TX);
  delay(1000);  // Wait for serial port to initialize
  Serial1.println("System starting...");
  printWakeupReason();  // Print the reason for waking up

  // Configure the green button as an input with pull-up resistor
  pinMode(GREEN_BUTTON, INPUT_PULLUP);
  // Enable wakeup from deep sleep when the button is pressed (LOW signal)
  esp_sleep_enable_ext0_wakeup((gpio_num_t)GREEN_BUTTON, LOW);

  // Configure display control pins as outputs
  pinMode(EPD_RES_PIN, OUTPUT);
  pinMode(EPD_DC_PIN, OUTPUT);
  pinMode(EPD_CS_PIN, OUTPUT);

  // Initialize SPI interface for the display
  hspi.begin(EPD_SCK_PIN, -1, EPD_MOSI_PIN, -1);
  display.epd2.selectSPI(hspi, SPISettings(2000000, MSBFIRST, SPI_MODE0));

  // Connect to Wi-Fi
  WiFi.begin(wifi_ssid, wifi_password);
  Serial1.print("Connecting to Wi-Fi");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial1.print(".");
  }
  Serial1.println("\nConnected to Wi-Fi");

  // Initialize the e-paper display
  display.init(0);
}

/**
 * @brief Main loop. Runs repeatedly after setup().
 */
void loop() {
  // Fetch the temperature from Home Assistant
  char* temperature = getTemperature();
  if (temperature != nullptr) {
    displayTemperature(temperature);  // Display the temperature
    free(temperature);  // Free the dynamically allocated memory
  } else {
    Serial1.println("ERROR: Failed to get temperature.");
    displayTemperature("Error");  // Display an error message
  }

  // Put the display into hibernation to save power
  display.hibernate();
  delay(1000);  // Wait for 1 seconds before entering deep sleep

  // Configure deep sleep wakeup after 10 minutes
  esp_sleep_enable_timer_wakeup(10 * 60 * 1000000);  // 10 minutes in microseconds
  esp_deep_sleep_start();  // Enter deep sleep mode
}

After uploading the program on reTerminal E1002, here is the rendering on its screen:

Fetching data over the internet using a RESTful API

Since WiFi, HTTP and JSON librairies are installled, it it very straightforward to fetch data from internet using RESTful API.

Here is an example to get Bitcoin value in USD:

C++

// Bitcoin URL should be stored in secret.h header file
const char* btc_url = "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd";

/**
 * @brief Get Bitcoin price (USD) from CoinGecko API.
 * @return Bitcoin price as integer, 0 if not available.
 */
int getBTC() {
  int btc = 0;
  if (WiFi.status() == WL_CONNECTED) {
    HTTPClient http;
    http.begin(btc_url);
    int httpCode = http.GET();
    if (httpCode == HTTP_CODE_OK) {
      DynamicJsonDocument doc(1024);
      if (!deserializeJson(doc, http.getString()))
        btc = doc["bitcoin"]["usd"].as<int>();
    }
    http.end();
  }
  return btc;
}

You can previously test the API, simply copy/paste the URL in your browser and its result will be displayed directly in JSON format:

Check the API directly from web browser (JSON format)

Displaying weather icons on the reTerminal

I decided to download weather icons from internet and to include it directly in the Arduino IDE as an array of unsigned char stored in program memory (PROGMEM) to optimize RAM usage. This type of data remains in the microcontroller’s flash memory rather than in volatile RAM. I used the excellent online tool image2cpp to change images into byte arrays.

To determine which weather conditions icon must be displayed, you have to use the weather code returned by the API (here weather_code = 2).

Weather code returned by open-meteo.com API

You then have to display the corresponding icon based on WMO Weather interpretation codes provided in open-Meteo documentation.

Weather code interprestation from open-meteo.com

I create the following function for this interpretation:

C++
/**
 * @brief Maps Open-Meteo weather code to icon index.
 * @param weatherCode Open-Meteo code
 * @return Icon bitmap index
 */
int weatherCodeToIcon(int weatherCode) {
  switch (weatherCode) {
    case 0: return 7;  // Sun
    case 1:
    case 2: return 3;  // Some clouds
    case 3: return 5;  // Cloudy
    case 45:
    case 48: return 2;  // Fog
    case 51:
    case 53:
    case 55:
    case 56:
    case 57: return 7;  // Freezing drizzle
    case 61:
    case 63:
    case 65:
    case 66:
    case 67: return 6;  // Freezing rain
    case 71:
    case 73:
    case 75: 
    case 77: return 4;  // Snow
    case 80:
    case 81:
    case 82: return 8;  // Showers
    case 85:
    case 86: return 4;  // Snow
    case 95:
    case 96:
    case 99: return 0;  // Thunderstorm
    default: return 5;
  }
}
C++
// The array of weather bitmaps is stored in icons_50x50.h
// Array of all bitmaps for convenience.
const unsigned char* epd_bitmap_allArray[9] = {
	epd_bitmap_icons8_eclair_dans_un_nuage_50,
	epd_bitmap_icons8_averse_50,
	epd_bitmap_icons8_brouillard_de_jour_50,
	epd_bitmap_icons8_journ_e_partiellement_nuageuse_50,
	epd_bitmap_icons8_neige_50,
	epd_bitmap_icons8_nuage_50,
	epd_bitmap_icons8_partiellement_nuageux_avec_pluie_50,
	epd_bitmap_icons8_soleil_50,
	epd_bitmap_icons8_peu_nuageux_2_50};
	

It is then easy to draw the 50×50 icon on the reTerminal screen at position x,y:

C++
  display.drawBitmap(x, y, epd_bitmap_allArray[iconNb], 50, 50, GxEPD_BLACK);

Displaying the degree (°) character on the reTerminal using GFX fonts.

By default, the degree character does not exist on GFX fonts. I found a discussion about this issue, but only the 12points font was available. So I decided to explore more and found this great online font customizer https://tchapi.github.io/Adafruit-GFX-Font-Customiser. Thanks to this customizer, I replaced the ‘`’ (0x60) character by ‘°’ in both 18 and 17 points FreeSans fonts.

Online font customizer to replace ‘`’ character (0x60) by ‘°’

It is straightforward to display 25.4° on the reTerminal screen adding ‘`’ or 0x60 to display the ‘°’ character, here is an example with the modified FreeSans12pt7b font:

C++
display.setFont(&FreeSans12pt7b);
display.setCursor(x, y);
display.print("25.4");
display.write(0x60);

Recuperation of internal temperature sensor of reTerminal

The reTerminal E Series includes an integrated SHT4x temperature and humidity sensor connected via I2C. You have to install the Sensirion I2C SHT4x library via Arduino Library Manager Tools > Manage Libraries…

The temperature and humidity values can then be obtained using the following code:

C++
#include <Wire.h>
#include <SensirionI2cSht4x.h>



// I2C pins for reTerminal E Series
#define I2C_SDA 19
#define I2C_SCL 20

// Object to manage onboard SHT4x sensor (I2C temperature/humidity)
SensirionI2cSht4x sht4x;

void setup() {
    // Initialize I2C with custom pins
    Wire.begin(I2C_SDA, I2C_SCL);
    
    uint16_t error;

    // Initialize the sensor
    sht4x.begin(Wire, 0x44);
}

void loop() {
    uint16_t error;
    float temperature;
    float humidity;
    
    // Measure temperature and humidity with medium precision
    error = sht4x.measureMediumPrecision(temperature, humidity);
}

Recuperation of battery level of reTerminal

The reTerminal E Series features battery voltage monitoring via an ADC pin connected through a voltage divider circuit. The internal battery charge level can be measured using the following code:

C++
// Battery monitoring pins
#define BATTERY_ADC_PIN 1      // GPIO1 - Battery voltage ADC
#define BATTERY_ENABLE_PIN 21  // GPIO21 - Battery monitoring enable

// 3.7 V Li-Ion battery voltage
const float minVoltage = 3.0;
const float maxVoltage = 4.0;

/**
 * @brief Mapp float voltage to % battery charge.
 * @param x=Battery voltage
 * @param in_min=Battery min voltage
 * @param in_max=Battery max voltage
 * @return % of battery charge.
 */
 uint8_t mapFloat(float x, float in_min, float in_max) {
  float val;
  val = (x - in_min) * (100) / (in_max - in_min);
  if (val < 0) {
    val = 0;
  } else if (val > 100) {
    val = 100;
  }
  return (uint8_t)val;
}

/**
 * @brief Read actual battery voltage from ADC.
 * @return Battery voltage in Volts.
 */
float getBatteryVoltage() {
  digitalWrite(BATTERY_ENABLE_PIN, HIGH);
  delay(5);
  int mv = analogReadMilliVolts(BATTERY_ADC_PIN);
  digitalWrite(BATTERY_ENABLE_PIN, LOW);
  return ((mv / 1000.0) * 2);  // Correction for voltage divider
}

void setup() {
  // Configure battery monitoring
  pinMode(BATTERY_ENABLE_PIN, OUTPUT);
  digitalWrite(BATTERY_ENABLE_PIN, HIGH);  // Enable battery monitoring

  // Configure ADC for battery
  analogReadResolution(12);  // 12-bit ADC
  analogSetPinAttenuation(BATTERY_ADC_PIN, ADC_11db);
}

void loop() {
  uint8_t vBatPercentage;
  float vBat;
  
  // Measure battery voltage
  vBat = getBatteryVoltage();
  vBatPercentage = mapFloat(vBat, minVoltage, maxVoltage);
}

Complete program

Thanks to all all features described in this article, I built an advanced weather and information dashboard for the Seeed Studio reTerminal E1002, programmed with the Arduino IDE and displayed on its 7.3″ color e‑paper screen.

Weather station on reTerminal E1002 programmed under Arduino IDE to display data from multiple sources

This project transforms the reTerminal E1002 into a multi‑source data station, combining local sensor readings with online APIs:

  • Retrieves weather forecasts from the Open-Meteo API.
  • Reads temperature sensors from Home Assistant via its REST API.
  • Fetches the Bitcoin price (USD) from CoinGecko.
  • Monitors the reTerminal’s battery level, internal temperature, and humidity via onboard sensors.
  • Presents all information with clear icons and text on the high‑resolution color e‑paper display.
  • Implements deep sleep to save power, with wake‑up triggered by a button press or a timer.
C++
/**
 * @file reTerminalE1002_HA.ino
 * @brief Weather, Home Assistant, and Bitcoin dashboard on Seeed reTerminal E1002 e-paper.
 * 
 * - Fetches weather data from Open-Meteo API.
 * - Fetches Home Assistant sensor temperatures via REST API.
 * - Fetches Bitcoin price in USD from CoinGecko.
 * - Displays all data on reTerminal E1002 7.3" color e-paper.
 * - Handles deep sleep and wake-up via button or timer.
 *   https://tutoduino.fr/
 */

#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <GxEPD2_7C.h>

#include <FreeSans12pt7b_mod.h>
#include <FreeSans18pt7b_mod.h>
#include <FreeSans24pt7b_mod.h>

#include <Wire.h>
#include <SensirionI2cSht4x.h>
#include "icons_50x50.h"
#include "icons_100x100.h"
#include "icons.h"
#include "secrets.h"

// Battery monitoring pins
#define BATTERY_ADC_PIN 1      // GPIO1 - Battery voltage ADC
#define BATTERY_ENABLE_PIN 21  // GPIO21 - Battery monitoring enable

// Serial communication pins for debugging
#define SERIAL_RX 44
#define SERIAL_TX 43

// SPI pinout for ePaper display (verify for your hardware)
#define EPD_SCK_PIN 7
#define EPD_MOSI_PIN 9
#define EPD_CS_PIN 10
#define EPD_DC_PIN 11
#define EPD_RES_PIN 12
#define EPD_BUSY_PIN 13
#define GREEN_BUTTON 3  // Deep sleep wake-up button

// I2C pins for reTerminal E Series
#define I2C_SDA 19
#define I2C_SCL 20

// Select the ePaper driver to use
// 0: reTerminal E1001 (7.5'' B&W)
// 1: reTerminal E1002 (7.3'' Color)
#define EPD_SELECT 1

#if (EPD_SELECT == 0)
#define GxEPD2_DISPLAY_CLASS GxEPD2_BW
#define GxEPD2_DRIVER_CLASS GxEPD2_750_GDEY075T7
#elif (EPD_SELECT == 1)
#define GxEPD2_DISPLAY_CLASS GxEPD2_7C
#define GxEPD2_DRIVER_CLASS GxEPD2_730c_GDEP073E01
#endif

#define MAX_DISPLAY_BUFFER_SIZE 16000

#define MAX_HEIGHT(EPD) \
  (EPD::HEIGHT <= MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8) \
     ? EPD::HEIGHT \
     : MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8))

GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)>
  display(GxEPD2_DRIVER_CLASS(/*CS=*/EPD_CS_PIN, /*DC=*/EPD_DC_PIN,
                              /*RST=*/EPD_RES_PIN, /*BUSY=*/EPD_BUSY_PIN));

// Global variable for SPI communication
SPIClass hspi(HSPI);

// English weekday and month names for date formatting on display
const char *days[] = { "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" };
const char *months[] = { "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" };

// Global weather variables
int D0MinTemp, D0MaxTemp, D1MinTemp, D1MaxTemp, D2MinTemp, D2MaxTemp, D3MinTemp, D3MaxTemp, D4MinTemp, D4MaxTemp;
int D0Code, D1Code, D2Code, D3Code, D4Code;
String localTime = "2025-01-01 00:00";
int currentTemp;
int g_x_start, g_y_start;

// Object to manage onboard SHT4x sensor (I2C temperature/humidity)
SensirionI2cSht4x sht4x;
const float sht4xCalibration = -1;  // SHT4x calibration offset

/**
 * @brief Fetch temperature from Home Assistant REST API.
 * @param entityId - Home Assistant sensor entity (e.g., "sensor.lumi_lumi_weather_temperature")
 * @return Temperature value or 0.0 if error.
 */
float getHomeAssistantSensorState(String entityId) {
  if (WiFi.status() != WL_CONNECTED) {
    Serial1.println("ERROR: Wi-Fi not connected.");
    return 0.0;
  }
  HTTPClient http;
  String url = String(ha_url) + entityId;
  http.begin(url);
  http.addHeader("Authorization", "Bearer " + String(ha_token));
  http.addHeader("Content-Type", "application/json");
  int httpCode = http.GET();
  if (httpCode != HTTP_CODE_OK) {
    Serial1.print("HTTP Error: ");
    Serial1.println(httpCode);
    Serial1.println(http.getString());
    http.end();
    return 0.0;
  }
  DynamicJsonDocument doc(1024);
  DeserializationError error = deserializeJson(doc, http.getString());
  http.end();
  if (error) {
    Serial1.print("JSON parsing error: ");
    Serial1.println(error.c_str());
    return 0.0;
  }
  if (!doc.containsKey("state")) {
    Serial1.println("ERROR: 'state' field missing in JSON response.");
    return 0.0;
  }
  float state = doc["state"].as<float>();
  return state;
}

/**
 * @brief Fetch weather data from Open-Meteo API (current and next 4 days).
 * @return 0 on success, <0 on error.
 */
int fetchWeatherData() {
  if (WiFi.status() != WL_CONNECTED) {
    Serial1.println("WiFi not connected");
    return -1;
  }
  HTTPClient http;
  http.begin(weather_url);
  int httpCode = http.GET();
  if (httpCode != HTTP_CODE_OK) {
    Serial1.print("HTTP Error: ");
    Serial1.println(httpCode);
    Serial1.println(http.getString());
    http.end();
    return -2;
  }
  DynamicJsonDocument doc(1024);
  DeserializationError error = deserializeJson(doc, http.getString());
  http.end();
  if (error) {
    Serial1.print("JSON parsing error: ");
    Serial1.println(error.c_str());
    return -3;
  }
  localTime = doc["current"]["time"].as<String>();
  currentTemp = doc["current"]["temperature"].as<int>();
  D0MinTemp = doc["daily"]["temperature_2m_min"][0].as<int>();
  D0MaxTemp = doc["daily"]["temperature_2m_max"][0].as<int>();
  D0Code = doc["daily"]["weather_code"][0].as<int>();
  D1MinTemp = doc["daily"]["temperature_2m_min"][1].as<int>();
  D1MaxTemp = doc["daily"]["temperature_2m_max"][1].as<int>();
  D1Code = doc["daily"]["weather_code"][1].as<int>();
  D2MinTemp = doc["daily"]["temperature_2m_min"][2].as<int>();
  D2MaxTemp = doc["daily"]["temperature_2m_max"][2].as<int>();
  D2Code = doc["daily"]["weather_code"][2].as<int>();
  D3MinTemp = doc["daily"]["temperature_2m_min"][3].as<int>();
  D3MaxTemp = doc["daily"]["temperature_2m_max"][3].as<int>();
  D3Code = doc["daily"]["weather_code"][3].as<int>();
  D4MinTemp = doc["daily"]["temperature_2m_min"][4].as<int>();
  D4MaxTemp = doc["daily"]["temperature_2m_max"][4].as<int>();
  D4Code = doc["daily"]["weather_code"][4].as<int>();
  return 0;
}

/**
 * @brief Get Bitcoin price (USD) from CoinGecko API.
 * @return Bitcoin price as integer, 0 if not available.
 */
int getBTC() {
  int btc = 0;
  if (WiFi.status() == WL_CONNECTED) {
    HTTPClient http;
    http.begin(btc_url);
    int httpCode = http.GET();
    if (httpCode == HTTP_CODE_OK) {
      DynamicJsonDocument doc(1024);
      if (!deserializeJson(doc, http.getString()))
        btc = doc["bitcoin"]["usd"].as<int>();
    }
    http.end();
  }
  return btc;
}

/**
 * @brief Zeller's congruence to compute day of week (0=Sunday).
 * @param y Year
 * @param m Month
 * @param d Day
 * @return Weekday index
 */
int dayOfTheWeek(int y, int m, int d) {
  if (m < 3) {
    m += 12;
    y -= 1;
  }
  int K = y % 100;
  int J = y / 100;
  int h = (d + 13 * (m + 1) / 5 + K + K / 4 + J / 4 + 5 * J) % 7;
  return ((h + 6) % 7);  // 0=Sunday
}

/**
 * @brief Formats ISO date string as US date for display.
 * @param dateStr "YYYY-MM-DD HH:MM"
 * @return "Saturday, October 4, 2025" style string.
 */
String formatDateEN(String dateStr) {
  int y = dateStr.substring(0, 4).toInt();
  int m = dateStr.substring(5, 7).toInt();
  int d = dateStr.substring(8, 10).toInt();
  int wday = dayOfTheWeek(y, m, d);
  return String(String(days[wday]) + ", " + String(months[m - 1]) + " " + String(d) + ", " + String(y));
}

/**
 * @brief Maps Open-Meteo weather code to icon index.
 * @param weatherCode Open-Meteo code
 * @return Icon bitmap index
 */
int weatherCodeToIcon(int weatherCode) {
  switch (weatherCode) {
    case 0: return 7;  // Sun
    case 1:
    case 2: return 3;  // Some clouds
    case 3: return 5;  // Cloudy
    case 45:
    case 48: return 2;  // Fog
    case 51:
    case 53:
    case 55:
    case 56:
    case 57: return 7;  // Freezing drizzle
    case 61:
    case 63:
    case 65:
    case 66:
    case 67: return 6;  // Freezing rain
    case 71:
    case 73:
    case 75: 
    case 77: return 4;  // Snow
    case 80:
    case 81:
    case 82: return 8;  // Showers
    case 85:
    case 86: return 4;  // Snow
    case 95:
    case 96:
    case 99: return 0;  // Thunderstorm
    default: return 5;
  }
}

/**
 * @brief Display forecast for one day at (x, y) with weather icon.
 */
void displayForecast(int x, int y, int day, int min, int max, int iconNb) {
  int16_t x1, y1;
  uint16_t w, h;
  display.drawBitmap(x + 30, y + 50, epd_bitmap_allArray[iconNb], 50, 50, GxEPD_BLACK);
  display.setFont(&FreeSans12pt7b);
  display.getTextBounds(days[day], x, y, &x1, &y1, &w, &h);
  display.setCursor(x + 55 - w / 2, y + 40);
  display.print(days[day]);
  display.setFont(&FreeSans12pt7b);
  display.getTextBounds(String(max) + "`", x, y, &x1, &y1, &w, &h);
  display.setCursor(x + 55 - w / 2, y + 130);
  display.print(max, 0);
  display.write(0x60);
  display.getTextBounds(String(min) + "`", x, y, &x1, &y1, &w, &h);
  display.setCursor(x + 55 - w / 2, y + 160);
  display.print(min, 0);
  display.write(0x60);
}

/**
 * @brief Displays today's weather with large icon and temperature.
 */
void displayCurrent(int x, int y, float current, int min, int max, int iconNb) {
  int16_t x1, y1;
  uint16_t w, h;
  display.drawBitmap(x + 30, y + 50, epd_bitmap2_allArray[iconNb], 100, 100, GxEPD_BLACK);
  display.setFont(&FreeSans24pt7b);
  display.setCursor(x + 150, y + 130);
  display.print(current, 0);
  display.write(0x60);
  display.setFont(&FreeSans12pt7b);

  display.getTextBounds(String(max) + "`", 0, 0, &x1, &y1, &w, &h);
  display.setCursor(x + 80 - w / 2, y + 170);
  display.print(max, 0);
  display.write(0x60);

  display.getTextBounds(String(min) + "`", 0, 0, &x1, &y1, &w, &h);
  display.setCursor(x + 80 - w / 2, y + 200);
  display.print(min, 0);
  display.write(0x60);
}

/**
 * @brief Estimate battery percentage from voltage.
 * @return Battery percent, min 0 to max 100.
 */
int getBatteryPercent() {
  float batteryVoltage = getBatteryVoltage();
  if (batteryVoltage > 4.15)
    return 100;
  else if (batteryVoltage > 3.96)
    return 90;
  else if (batteryVoltage > 3.91)
    return 80;
  else if (batteryVoltage > 3.85)
    return 70;
  else if (batteryVoltage > 3.80)
    return 60;
  else if (batteryVoltage > 3.75)
    return 50;
  else if (batteryVoltage > 3.68)
    return 40;
  else if (batteryVoltage > 3.58)
    return 30;
  else if (batteryVoltage > 3.49)
    return 20;
  else if (batteryVoltage > 3.41)
    return 10;
  else if (batteryVoltage > 3.30)
    return 5;
  else
    return 0;
}

/**
 * @brief Read actual battery voltage from ADC.
 * @return Battery voltage in Volts.
 */
float getBatteryVoltage() {
  digitalWrite(BATTERY_ENABLE_PIN, HIGH);
  delay(5);
  int mv = analogReadMilliVolts(BATTERY_ADC_PIN);
  digitalWrite(BATTERY_ENABLE_PIN, LOW);
  return ((mv / 1000.0) * 2);  // Correction for voltage divider
}

/**
 * @brief Arduino setup: serial, display, WiFi, sensors.
 */
void setup() {
  Serial1.begin(115200, SERIAL_8N1, SERIAL_RX, SERIAL_TX);
  delay(500);
  pinMode(GREEN_BUTTON, INPUT_PULLUP);

  // Prepare deep sleep wake-up on button press
  esp_sleep_enable_ext0_wakeup((gpio_num_t)GREEN_BUTTON, LOW);

  // SPI and display initialization
  hspi.begin(EPD_SCK_PIN, -1, EPD_MOSI_PIN, -1);
  display.epd2.selectSPI(hspi, SPISettings(2000000, MSBFIRST, SPI_MODE0));
  display.init(0);
  display.setRotation(0);

  // Connect to WiFi
  WiFi.begin(wifi_ssid, wifi_password);
  Serial1.print("Connecting to WiFi...");
  unsigned long start = millis();
  while (WiFi.status() != WL_CONNECTED && (millis() - start) < 15000) {
    delay(500);
    Serial1.print(".");
  }
  if (WiFi.status() != WL_CONNECTED) {
    Serial1.println("[ERROR] WiFi connection failed, restart required.");
    ESP.restart();
  }

  // Initialize I2C with custom pins
  Wire.begin(I2C_SDA, I2C_SCL);

  // Initialize onboard SHT4x sensor
  sht4x.begin(Wire, 0x44);

  // Configure battery monitoring
  pinMode(BATTERY_ENABLE_PIN, OUTPUT);
  digitalWrite(BATTERY_ENABLE_PIN, HIGH);  // Enable battery monitoring

  // Configure ADC for battery
  analogReadResolution(12);  // 12-bit ADC
  analogSetPinAttenuation(BATTERY_ADC_PIN, ADC_11db);
}

/**
 * @brief Main loop: fetch sensors, display dashboard, go to deep sleep.
 */
void loop() {
  int x, y;
  int x_ha_box, y_ha_box, ha_box_w, ha_box_h;                          // home assistant box
  int x_current_box, y_current_box, current_box_w, current_box_h;      // current weather box
  int x_forecast_box, y_forecast_box, forecast_box_w, forecast_box_h;  // forecast box
  int x_bitcoin_box, y_bitcoin_box, bitcoin_box_w, bitcoin_box_h;      // bitcoin box
  int x_battery_box, y_battery_box, battery_box_w, battery_box_h;      // battery box
  int16_t x1, y1;
  uint16_t w, h;

  float internalTemperatureSensor, externalTemperatureSensor, greenHouseTemperature;
  float min, max;
  String icon;
  float sht4xTemperature, sht4xHumidity;

  // Measure reTerminal internal SHT4 sensor
  uint16_t error = sht4x.measureMediumPrecision(sht4xTemperature, sht4xHumidity);
  sht4xTemperature += sht4xCalibration;

  float batteryVoltage = getBatteryVoltage();
  int btc = getBTC();

  // Fetch weather data for current and next 4 days
  if (fetchWeatherData() != 0) {
    Serial1.print("fetchWeatherData failed!");
    return;
  }

  String dateEN = formatDateEN(localTime);  // French formatted date
  int today = dayOfTheWeek(localTime.substring(0, 4).toInt(), localTime.substring(5, 7).toInt(), localTime.substring(8, 10).toInt());

  // Fetch Home Assistant sensor states
  internalTemperatureSensor = getHomeAssistantSensorState("sensor.tutoduino_esp32c6tempsensor_temperature");
  externalTemperatureSensor = getHomeAssistantSensorState("sensor.lumi_lumi_weather_temperature");
  greenHouseTemperature = getHomeAssistantSensorState("sensor.lumi_lumi_weather_temperature_2");

  display.setFullWindow();
  display.firstPage();

  do {
    display.fillScreen(GxEPD_WHITE);
    display.setTextColor(GxEPD_BLACK);

    // Display date on top
    display.setFont(&FreeSans18pt7b);
    display.getTextBounds(dateEN, 0, 2, &x1, &y1, &w, &h);
    display.setCursor(400 - w / 2, 40);
    display.print(dateEN);

    // Current weather box
    x_current_box = 30;
    y_current_box = 60;
    current_box_w = 240;
    current_box_h = 210;
    display.fillRect(x_current_box, y_current_box, current_box_w, 40, GxEPD_GREEN);
    display.drawRect(x_current_box, y_current_box, current_box_w, current_box_h, GxEPD_BLACK);
    display.drawRect(x_current_box, y_current_box, current_box_w, 40, GxEPD_BLACK);

    display.setFont(&FreeSans12pt7b);
    display.getTextBounds("Today", 0, 0, &x1, &y1, &w, &h);
    display.setCursor(x_current_box + current_box_w / 2 - w / 2, y_current_box + 30);
    display.print("Today");
    displayCurrent(x_current_box, y_current_box, currentTemp, D0MinTemp, D0MaxTemp, weatherCodeToIcon(D0Code));

    // Forecast box for next 4 days
    x_forecast_box = 300;
    y_forecast_box = 60;
    forecast_box_w = 480;
    forecast_box_h = 210;
    display.fillRect(x_forecast_box, y_forecast_box, forecast_box_w, 40, GxEPD_GREEN);
    display.drawRect(x_forecast_box, y_forecast_box, forecast_box_w, forecast_box_h, GxEPD_BLACK);
    display.drawRect(x_forecast_box, y_forecast_box, forecast_box_w, 40, GxEPD_BLACK);
    display.setFont(&FreeSans12pt7b);
    display.getTextBounds("Forecast", 0, 0, &x1, &y1, &w, &h);
    display.setCursor(x_forecast_box + forecast_box_w / 2 - w / 2, y_forecast_box + 30);
    display.print("Forecast");

    displayForecast(x_forecast_box+10, y_forecast_box+40, (today + 1) % 7, D1MinTemp, D1MaxTemp, weatherCodeToIcon(D1Code));
    displayForecast(x_forecast_box+120, y_forecast_box+40, (today + 2) % 7, D2MinTemp, D2MaxTemp, weatherCodeToIcon(D2Code));
    displayForecast(x_forecast_box+240, y_forecast_box+40, (today + 3) % 7, D3MinTemp, D3MaxTemp, weatherCodeToIcon(D3Code));
    displayForecast(x_forecast_box+360, y_forecast_box+40, (today + 4) % 7, D4MinTemp, D4MaxTemp, weatherCodeToIcon(D4Code));

    // Home Assistant sensors box
    x_ha_box = 30;
    y_ha_box = 300;
    ha_box_w = 400;
    ha_box_h = 150;
    display.fillRect(x_ha_box, y_ha_box, ha_box_w, 40, GxEPD_GREEN);
    display.drawRect(x_ha_box, y_ha_box, ha_box_w, ha_box_h, GxEPD_BLACK);
    display.drawRect(x_ha_box, y_ha_box, ha_box_w, 40, GxEPD_BLACK);
    display.setFont(&FreeSans12pt7b);
    display.getTextBounds("Home Assistant Sensors", 0, 0, &x1, &y1, &w, &h);
    display.setCursor(x_ha_box + ha_box_w / 2 - w / 2, y_ha_box + 30);
    display.print("Home Assistant Sensors");

    // Internal temperature from SHT4
    display.setFont(&FreeSans12pt7b);
    display.getTextBounds("Indoor", 0, 0, &x1, &y1, &w, &h);
    display.setCursor(x_ha_box + ha_box_w / 6 - w / 2, y_ha_box + 70);
    display.print("Indoor");
    display.setFont(&FreeSans18pt7b);
    display.getTextBounds(String(sht4xTemperature, 1) + "`", 0, 0, &x1, &y1, &w, &h);
    display.setCursor(x_ha_box + ha_box_w / 6 - w / 2, y_ha_box + 120);
    display.print(sht4xTemperature, 1);
    display.write(0x60);

    // Outdoor temperature (Home Assistant)
    display.setFont(&FreeSans12pt7b);
    display.getTextBounds("Outdoor", 0, 0, &x1, &y1, &w, &h);
    display.setCursor(x_ha_box + ha_box_w / 2 - w / 2, y_ha_box + 70);
    display.print("Outdoor");
    display.setFont(&FreeSans18pt7b);
    display.getTextBounds(String(externalTemperatureSensor, 1) + "`", 0, 0, &x1, &y1, &w, &h);
    display.setCursor(x_ha_box + ha_box_w / 2 - w / 2, y_ha_box + 120);
    display.print(externalTemperatureSensor, 1);
    display.write(0x60);

    // Greenhouse temperature (Home Assistant)
    display.setFont(&FreeSans12pt7b);
    display.getTextBounds("Room", 0, 0, &x1, &y1, &w, &h);
    display.setCursor(x_ha_box + 5 * ha_box_w / 6 - w / 2, y_ha_box + 70);
    display.print("Room");
    display.setFont(&FreeSans18pt7b);
    display.getTextBounds(String(greenHouseTemperature, 1) + "`", 0, 0, &x1, &y1, &w, &h);
    display.setCursor(x_ha_box + 5 * ha_box_w / 6 - w / 2, y_ha_box + 120);
    display.print(greenHouseTemperature, 1);
    display.write(0x60);

    // Bitcoin price box
    x_bitcoin_box = 460;
    y_bitcoin_box = 300;
    bitcoin_box_w = 150;
    bitcoin_box_h = 150;
    display.fillRect(x_bitcoin_box, y_bitcoin_box, bitcoin_box_w, 40, GxEPD_GREEN);
    display.drawRect(x_bitcoin_box, y_bitcoin_box, bitcoin_box_w, bitcoin_box_h, GxEPD_BLACK);
    display.drawRect(x_bitcoin_box, y_bitcoin_box, bitcoin_box_w, 40, GxEPD_BLACK);
    display.setFont(&FreeSans12pt7b);
    display.getTextBounds("Bitcoin", 0, 0, &x1, &y1, &w, &h);
    display.setCursor(x_bitcoin_box + bitcoin_box_w / 2 - w / 2, y_bitcoin_box + 30);
    display.print("Bitcoin");
    display.drawBitmap(x_bitcoin_box, y_bitcoin_box + 70, epd_bitmap3_allArray[0], 40, 40, GxEPD_BLACK);
    display.setFont(&FreeSans12pt7b);
    display.setCursor(x_bitcoin_box + 40, y_bitcoin_box + 100);
    display.print(btc);
    display.print(" $");

    // Battery percentage box
    x_battery_box = 640;
    y_battery_box = 300;
    battery_box_w = 140;
    battery_box_h = 150;
    display.fillRect(x_battery_box, y_battery_box, battery_box_w, 40, GxEPD_GREEN);
    display.drawRect(x_battery_box, y_battery_box, battery_box_w, battery_box_h, GxEPD_BLACK);
    display.drawRect(x_battery_box, y_battery_box, battery_box_w, 40, GxEPD_BLACK);
    display.setFont(&FreeSans12pt7b);
    display.getTextBounds("Battery", 0, 0, &x1, &y1, &w, &h);
    display.setCursor(x_battery_box + battery_box_w / 2 - w / 2, y_battery_box + 30);
    display.print("Battery");
    display.drawBitmap(x_battery_box + 10, y_battery_box + 70, epd_bitmap3_allArray[1], 40, 40, GxEPD_BLACK);
    display.setFont(&FreeSans12pt7b);
    display.setCursor(x_battery_box + 50, y_battery_box + 100);
    display.print(getBatteryPercent());
    display.print(" %");

  } while (display.nextPage());

  display.hibernate();
  delay(1000);
  esp_sleep_enable_timer_wakeup(30 * 60 * 1000000);  // 30 minutes sleep
  Serial1.println("Deep sleep 30min...");
  esp_deep_sleep_start();
}

The full source code is available on my GitHub, including icons and fonts used in this example.

I hope you found this article interesting. Feel free to give your opinion by clicking on the stars below.

How useful was this post?

Click on a star to rate it!

Average rating 5 / 5. Vote count: 1

No votes so far! Be the first to rate this post.

We are sorry that this post was not useful for you!

Let us improve this post!

Tell us how we can improve this post?