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.
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.
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.
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:
Then you'll solder the Feather headers second:
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.
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.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.
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.
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:
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.
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.
Before we write any code, scroll down to the bottom. You'll notice that the last line is a function call:
(). 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
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.
In this next section, we write some functions that will help our main program get things done:
Setting up WiFi if needed
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
post_data() function takes four parameters:
battery. Again it calls
setup_wifi_if_needed(), then creates an
io object to handle interacting with Adafruit IO.
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
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.
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. 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
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