In this project we're going to build a low power temperature and humidity logger with an ultra-low-power liquid crystal display. The LCD will double as a temperature display, AND we’re going to use CircuitPython Deep Sleep to make sure it lasts for a very, very long time.
Unlike an e-paper display, which takes some seconds to refresh, it takes almost no time at all to update the LCD. This means that once a minute, we can wake from deep sleep, update the display, and then go right back to sleep. This minimizes the amount of time our microcontroller is awake, which makes the LCD FeatherWing a very low power option for a display that needs to update roughly every minute.
What you'll need
We're going to use an LCD FeatherWing for the display, paired with an ESP32-S2 Feather with an integrated BME280 sensor. You'll solder female headers into the ESP32-S2 Feather, and plug the LCD FeatherWing into those.
We'll power the whole gadget with a 400 mAh battery that fits just behind the whole assembly.
![Overhead video of an assembled monochrome LCD screen breakout displaying the time 11:43 A.M.](https://cdn-shop.adafruit.com/product-videos/640x480/5581-03.jpg)
![Angled view of rectangular microcontroller with WiFi module.](https://cdn-shop.adafruit.com/640x480/5303-18.jpg)
![Angled shot of a Header Kit for Feather - 12-pin and 16-pin Female Header Set.](https://cdn-shop.adafruit.com/640x480/2886-00.jpg)
![Slim Lithium Ion Polymer Battery 3.7v 400mAh with JST 2-PH connector and short cable](https://cdn-shop.adafruit.com/640x480/3898-05.jpg)
Assemble the ESP32-S2 Feather
Your ESP32-S2 Feather comes with plain 0.1 inch pin header. Don't use that pin header! Instead, solder the female header in facing up, so that your LCD FeatherWing can plug into the top of the board.
Assemble the LCD FeatherWing
The LCD FeatherWing also comes with a strip of pin header. You will use that header, but first, you'll need to solder the LCD glass into the LCD pins:
![lcd_wing_soldering_for_learn_guide.jpg](https://cdn-learn.adafruit.com/user_assets/assets/000/000/109/large1024/lcd_wing_soldering_for_learn_guide.jpg?1681579608)
Then you'll solder the Feather headers second:
Putting it all together
Set the ESP32-S2 Feather face up on your work area, and plug the LCD FeatherWing into the female headers.
Finally, plug your battery into the Feather's battery port, and optionally secure it to the back using a little bit of blue painter's tape or masking tape.
Setting up the Adafruit.io feeds
First, let's go to Adafruit IO and click on the Feeds tab. There, we'll create a New Group and name it "LCD Logger".
Next, click the Plus sign to the right of the LCD Logger header and create a new feed called "Battery". Repeat this three more times to create feeds called "Humidity", "Pressure" and "Temperature". At the end, you should have four feeds, whose keys are lcd-logger.battery
, lcd-logger.humidity
, lcd-logger.pressure
and lcd-logger.temperature
. This is where your logged data is going to go!
You can also set up a dashboard to monitor your data. That's covered in great detail in this guide.
Writing the Code
Before we put down any code, let's establish what we want the device to do:
- At first boot, we want to connect to wifi and fetch the time to set our clock.
- After that, we want to wake up every minute to update the display.
- Then once an hour, at the top of the hour, we want to connect to Adafruit IO and post the current temperature, humidity and barometric pressure. And the battery voltage too; why not?
- Finally once a day, at the beginning of the day, we want to fetch the time again to correct for any drift.
What data are we going to actually show on the LCD? That's up to you! In this example we're going to display the current temperature, but by uncommenting one of the other lines, you can have the display show the relative humidity or the current time.
Install CircuitPython and required libraries
Download the latest CircuitPython for your board from circuitpython.org, along with the matching library bundle. In addition, download the LCD FeatherWing library. You'll need to copy the following libraries to the lib folder on your CircuitPython board:
- oso_lcd
- adafruit_bme280
- adafruit_io
- adafruit_lc709203f.mpy
- adafruit_requests.mpy
- adafruit_minimqtt
Wifi Setup
First, you're going to need a secrets.py
file to hold your wifi password and Adafruit IO credentials. I can't put the code on this page, but a typical secrets.py file is available in this gist.
Replace the values in this dict with your wifi network details and Adafruit IO username and API key. You should also set your time zone so the clock is synced to your location.
Follow the link to the code!
Now let's move on to our code.py
file. Unfortunately the code doesn't fit on this page, but you can follow along at the gist here.
Understanding a Deep Sleep program
Before we write any code, scroll down to the bottom. You'll notice that the last line is a function call: go_to_sleep
()
. We're going to write that function in just a moment, but the important thing to note is that when our code finishes running, the microcontroller is going to go into deep sleep, and — most importantly — all of the objects and global state that we set up is going to vanish. When the microcontroller wakes up from deep sleep, our entire code.py file runs again from the beginning. This means there's no run loop of any kind; our code.py runs once, terminates, and then runs anew every time we wake from deep sleep.
There is one special kind of memory that can stick around during deep sleep. But we'll talk about that in a moment.
Setting up our global state
After finishing all of our imports, you'll see where we create four objects: we create a requests
variable (which we initialize to None
), a display
for the LCD, a bme280
to interact with our temperature and humidity sensor, and a clock
to interact with the RTC. As our program runs, we'll make use of these to get sensor data and display it, to time our waking up, and to call out to wifi as needed.
Writing functions
In this next section, we write some functions that will help our main program get things done:
Setting up WiFi if needed
The setup_wifi_if_needed()
function checks if the requests
variable we created is None
; if it is, we know that WiFi hasn't been set up. In that case, we clear the LCD and flash the Indicator.WIFI
indicator while we connect to WiFi.
Setting the time with Adafruit IO
Next up, the set_time() function will connect to Adafruit IO and fetch the time, which will be useful to make sure we upload our data at the top of the hour. Note that it first calls setup_wifi_if_needed()
, then makes use of the now-initialized requests
variable to reach out with the credentials from our secrets.py
. Finally, it splits the response apart into a timestamp and a date stamp that we can use to set the clock's datetime.
Uploading data to Adafruit IO
The post_data()
function takes four parameters: temperature
, humidity
, pressure
and battery
. Again it calls setup_wifi_if_needed()
, then creates an io
object to handle interacting with Adafruit IO.
Inside the try
block, we try to post the data to each of the feeds that we set up earlier. If for any reason there's an exception, we display "Error" and then go to sleep; this may be a missed data point, but at least it won't crash our logger.
Going to sleep
Finally, the go_to_sleep()
function is what we'll call at the end of the sketch (or, as above, in the case of an exception). First, we clear all the indicators on the LCD, and set only the Indicator.MOON
icon to signify that we're in deep sleep. We shut down power to our I2C devices, and then create a time_alarm to wake the board up at the top of the next minute (60 - clock.datetime.tm_sec
is the number of seconds until the top of the minute).
Finally, we call alarm.exit_and_deep_sleep_until_alarms(time_alarm)
, which puts the board into deep sleep until the alarm fires.
Writing the main loop
At this point you should be at the comment: # Main loop begins here
! Let's go line by line through what we do, because it's mostly function calls to the code we've already written.
First, we check if alarm.wake_alarm is None
. The only time this will be true is at first boot, since we won't have set an alarm yet. In that case, we call the set_time()
function to make sure we have the correct time going forward.
Next, we get the temperature from the BME280. We need this regardless of whether we're uploading it to Adafruit IO, because we want to update the display every minute.
Finally, the big conditional: we want to upload data to Adafruit IO if clock.datetime.tm_min == 0 and clock.datetime.tm_hour != alarm.sleep_memory[0]
. Let's unpack this a bit. If the current minute is zero, we're at the top of the hour, i.e. 9:00, which means we want to log data. That much is straightforward.
But we're also going to update the time at the top of the hour, and it's possible our RTC has drifted backwards. If that's the case, we could wake up a minute later and find ourselves at the top of the hour again! So our plan is to use the first byte of sleep memory to store the hour when we last updated, and check to make sure that we haven't already updated for that hour.
Assuming we've passed both of these checks, we first stash the current hour in sleep memory, as discussed; then we read the humidity and pressure data from the BME280. We also read the battery voltage from the on-board LC709203F fuel gauge, creating the device and then reading data from it in one line.
Finally, we set the Indicator.DATA
icon on the LCD, and call both post_data
and set_time
. This indicator will remain on while data is being uploaded and downloaded. Then we clear the Indicator.DATA
icon, and fall back through to the main body of our code.
There's not much left to do at this point! All we have to do is print the current temperature to the display, and then go_to_sleep()
.