Building a Low-Power Solar Weather Station
2024-09-16 | By Maker.io Staff
License: See Original Project Energy Harvesting / Solar
Read on to learn how to utilize various energy-conserving measures to build a little, yet powerful custom weather station that periodically fetches the current day’s weather forecast from an external API before displaying the information on an easy-to-read, three-color e-ink display.
Project Components
This stunning little everyday helper packs quite a punch within its custom 3D-printed enclosure. The device’s low-power three-color e-ink screen retains the displayed information even when disconnected from power, only drawing electricity while refreshing the displayed pixels. Every two hours, an Arduino Nano 33 IoT fetches the current day’s forecast and shows the retrieved information on the e-ink screen. An external low-power timer wakes up the Arduino regularly, thus conserving power by not keeping the MCU board on constantly. A beefy lithium-ion battery provides the electronics with juice at night and on cloudy days, while the solar cells on top of the custom enclosure deliver power when the sun’s out. Excess power generated by the solar cells is used to charge the battery through a specialized protection breakout board that ensures that the battery operates within safe conditions.
Quantity Part
3 - Solar Cells
Understanding the Electronic Connections
The Arduino Nano 33 IoT is the core of this build. Whenever it boots up, the MCU board establishes a WiFi connection to a known network. Once it succeeds, the device pulls the current weather forecast from an external weather API.
The Arduino shuts down between refresh cycles to conserve energy, and an external timer wakes it up periodically. This particular timing module has an input pin that allows the device under control, which is the Arduino in this project, to let the timer know once it is done with its task. Once the timer receives the ready signal, it cuts power to the controlled device. A trimmer on the module adjusts the delay from 100 milliseconds to two hours, which is the setting used in this project. The setting can be selected by turning the trimmer potentiometer all the way to the right.
However, before the Arduino lets the timer know that it can go back to sleep, the microcontroller needs to refresh the data displayed on the e-ink display. This article assumes that you are familiar with using an e-ink display with an Arduino, and you can refer to this article for a helpful refresher on how to work with this type of display. The timing module draws small amounts of current while sleeping, and the breakout module has an LED that turns on whenever the timer wakes up the Arduino. However, you can disable this LED by cutting a trace on the underside of the board. Bridging the two contacts enables the LED:
Cut the small trace that connects the highlighted pads to disable the power-on LED.
Finally, the battery and the solar cells interface with the charge controller, which acts as this project’s primary power delivery system. You can choose from a wide range of solar panel models and wiring configurations. Generally, connecting the panels in parallel increases the output current, thus facilitating faster or slower charge rates. Connecting multiple panels in series increases the output voltage, which might be necessary for certain types of solar cells. In either case, it’s vital to ensure that the solar panel assembly’s output voltage and current that go into the charge controller do not exceed the controller’s maximum allowed values, which are 1.5 Amperes and 10 Volts in this case. Similar to enabling and disabling the timer module’s LED, you can cut and bridge one of the three pads on the underside of the charge controller to set the maximum charge current. The module defaults to one Ampere.
I decided to use three 3V solar panels with an output of 100mA each in series to reach a sensible maximum Voltage of around 9V in full, direct sunlight. This value should establish a good balance between charge rate and maintaining maximum battery life.
This wiring diagram illustrates how to connect the components in the project. Scheme-It link.
The Arduino attaches to the display via a piece of prototyping perfboard and female pin headers. This approach ensures that the vital electronic parts are mounted in a compact and reliable way, which facilitates effortless final assembly.
The Arduino is mounted on a small piece of perfboard that connects the MCU to the display via a female pin header.
However, the timer and the charge controller are attached to the bottom half of the case via screws and connected with soldered wires. This approach results in the entire design being more flexible and allowing the use of different charge controllers and timers. Utilizing this approach also minimizes the risk of short circuits due to loose wires, which is crucial in preventing hazardous faults in battery-operated projects.
The other components are attached to the case using screws. The battery sits in its own compartment.
Installing the Necessary Arduino Libraries
This project’s firmware requires the following libraries to be installed via the Arduino IDE’s library manager:
ArduinoHttpClient
WiFiNINA
Adafruit GFX Library
Adafruit EPD
Adafruit ImageReader Library
ArduinoJson
You should also update the Arduino’s firmware by following this guide, as older versions contain a known bug that prohibits the board from connecting to an API that uses SSL over HTTPS, as is the case with the weather API used in this project. However, there are no issues with firmware revision 1.5.0 on the Arduino Nano 33 IoT.
The Project’s Firmware
The sketch first defines the external libraries it uses and the GPIO pins for interfacing with the e-ink screen:
#include <SPI.h> #include <WiFiNINA.h> #include <ArduinoJson.h> #include <ArduinoHttpClient.h> #include "Adafruit_ThinkInk.h" #define EPD_CS 9 #define EPD_DC 10 #define SRAM_CS 6 #define EPD_RESET 7 #define EPD_BUSY 8 #define TIMER_INTERRUPT 3
It then creates some variables for storing the WiFi credentials, the external API’s URL and port, and the variables that hold the information returned by the API:
const char ssid[] = "YOUR_WIFI_SSID"; const char pass[] = "YOUR_WIFI_PASSWORD"; const char serverAddress[] = "wttr.in"; const int port = 443; String temperature; String humidity; String visibility; String pressure; String windSpeed; String uvIndex; String windDir; String area; String condition; bool parseSuccess = false;
Then, the code also contains three objects. The first two represent the connection to the external API, and the third one is used for communicating with the display:
WiFiSSLClient wifi; HttpClient client = HttpClient(wifi, serverAddress, port); ThinkInk_290_Tricolor_Z10 display(EPD_DC, EPD_RESET, EPD_CS, SRAM_CS, EPD_BUSY, &SPI);
The remainder of this project’s code is structured into helper methods that each perform one of the functions necessary for connecting to an external WiFi network, initializing the e-ink display, fetching the weather data, extracting the relevant information from the server’s response, and finally displaying the information on the display. The setup method calls the helpers in the correct order. When all steps are finished, the setup method informs the timer module that the Arduino is done and ready to shut down:
void setup() { connectNetwork(); initDisplay(); initWeather(); displayContents(); digitalWrite(TIMER_INTERRUPT, HIGH); } void loop() { /* Empty loop method */ }
In the first helper function, the Arduino connects to a known WiFi each time it boots up. This procedure is necessary as the external timer completely cuts power to the MCU board, which means that it restarts each time the timer restores power to the device:
void connectNetwork() { int retries = 0; int status = WiFi.begin(ssid, pass); while (status != WL_CONNECTED) { if (retries >= 10) { // Turn off after 10 unsuccessful retries digitalWrite(TIMER_INTERRUPT, HIGH); } delay(1000); status = WiFi.begin(ssid, pass); retries = retries + 1; } }
When the loop retries to establish a connection to the WiFi network, it waits 1000 milliseconds before consecutive attempts. If that process fails ten times, the Arduino instructs the timing module to cut power to conserve energy.
Once connected, the Arduino initializes the display by sending a set of commands:
void initDisplay() { display.begin(THINKINK_TRICOLOR); display.clearBuffer(); display.setRotation(2); display.setTextColor(EPD_BLACK); display.setTextWrap(false); }
Next, the board establishes a connection to an external weather API and requests the weather forecast for the current day:
void initWeather() { int retries = 0; while (!wifi.connectSSL(serverAddress, port)) { if (retries >= 10) { digitalWrite(TIMER_INTERRUPT, HIGH); } delay(1500); retries = retries + 1; } client.get("/?format=j2"); // j2 = Minimal report as JSON int statusCode = client.responseStatusCode(); String response = client.responseBody(); if (statusCode == 200) { parseSuccess = parseApiJson(response); } }
Like before, the Arduino instructs the timer to cut power after ten failed connection attempts to save energy. Otherwise, it checks the values returned by the weather API. When the API returns the code 200, which indicates success, the Arduino calls another helper to extract various values from the server response, which it stores in the variables defined at the start of the sketch:
bool parseApiJson(String apiResponse) { StaticJsonDocument<2048> doc; DeserializationError error = deserializeJson(doc, apiResponse); if (error) { Serial.print("Failed to parse JSON: "); Serial.println(error.c_str()); return false; } String areaName = String(doc["nearest_area"][0]["areaName"][0]["value"].as<const char*>()); String country = String(doc["nearest_area"][0]["country"][0]["value"].as<const char*>()); condition = String(doc["current_condition"][0]["weatherDesc"][0]["value"].as<const char*>()); windDir = String(doc["current_condition"][0]["winddir16Point"].as<const char*>()); windSpeed = String(doc["current_condition"][0]["windspeedKmph"].as<int>()) + "km/h"; visibility = String(doc["current_condition"][0]["visibility"].as<int>()) + "km"; pressure = String(doc["current_condition"][0]["pressure"].as<int>()) + "mBar"; humidity = String(doc["current_condition"][0]["humidity"].as<int>()) + "%"; uvIndex = String(doc["current_condition"][0]["uvIndex"].as<int>()); temperature = String(doc["current_condition"][0]["temp_C"].as<int>()) + "C"; area = areaName + ", " + country; return true; }
This helper deserializes the response returned by the API using the external ArduinoJson library. If this process results in an error, the function returns false to let the subsequent display function know that something went wrong, which lets it display an error message. Otherwise, the function extracts certain values from the API response and copies them to the firmware’s variables. Finally, the function returns true on success or false in case of an error.
The Arduino then displays the extracted information on the e-ink display using the Adafruit GFX library. A short helper method helps align text on the display’s right edge by calculating a label’s x position on the display based on the text length and font size:
int getRightAlignXPosition(String toAlign, int marginRight, int fontSize) { return display.width() - marginRight - toAlign.length() * 6 * fontSize; } void displayContents() { if (parseSuccess) { display.setCursor(10, 10); display.print("Weather Forecast for " + area); display.setCursor(10, 30); display.setTextColor(EPD_RED); display.setTextSize(5); display.print(temperature); display.setTextSize(2); display.setCursor(10, 80); display.print(condition); display.setCursor(10, 100); display.print("Wind: "); display.setCursor(70, 100); display.print(String(windSpeed) + " " + windDir); display.setTextColor(EPD_BLACK); display.setTextSize(1); display.setCursor(165, 30); display.print("Humidity: "); display.setCursor(getRightAlignXPosition(humidity, 10, 1), 30); display.print(humidity); display.setCursor(165, 40); display.print("Visibility: "); display.setCursor(getRightAlignXPosition(visibility, 10, 1), 40); display.print(visibility); display.setCursor(165, 50); display.print("Pressure: "); display.setCursor(getRightAlignXPosition(pressure, 10, 1), 50); display.print(pressure); display.setCursor(165, 60); display.print("UV Index: "); display.setCursor(getRightAlignXPosition(uvIndex, 10, 1), 60); display.print(uvIndex); } else { display.setCursor((display.width() - 180)/2, (display.height() - 24)/2); display.print("Parse Error!"); } display.display(); }
Finally, the Arduino informs the external timer module that it’s done and ready to shut down by pulling the GPIO pin, which is connected to the timer module’s DONE pin, high.
Assembling the Device
All electronic components of this project are housed within a custom 3D-printed case, which can be downloaded here and consists of two parts. The previously assembled breadboard circuit combines the display and Arduino into a neat, compact package attached to the back of the screen. The display module slides into grooves toward the front of the enclosure, which holds all parts in place once the two halves are combined.
The display assembly slides into a groove in the case.
The charge controller and timer are attached to the bottom half of the enclosure with nuts and bolts:
Nuts and bolts secure the other modules to the bottom half of the 3D-printed enclosure.
The battery is in a compartment towards the rear of the case, and the solar panels are mounted on top of the plastic enclosure using glue. The case is held together with a friction fit using the locating pins in either of the case’s four corners.
Summary
This image shows a close-up of the screen displaying the weather forecast.
This low-power weather station’s 3D-printed enclosure contains more than one would expect. The centerpiece of the device is an Arduino Nano 33 IoT. Whenever it powers up, the MCU connects to a local WiFi network, downloads the current weather forecast, and displays the result on a low-power e-ink screen. This type of display retains the pixel data even when not supplied with energy, and it only requires power when refreshing the screen’s content.
Three external components are responsible for delivering power to the electronics. A simple, low-power timing module periodically activates the Arduino every two hours. A beefy LiPo battery supplies the electronics with power. A charging module and solar panels on top of the device ensure the battery is always topped up.
Have questions or comments? Continue the conversation on TechForum, DigiKey's online community and technical resource.
Visit TechForum