Once you've finished setting up your Feather ESP32-S2 Reverse 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 to your computer as a zipped folder.
# SPDX-FileCopyrightText: 2024 Liz Clark for Adafruit Industries # SPDX-License-Identifier: MIT import ssl import os import socketpool import wifi import board import digitalio import displayio import vectorio from adafruit_bitmap_font import bitmap_font from adafruit_display_text import bitmap_label import adafruit_imageload from adafruit_io.adafruit_io import IO_HTTP, AdafruitIO_RequestError import adafruit_max1704x import adafruit_requests from simpleio import map_range from adafruit_ticks import ticks_ms, ticks_add, ticks_diff # states send_io = True bat_clock = ticks_ms() bat_timer = 60 * 1000 first_run = True # settings.toml imports aio_username = os.getenv('AIO_USERNAME') aio_key = os.getenv('AIO_KEY') # connect to wifi wifi.radio.connect(os.getenv('CIRCUITPY_WIFI_SSID'), os.getenv('CIRCUITPY_WIFI_PASSWORD')) pool = socketpool.SocketPool(wifi.radio) requests = adafruit_requests.Session(pool, ssl.create_default_context()) io = IO_HTTP(aio_username, aio_key, requests) try: # get feed battery_feed = io.get_feed("battery-monitor") except AdafruitIO_RequestError: # if no feed exists, create one battery_feed = io.create_new_feed("battery-monitor") # default group group = displayio.Group() # text only group textOnly_group = displayio.Group() board.DISPLAY.root_group = group # palette for vector graphics palette = displayio.Palette(5) palette[0] = 0xFF0000 palette[1] = 0xFFFF00 palette[2] = 0x00FF00 palette[3] = 0x0000FF palette[4] = 0x000000 # battery rectangle rect = vectorio.Rectangle(pixel_shader=palette, width=72, height=45, x=140, y=70, color_index = 0) group.append(rect) text_bg = vectorio.Rectangle(pixel_shader=palette, width=115, height=70, x=120, y=60, color_index = 4) # io indicator circle circle = vectorio.Circle(pixel_shader=palette, radius=8, x=10, y=10, color_index=3) textOnly_group.append(circle) # graphics bitmap bitmap, palette_bit = adafruit_imageload.load( "/bat_bg.bmp", bitmap=displayio.Bitmap, palette=displayio.Palette, ) # purple is made transparent palette_bit.make_transparent(0) tile_grid = displayio.TileGrid(bitmap, pixel_shader=palette_bit) group.append(tile_grid) group.append(circle) # font for graphics sm_file = "/roundedHeavy-26.bdf" sm_font = bitmap_font.load_font(sm_file) # font for text only lg_file = "/roundedHeavy-46.bdf" lg_font = bitmap_font.load_font(lg_file) volt_text = bitmap_label.Label(sm_font, text=" V", x=150, y=33) group.append(volt_text) big_volt_text = bitmap_label.Label(lg_font, text=" V") big_volt_text.anchor_point = (0.5, 0.0) big_volt_text.anchored_position = (board.DISPLAY.width / 2, 0) textOnly_group.append(big_volt_text) percent_text = bitmap_label.Label(sm_font, text=" %", x=150, y=90) big_percent_text = bitmap_label.Label(lg_font, text=" %", x=board.DISPLAY.width//2, y=90) big_percent_text.anchor_point = (0.5, 1.0) big_percent_text.anchored_position = (board.DISPLAY.width / 2, board.DISPLAY.height - 15) textOnly_group.append(big_percent_text) # buttons button0 = digitalio.DigitalInOut(board.D0) button0.direction = digitalio.Direction.INPUT button0.pull = digitalio.Pull.UP button0_state = False button1 = digitalio.DigitalInOut(board.D1) button1.direction = digitalio.Direction.INPUT button1.pull = digitalio.Pull.DOWN button1_state = False button2 = digitalio.DigitalInOut(board.D2) button2.direction = digitalio.Direction.INPUT button2.pull = digitalio.Pull.DOWN button2_state = False # MAX17048 instantiation monitor = adafruit_max1704x.MAX17048(board.I2C()) monitor.activity_threshold = 0.01 # colors for battery graphic def get_color(value): if value < 30: return 0 elif 30 <= value <= 75: return 1 else: return 2 while True: # reset button state on release if button0.value and button0_state: button0_state = False if not button1.value and button1_state: button1_state = False if not button2.value and button2_state: button2_state = False # toggle sending to adafruit io if not button0.value and not button0_state: button0_state = True send_io = not send_io if send_io: circle.color_index = 3 else: circle.color_index = 4 # toggle graphics or text only if button1.value and not button1_state: button1_state = True if board.DISPLAY.root_group == group: board.DISPLAY.root_group = textOnly_group else: board.DISPLAY.root_group = group # toggle battery graphic or % text if button2.value and not button2_state: button2_state = True if len(group) > 4: group.pop() group.pop() else: group.append(text_bg) group.append(percent_text) # read MAX17048 every 60 seconds if first_run or ticks_diff(ticks_ms(), bat_clock) >= bat_timer: first_run = False battery_volts = monitor.cell_voltage battery_percent = monitor.cell_percent print(f"Battery voltage: {battery_volts:.2f} Volts") print(f"Battery percentage: {battery_percent:.1f} %") print() battery_display = map_range(battery_percent, 0, 100, 0, 72) battery_x = map_range(battery_percent, 0, 100, 210, 140) # update rectangle to reflect battery charge rect.width = int(battery_display) rect.x = int(battery_x) rect.color_index = get_color(battery_percent) volt_text.text = f"{battery_volts:.2f} V" percent_text.text = f"{battery_percent:.1f} %" big_volt_text.text = f"{battery_volts:.2f} V" big_percent_text.text = f"{battery_percent:.1f} %" if battery_percent >= 100 and send_io: io.send_data(battery_feed["key"], battery_percent) bat_clock = ticks_add(bat_clock, bat_timer)
Upload the Code and Libraries to the Feather ESP32-S2 Reverse TFT
After downloading the Project Bundle, plug your Feather ESP32-S2 Reverse 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's CIRCUITPY drive:
- lib folder
- code.py
- bat_bg.bmp
- roundedHeavy-26.bdf
- roundedHeavy-46.bdf
Your Feather ESP32-S2 Reverse TFT CIRCUITPY drive should look like this after copying the lib folder, .bdf font files, .bmp image file and the code.py file.
Add Your settings.toml File
As of CircuitPython 8.0.0, there is support for Environment Variables. Environment variables are stored in a settings.toml file. Similar to secrets.py, the settings.toml file separates your sensitive information from your main code.py file. Add your settings.toml file as described in the Create Your settings.toml File page earlier in this guide. You'll need to include your WiFi network SSID and password as CIRCUITPY_WIFI_SSID
and CIRCUITPY_WIFI_PASSWORD
and your Adafruit IO username and key as AIO_USERNAME
and AIO_KEY
.
CIRCUITPY_WIFI_SSID = "YOUR-SSID-HERE" CIRCUITPY_WIFI_PASSWORD = "YOUR-SSID-PASSWORD-HERE" AIO_USERNAME = "YOUR-AIO-USERNAME-HERE" AIO_KEY = "YOUR-AIO-KEY-HERE"
How the CircuitPython Code Works
At the top of the code are some states that are used in the loop. send_io
enables sending the battery charging data to Adafruit IO. bat_timer
is the amount of time that the battery monitor is checked and first_run
and used to note that the code is running for the first time in the loop.
# states send_io = True bat_clock = ticks_ms() bat_timer = 60 * 1000 first_run = True
After the states are the settings.toml imports and WiFi and Adafruit IO connections.
# settings.toml imports aio_username = os.getenv('AIO_USERNAME') aio_key = os.getenv('AIO_KEY') # connect to wifi wifi.radio.connect(os.getenv('CIRCUITPY_WIFI_SSID'), os.getenv('CIRCUITPY_WIFI_PASSWORD')) pool = socketpool.SocketPool(wifi.radio) requests = adafruit_requests.Session(pool, ssl.create_default_context()) io = IO_HTTP(aio_username, aio_key, requests) try: # get feed battery_feed = io.get_feed("battery-monitor") except AdafruitIO_RequestError: # if no feed exists, create one battery_feed = io.create_new_feed("battery-monitor")
If your WiFi access point name or password are incorrect, an error will be generated.
Graphics
Next are the graphics. There are two display groups: one that includes graphical elements and one that is text only. You can switch between then in the loop depending on your preferences. the bat_bg bitmap file is loaded using the adafruit_image library and its background color is made transparent. vectorio shapes are used to show the battery charge graphic and if you are sending data to Adafruit IO.
# default group group = displayio.Group() # text only group textOnly_group = displayio.Group() board.DISPLAY.root_group = group # palette for vector graphics palette = displayio.Palette(5) palette[0] = 0xFF0000 palette[1] = 0xFFFF00 palette[2] = 0x00FF00 palette[3] = 0x0000FF palette[4] = 0x000000 # battery rectangle rect = vectorio.Rectangle(pixel_shader=palette, width=72, height=45, x=140, y=70, color_index = 0) group.append(rect) text_bg = vectorio.Rectangle(pixel_shader=palette, width=115, height=70, x=120, y=60, color_index = 4) # io indicator circle circle = vectorio.Circle(pixel_shader=palette, radius=8, x=10, y=10, color_index=3) textOnly_group.append(circle) # graphics bitmap bitmap, palette_bit = adafruit_imageload.load( "/bat_bg.bmp", bitmap=displayio.Bitmap, palette=displayio.Palette, ) # purple is made transparent palette_bit.make_transparent(0) tile_grid = displayio.TileGrid(bitmap, pixel_shader=palette_bit) group.append(tile_grid) group.append(circle)
Then all of the text attributes are created. There are two font files, one for a smaller font and one for a larger font. The larger font is used for the text only graphics group.
# font for graphics sm_file = "/roundedHeavy-26.bdf" sm_font = bitmap_font.load_font(sm_file) # font for text only lg_file = "/roundedHeavy-46.bdf" lg_font = bitmap_font.load_font(lg_file) volt_text = bitmap_label.Label(sm_font, text=" V", x=150, y=33) group.append(volt_text) big_volt_text = bitmap_label.Label(lg_font, text=" V") big_volt_text.anchor_point = (0.5, 0.0) big_volt_text.anchored_position = (board.DISPLAY.width / 2, 0) textOnly_group.append(big_volt_text) percent_text = bitmap_label.Label(sm_font, text=" %", x=150, y=90) big_percent_text = bitmap_label.Label(lg_font, text=" %", x=board.DISPLAY.width//2, y=90) big_percent_text.anchor_point = (0.5, 1.0) big_percent_text.anchored_position = (board.DISPLAY.width / 2, board.DISPLAY.height - 15) textOnly_group.append(big_percent_text)
Buttons and I2C
The three buttons on the Feather are setup as digitalio inputs. The onboard MAX17048 is instantiated over I2C.
# buttons button0 = digitalio.DigitalInOut(board.D0) button0.direction = digitalio.Direction.INPUT button0.pull = digitalio.Pull.UP button0_state = False button1 = digitalio.DigitalInOut(board.D1) button1.direction = digitalio.Direction.INPUT button1.pull = digitalio.Pull.DOWN button1_state = False button2 = digitalio.DigitalInOut(board.D2) button2.direction = digitalio.Direction.INPUT button2.pull = digitalio.Pull.DOWN button2_state = False # MAX17048 instantiation monitor = adafruit_max1704x.MAX17048(board.I2C()) monitor.activity_threshold = 0.01
Colors
There is a simple function called get_color()
that maps the charge percentage of the battery to a color in a palette. This changes the color of the battery from red to yellow to green as it charges up.
# colors for battery graphic def get_color(value): if value < 30: return 0 elif 30 <= value <= 75: return 1 else: return 2
The Loop
In the loop, the buttons control the UI appearance and code functionality. Button D0 toggles sending the battery info to Adafruit IO. Button D1 changes between the text only display group and the graphical group. Button D2 changes between representing the battery charge percentage in text or via the battery graphic.
# toggle sending to adafruit io if not button0.value and not button0_state: button0_state = True send_io = not send_io if send_io: circle.color_index = 3 else: circle.color_index = 4 # toggle graphics or text only if button1.value and not button1_state: button1_state = True if board.DISPLAY.root_group == group: board.DISPLAY.root_group = textOnly_group else: board.DISPLAY.root_group = group # toggle battery graphic or % text if button2.value and not button2_state: button2_state = True if len(group) > 4: group.pop() group.pop() else: group.append(text_bg) group.append(percent_text)
Monitor the Battery
Every minute the MAX17048 is read to check on the battery voltage and charge percentage. The graphics are updated with the data to show on the TFT. If you chose to send the data to IO, the charge percentage is sent to your feed when the battery is fully charged.
# read MAX17048 every 60 seconds if first_run or ticks_diff(ticks_ms(), bat_clock) >= bat_timer: first_run = False battery_volts = monitor.cell_voltage battery_percent = monitor.cell_percent print(f"Battery voltage: {battery_volts:.2f} Volts") print(f"Battery percentage: {battery_percent:.1f} %") print() battery_display = map_range(battery_percent, 0, 100, 0, 72) battery_x = map_range(battery_percent, 0, 100, 210, 140) # update rectangle to reflect battery charge rect.width = int(battery_display) rect.x = int(battery_x) rect.color_index = get_color(battery_percent) volt_text.text = f"{battery_volts:.2f} V" percent_text.text = f"{battery_percent:.1f} %" big_volt_text.text = f"{battery_volts:.2f} V" big_percent_text.text = f"{battery_percent:.1f} %" if battery_percent >= 100 and send_io: io.send_data(battery_feed["key"], battery_percent) bat_clock = ticks_add(bat_clock, bat_timer)
Text editor powered by tinymce.