Once you've finished setting up your Feather ESP32-S2 TFT with CircuitPython, you can access the code and necessary libraries by downloading the Project Bundle.
To do this, click on the Download Project Bundle button in the window below. It will download as a zipped folder.
# SPDX-FileCopyrightText: 2022 Liz Clark for Adafruit Industries
# SPDX-License-Identifier: MIT
from os import getenv
import time
import json
import supervisor
import simpleio
import vectorio
import board
import terminalio
import rtc
import socketpool
import wifi
import displayio
import adafruit_ntp
import adafruit_connection_manager
from adafruit_display_text import bitmap_label, wrap_text_to_lines
from adafruit_bitmap_font import bitmap_font
from adafruit_azureiot import IoTCentralDevice
import adafruit_max1704x
import adafruit_bme680
#from adafruit_lc709203f import LC709203F, PackSize
# Get WiFi details, ensure these are setup in settings.toml
ssid = getenv("CIRCUITPY_WIFI_SSID")
password = getenv("CIRCUITPY_WIFI_PASSWORD")
if None in [ssid, password]:
raise RuntimeError(
"WiFi settings are kept in settings.toml, "
"please add them there. The settings file must contain "
"'CIRCUITPY_WIFI_SSID', 'CIRCUITPY_WIFI_PASSWORD', "
"at a minimum."
)
print("Connecting to WiFi...")
wifi.radio.connect(ssid, password)
print("Connected to WiFi!")
# ntp clock - update tz_offset to your timezone
pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio)
ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio)
ntp = adafruit_ntp.NTP(pool, tz_offset=-4)
rtc.RTC().datetime = ntp.datetime
if time.localtime().tm_year < 2022:
print("Setting System Time in UTC")
rtc.RTC().datetime = ntp.datetime
else:
print("Year seems good, skipping set time.")
cal = ntp.datetime
year = cal[0]
mon = cal[1]
day = cal[2]
hour = cal[3]
minute = cal[4]
# To use Azure IoT Central, you will need to create an IoT Central app.
# You can either create a free tier app that will live for 7 days without an Azure subscription,
# Or a standard tier app that will last for ever with an Azure subscription.
# The standard tiers are free for up to 2 devices
#
# If you don't have an Azure subscription:
#
# If you are a student, head to https://aka.ms/FreeStudentAzure and sign up, validating with your
# student email address. This will give you $100 of Azure credit and free tiers of a load of
# service, renewable each year you are a student
#
# If you are not a student, head to https://aka.ms/FreeAz and sign up to get $200 of credit for 30
# days, as well as free tiers of a load of services
#
# Create an Azure IoT Central app by following these
# instructions: https://aka.ms/CreateIoTCentralApp
# Add a device template with telemetry, properties and commands, as well as a view to visualize the
# telemetry and execute commands, and a form to set properties.
#
# Next create a device using the device template, and select Connect to get the
# device connection details.
# Add the connection details to your settings.toml file, using the following values:
#
# 'id_scope' - the devices ID scope
# 'device_id' - the devices device id
# 'device_sas_key' - the devices primary key
#
# The adafruit-circuitpython-azureiot library depends on the following libraries:
#
# From the Adafruit CircuitPython Bundle
# (https://github.com/adafruit/Adafruit_CircuitPython_Bundle):
# * adafruit-circuitpython-minimqtt
# * adafruit-circuitpython-requests
# Create sensor object, communicating over the board's default I2C bus
i2c = board.I2C() # uses board.SCL and board.SDA
# i2c = board.STEMMA_I2C() # For using the built-in STEMMA QT connector on a microcontroller
bme680 = adafruit_bme680.Adafruit_BME680_I2C(i2c, debug=False)
# change this to match the location's pressure (hPa) at sea level
bme680.sea_level_pressure = 1013.25
# Create an IoT Hub device client and connect
esp = None
pool = socketpool.SocketPool(wifi.radio)
device = IoTCentralDevice(
pool, ssl_context, getenv("id_scope"), getenv("device_id"), getenv("device_primary_key")
)
print("Connecting to Azure IoT Central...")
device.connect()
print("Connected to Azure IoT Central!")
temperature_offset = -5
# Create sensor object, using the board's default I2C bus.
#battery_monitor = LC709203F(i2c)
battery_monitor = adafruit_max1704x.MAX17048(board.I2C())
# Update to match the mAh of your battery for more accurate readings.
# Can be MAH100, MAH200, MAH400, MAH500, MAH1000, MAH2000, MAH3000.
# Choose the closest match. Include "PackSize." before it, as shown.
#battery_monitor.pack_size = PackSize.MAH2000
temp = int((bme680.temperature * 9/5) + (32 + temperature_offset))
humidity = int(bme680.relative_humidity)
pressure = int(bme680.pressure)
battery = battery_monitor.cell_percent
# display setup
display = board.DISPLAY
palette0 = displayio.Palette(2)
palette0[0] = 0x00FF00
palette0[1] = 0xFF0000
# load bitmap
bitmap = displayio.OnDiskBitmap("/bmeTFT.bmp")
tile_grid = displayio.TileGrid(bitmap, pixel_shader=bitmap.pixel_shader)
group = displayio.Group()
group.append(tile_grid)
# rectangle for battery life monitor
# vectorio allows for resizing in the loop
rect = vectorio.Rectangle(pixel_shader=palette0, width=22, height=10, x=12, y=116, color_index = 0)
group.append(rect)
# bitmap font
font_file = "/roundedHeavy-26.bdf"
font = bitmap_font.load_font(font_file)
# text elements
temp_text = bitmap_label.Label(font, text="%0.1f° F" % temp, x=20, y=80, color=0xFFFFFF)
humid_text = bitmap_label.Label(font, text="%0.1f %%" % humidity, x=95, y=80, color=0xFFFFFF)
press_text = bitmap_label.Label(font, text="%0.2f" % pressure, x=170, y=80, color=0xFFFFFF)
time_text = bitmap_label.Label(terminalio.FONT,
text="\n".join(wrap_text_to_lines
("Data sent on %s/%s/%s at %s:%s" % (mon,day,year,hour,minute), 20)),
x=125, y=105, color=0xFFFFFF)
group.append(temp_text)
group.append(humid_text)
group.append(press_text)
group.append(time_text)
display.root_group = group
# clock to count down to sending data to Azure
azure_clock = 500
# clock to count down to updating TFT
feather_clock = 30
while True:
try:
# read BME sensor
temp = int((bme680.temperature * 9/5) + (32 + temperature_offset))
humidity = int(bme680.relative_humidity)
pressure = int(bme680.pressure)
# log battery %
battery = battery_monitor.cell_percent
# map range of battery charge to rectangle size on screen
battery_display = round(simpleio.map_range(battery, 0, 100, 0, 22))
# update rectangle to reflect battery charge
rect.width = int(battery_display)
# if below 20%, change rectangle color to red
if battery_monitor.cell_percent < 20:
rect.color_index = 1
# when the azure clock runs out
if azure_clock > 500:
print("getting ntp date/time")
cal = ntp.datetime
year = cal[0]
mon = cal[1]
day = cal[2]
hour = cal[3]
minute = cal[4]
time.sleep(2)
print("getting msg")
# pack message
message = {"Temperature": temp,
"Humidity": humidity,
"Pressure": pressure,
"BatteryPercent": battery}
print("sending json")
device.send_telemetry(json.dumps(message))
print("data sent")
clock_view = "%s:%s" % (hour, minute)
if minute < 10:
clock_view = "%s:0%s" % (hour, minute)
print("updating time text")
time_text.text="\n".join(wrap_text_to_lines
("Data sent on %s/%s/%s at %s" % (mon,day,year,clock_view), 20))
# reset azure clock
azure_clock = 0
# when the feather clock runs out
if feather_clock > 30:
print("updating screen")
temp_text.text = "%0.1f° F" % temp
humid_text.text = "%0.1f %%" % humidity
press_text.text = "%0.2f" % pressure
# reset feather clock
feather_clock = 0
# if no clocks are running out
# increase counts by 1
else:
feather_clock += 1
azure_clock += 1
# ping azure
device.loop()
# if something disrupts the loop, reconnect
# pylint: disable=broad-except
except (ValueError, RuntimeError, OSError, ConnectionError) as e:
print("Network error, reconnecting\n", str(e))
supervisor.reload()
continue
# delay
time.sleep(1)
print(azure_clock)
Upload the Code and Libraries to the Feather ESP32-S2 TFT
After downloading the Project Bundle, plug your Feather ESP32-S2 TFT into the computer's USB port with a known good USB data+power cable. You should see a new flash drive appear in the computer's File Explorer or Finder (depending on your operating system) called CIRCUITPY. Unzip the folder and copy the following items to the Feather ESP32-S2 TFT's CIRCUITPY drive.
- lib folder
- roundedHeavy-26.bdf
- bmeTFT.bmp
- code.py
Your Feather ESP32-S2 TFT CIRCUITPY drive should look like this after copying the lib folder, the bmeTFT.bmp image file, the roundedHeavy-26.bdf bitmap font file, and the code.py file.
settings.toml
You will need to create and add a settings.toml file to your CIRCUITPY drive. Your settings.toml file will need to include the following information:
CIRCUITPY_WIFI_SSID="your-wifi-ssid" CIRCUITPY_WIFI_PASSWORD="your-wifi-password" id_scope="YOUR-AZURE-ID-SCOPE-HERE" device_id="YOUR-AZURE-DEVICE-ID-HERE" device_primary_key="YOUR-AZURE-DEVICE-PRIMARY-KEY-HERE"
You'll gather your ID scope, device ID and device primary key from your device connection groups page in your Azure application. Make sure to refer to the Connect Your Device page in this guide to see the process for accessing the keys.
The code begins by connecting to WiFi and grabbing the date and time using the adafruit_ntp library. The tz_offset can be set to your local timezone.
# Get WiFi details, ensure these are setup in settings.toml
ssid = getenv("CIRCUITPY_WIFI_SSID")
password = getenv("CIRCUITPY_WIFI_PASSWORD")
if None in [ssid, password]:
raise RuntimeError(
"WiFi settings are kept in settings.toml, "
"please add them there. The settings file must contain "
"'CIRCUITPY_WIFI_SSID', 'CIRCUITPY_WIFI_PASSWORD', "
"at a minimum."
)
print("Connecting to WiFi...")
wifi.radio.connect(ssid, password)
print("Connected to WiFi!")
# ntp clock - update tz_offset to your timezone
pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio)
ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio)
ntp = adafruit_ntp.NTP(pool, tz_offset=-4)
rtc.RTC().datetime = ntp.datetime
Next, a connection is established with Microsoft Azure.
esp = None
pool = socketpool.SocketPool(wifi.radio)
device = IoTCentralDevice(
pool, ssl_context, getenv("id_scope"), getenv("device_id"), getenv("device_primary_key")
)
print("Connecting to Azure IoT Central...")
device.connect()
print("Connected to Azure IoT Central!")
The BME688 sensor is setup, along with the onboard battery monitor. Variables are created to hold the values of the sensor and the battery monitor. An equation is used to convert the temperature to Fahrenheit.
i2c = board.I2C() # uses board.SCL and board.SDA bme680 = adafruit_bme680.Adafruit_BME680_I2C(i2c, debug=False) # change this to match the location's pressure (hPa) at sea level bme680.sea_level_pressure = 1013.25 temperature_offset = -5 # Create sensor object, using the board's default I2C bus. battery_monitor = LC709203F(board.I2C()) # Update to match the mAh of your battery for more accurate readings. # Can be MAH100, MAH200, MAH400, MAH500, MAH1000, MAH2000, MAH3000. # Choose the closest match. Include "PackSize." before it, as shown. battery_monitor.pack_size = PackSize.MAH2000 temp = int((bme680.temperature * 9/5) + (32 + temperature_offset)) humidity = int(bme680.relative_humidity) pressure = int(bme680.pressure) battery = battery_monitor.cell_percent
There are a series of displayio elements being used. The background bitmap has the vector icons for the environmental readings and the battery. Label text elements using a bitmap_font are added in the code to display the readings from the BME688.
A vectorio.Rectangle is using to show battery life since the size of the rectangle can be changed in the loop. It also uses a special Palette that includes green and red. Once the battery life goes below a certain threshold, the rectangle is shown as red.
display = board.DISPLAY
palette0 = displayio.Palette(2)
palette0[0] = 0x00FF00
palette0[1] = 0xFF0000
bitmap = displayio.OnDiskBitmap("/bmeTFT.bmp")
tile_grid = displayio.TileGrid(bitmap, pixel_shader=bitmap.pixel_shader)
group = displayio.Group()
group.append(tile_grid)
rect = vectorio.Rectangle(pixel_shader=palette0, width=22, height=10, x=12, y=116, color_index = 0)
group.append(rect)
font_file = "/roundedHeavy-26.bdf"
font = bitmap_font.load_font(font_file)
temp_text = bitmap_label.Label(font, text="%0.1f° F" % temp, color=0xFFFFFF)
temp_text.x = 20
temp_text.y = 80
humid_text = bitmap_label.Label(font, text="%0.1f %%" % humidity, color=0xFFFFFF)
humid_text.x = 95
humid_text.y = 80
press_text = bitmap_label.Label(font, text="%0.2f" % pressure, color=0xFFFFFF)
press_text.x = 170
press_text.y = 80
time_text = bitmap_label.Label(terminalio.FONT,
text="\n".join(wrap_text_to_lines("Data sent on %s/%s/%s at %s:%s" % (mon,day,year,hour,minute), 20)),
color=0xFFFFFF)
time_text.x = 125
time_text.y = 105
group.append(temp_text)
group.append(humid_text)
group.append(press_text)
group.append(time_text)
display.root_group = group
There are two variables setup for time keeping, azure_clock and feather_clock. The azure_clock counts down to sending data to Microsoft Azure and the feather_clock counts down to updating the TFT screen.
azure_clock = 750 feather_clock = 30
Once the azure_clock count is reached, the data is packed into a JSON message that is sent to Microsoft Azure. ntp.datetime is used to grab the date and time that the message is being sent. The time_text element is updated to display this timestamp. Finally, the azure_clock is reset to 0.
if azure_clock is 750:
print("getting msg")
message = {"Temperature": temp,
"Humidity": humidity,
"Pressure": pressure,
"BatteryPercent": battery}
print("sending json")
device.send_telemetry(json.dumps(message))
print("data sent")
print("getting ntp date/time")
cal = ntp.datetime
year = cal[0]
mon = cal[1]
day = cal[2]
hour = cal[3]
minute = cal[4]
time.sleep(2)
clock_view = "%s:%s" % (hour, minute)
if minute < 10:
clock_view = "%s:0%s" % (hour, minute)
print("updating time text")
time_text.text="\n".join(wrap_text_to_lines("Data sent on %s/%s/%s at %s" % (mon,day,year,clock_view), 20))
azure_clock = 0
Once the feather_clock count is reached, the TFT screen is updated with the current values from the BME688 and the battery_monitor. The feather_clock is then reset to 0.
if feather_clock is 30:
temp = int((bme680.temperature * 9/5) + (32 + temperature_offset))
humidity = int(bme680.relative_humidity)
pressure = int(bme680.pressure)
battery = battery_monitor.cell_percent
battery_display = round(simpleio.map_range(battery, 0, 100, 0, 22))
rect.width = int(battery_display)
if battery_monitor.cell_percent < 20:
rect.color_index = 1
print("updating screen")
temp_text.text = "%0.1f° F" % temp
humid_text.text = "%0.1f %%" % humidity
press_text.text = "%0.2f" % pressure
feather_clock = 0
Every second, the feather_clock and azure_clock counts are increased by 1. device.loop() is also called to poll for messages from Microsoft Azure.
feather_clock += 1 azure_clock += 1 device.loop() time.sleep(1)
If there is a connection error, a connection is reestablished with your WiFi network and Microsoft Azure by reloading the device.
except (ValueError, RuntimeError, OSError, ConnectionError) as e:
print("Network error, reconnecting\n", str(e))
supervisor.reload()
continue
Page last edited March 25, 2025
Text editor powered by tinymce.