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 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_bme680 import adafruit_max1704x #from adafruit_lc709203f import LC709203F, PackSize # Get wifi details and more from a secrets.py file try: from secrets import secrets except ImportError: print("WiFi secrets are kept in secrets.py, please add them there!") raise print("Connecting to WiFi...") wifi.radio.connect(secrets["ssid"], secrets["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 secrets.py 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, secrets["id_scope"], secrets["device_id"], secrets["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.

secrets.py
You will need to create and add a secrets.py file to your CIRCUITPY drive. Your secrets.py file will need to include the following information:
secrets = { 'ssid' : 'YOUR-SSID-HERE', 'password' : 'YOUR-SSID-PASSWORD-HERE', '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 and more from a secrets.py file try: from secrets import secrets except ImportError: print("WiFi secrets are kept in secrets.py, please add them there!") raise print("Connecting to WiFi...") wifi.radio.connect(secrets["ssid"], secrets["password"]) print("Connected to WiFi!") pool = socketpool.SocketPool(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, esp, secrets["id_scope"], secrets["device_id"], secrets["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.
except (ValueError, RuntimeError, OSError) as e: print("Connection error, reconnecting\n", str(e)) wifi.radio.enabled = False wifi.radio.enabled = True wifi.radio.connect(secrets["ssid"], secrets["password"]) device.reconnect() continue
Page last edited January 17, 2025
Text editor powered by tinymce.