Once you've finished setting up your Feather ESP32-S3 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 time import os import ssl import wifi import socketpool import board import digitalio import displayio import adafruit_requests from adafruit_bitmap_font import bitmap_font from adafruit_display_shapes.circle import Circle from adafruit_display_text import bitmap_label from adafruit_seesaw import seesaw, rotaryio, digitalio as seesaw_digitalio, neopixel from adafruit_ticks import ticks_ms, ticks_add, ticks_diff num_lights = 1 light = os.getenv('ELGATO_LIGHT') clock_clock = ticks_ms() clock_timer = 3 * 1000 i2c = board.I2C() # uses board.SCL and board.SDA seesaw = seesaw.Seesaw(i2c, addr=0x36) encoder = rotaryio.IncrementalEncoder(seesaw) seesaw.pin_mode(24, seesaw.INPUT_PULLUP) switch = seesaw_digitalio.DigitalIO(seesaw, 24) switch_state = False pixel = neopixel.NeoPixel(seesaw, 6, 1) pixel.brightness = 0.2 pixel.fill((255, 0, 0)) print() print("Connecting to WiFi") # connect to your SSID try: wifi.radio.connect(os.getenv('CIRCUITPY_WIFI_SSID'), os.getenv('CIRCUITPY_WIFI_PASSWORD')) # pylint: disable = broad-except except Exception: wifi.radio.connect(os.getenv('WIFI_SSID'), os.getenv('WIFI_PASSWORD')) print("Connected to WiFi") pixel.fill((0, 0, 255)) pool = socketpool.SocketPool(wifi.radio) requests = adafruit_requests.Session(pool, ssl.create_default_context()) # 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 group = displayio.Group() board.DISPLAY.root_group = 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) http_text = bitmap_label.Label(sm_font, text=" ") http_text.anchor_point = (1.0, 0.0) http_text.anchored_position = (board.DISPLAY.width, 0) group.append(http_text) status_text = bitmap_label.Label(sm_font, text=" ") status_text.anchor_point = (0.0, 0.5) status_text.anchored_position = (0, board.DISPLAY.height / 2) group.append(status_text) temp_text = bitmap_label.Label(lg_font, text=" K") temp_text.anchor_point = (1.0, 0.5) temp_text.anchored_position = (board.DISPLAY.width, board.DISPLAY.height / 2) group.append(temp_text) bright_text = bitmap_label.Label(lg_font, text=" %", x=board.DISPLAY.width//2, y=90) bright_text.anchor_point = (1.0, 1.0) bright_text.anchored_position = (board.DISPLAY.width, board.DISPLAY.height - 15) group.append(bright_text) onOff_circ = Circle(12, 12, 10, fill=None, stroke = 2, outline = 0xcccc00) group.append(onOff_circ) def kelvin_to_elgato(value): t = value * 0.05 t = max(min(344, int(t)), 143) return t def elgato_to_kelvin(value): t = value / 0.05 return t def ctrl_light(b, t, onOff): url = f"http://{light}:9123/elgato/lights" json = {"numberOfLights":num_lights,"lights":[{"on":onOff,"brightness":b,"temperature":t}]} print(f"PUTting data to {url}: {json}") status_text.text = "sending.." for i in range(5): try: pixel.fill((0, 255, 0)) r = requests.request(method="PUT", url=url, data=None, json=json, headers = {'Content-Type': 'application/json'}, timeout=10) print("-" * 40) print(r.status_code) # if PUT fails, try again if r.status_code != 200: status_text.text = "..sending.." pixel.fill((255, 255, 0)) time.sleep(2) r = requests.request(method="PUT", url=url, data=None, json=json, headers = {'Content-Type': 'application/json'}, timeout=10) if r.status_code != 200: pixel.fill((255, 0, 0)) except Exception: pixel.fill((255, 0, 0)) time.sleep(2) if i < 5 - 1: continue raise break status_text.text = "sent!" light_indicator(onOff) pixel.fill((255, 0, 255)) def read_light(): status_text.text = "reading.." for i in range(5): try: pixel.fill((0, 255, 0)) r = requests.get(f"http://{light}:9123/elgato/lights") j = r.json() if r.status_code != 200: status_text.text = "..reading.." pixel.fill((255, 255, 0)) time.sleep(2) r = requests.get(f"http://{light}:9123/elgato/lights") j = r.json() if r.status_code != 200: pixel.fill((255, 0, 0)) except Exception: pixel.fill((255, 0, 0)) time.sleep(2) if i < 5 - 1: continue raise break status_text.text = "read!" pixel.fill((255, 0, 255)) onOff = j['lights'][0]['on'] light_indicator(onOff) b = round(j['lights'][0]['brightness'] / 10) * 10 bright_text.text = f"{b}%" t = j['lights'][0]['temperature'] color_t = round(elgato_to_kelvin(t) / 100) * 100 temp_text.text = f"{color_t}K" return b, color_t, t, onOff def light_indicator(onOff): if onOff: onOff_circ.fill = 0xcccc00 else: onOff_circ.fill = None selected_light = 0 adjust_temp = True last_position = 0 http_text.text = f"{light}" # get on/off state of light on start-up try: brightness, color_temp, temp, light_on = read_light() except Exception: print() print("Could not find your Elgato light on the network..") print("Make sure it is powered on and that its IP address is correct in settings.toml.") print() raise while True: position = encoder.position # reset button state on release if not switch.value and switch_state: switch_state = False 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 if position != last_position: if position > last_position: if adjust_temp: color_temp = color_temp + 100 color_temp = max(min(7000, color_temp), 2900) temp_text.text = f"{color_temp}K" else: brightness = brightness + 10 brightness = max(min(100, brightness), 10) bright_text.text = f"{brightness}%" else: if adjust_temp: color_temp = color_temp - 100 color_temp = max(min(7000, color_temp), 2900) temp_text.text = f"{color_temp}K" else: brightness = brightness - 10 brightness = max(min(100, brightness), 10) bright_text.text = f"{brightness}%" temp = kelvin_to_elgato(color_temp) last_position = position # switch between adjusting color temp or brightness if switch.value and not switch_state: switch_state = True adjust_temp = not adjust_temp # toggle light on/off if not button0.value and not button0_state: button0_state = True light_on = not light_on ctrl_light(brightness, temp, light_on) clock_clock = ticks_add(clock_clock, clock_timer) # update brightness/temp if button1.value and not button1_state: button1_state = True light_on = True ctrl_light(brightness, temp, light_on) clock_clock = ticks_add(clock_clock, clock_timer) # check values if button2.value and not button2_state: button2_state = True brightness, color_temp, temp, light_on = read_light() clock_clock = ticks_add(clock_clock, clock_timer) if ticks_diff(ticks_ms(), clock_clock) >= clock_timer: status_text.text = "Connected" clock_clock = ticks_add(clock_clock, clock_timer)
Upload the Code and Libraries to the Feather ESP32-S3 Reverse TFT
After downloading the Project Bundle, plug your Feather ESP32-S3 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
- roundedHeavy-26.bdf
- roundedHeavy-46.bdf
Your Feather ESP32-S3 Reverse TFT CIRCUITPY drive should look like this after copying the lib folder, .bdf font files 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 light's IP address as ELGATO_LIGHT
. You'll also need to include your WiFi network SSID and password as CIRCUITPY_WIFI_SSID
and CIRCUITPY_WIFI_PASSWORD
.
CIRCUITPY_WIFI_SSID = "your-ssid-here" CIRCUITPY_WIFI_PASSWORD = "your-ssid-password-here" ELGATO_LIGHT = "your-Elgato-light-IP-address-here"
How the CircuitPython Code Works
At the top of the CircuitPython code are user definable variables. num_lights
is the number of lights you are using. The light
variable holds your Elgato light IP address that is loaded in from your settings.toml file. The clock_timer
is a non-blocking delay for use with ticks
for updating text on the display after requests are sent to the light.
num_lights = 1 light = os.getenv('ELGATO_LIGHT') clock_clock = ticks_ms() clock_timer = 3 * 1000
i2c = board.I2C() # uses board.SCL and board.SDA seesaw = seesaw.Seesaw(i2c, addr=0x36) encoder = rotaryio.IncrementalEncoder(seesaw) seesaw.pin_mode(24, seesaw.INPUT_PULLUP) switch = seesaw_digitalio.DigitalIO(seesaw, 24) switch_state = False pixel = neopixel.NeoPixel(seesaw, 6, 1) pixel.brightness = 0.2 pixel.fill((255, 0, 0))
WiFi and Buttons
The Feather connects to your WiFi SSID. Then, the three onboard buttons are set up as digital inputs.
# connect to your SSID wifi.radio.connect(os.getenv('CIRCUITPY_WIFI_SSID'), os.getenv('CIRCUITPY_WIFI_PASSWORD')) print("Connected to WiFi") pixel.fill((0, 0, 255)) pool = socketpool.SocketPool(wifi.radio) requests = adafruit_requests.Session(pool, ssl.create_default_context()) # 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
Display
Next up is all of the display code. Two different sized font files are used for displaying text on the TFT. The light's IP address and connection status is shown on the TFT in the smaller font. The brightness percentage and LED temperature are shown in the larger font. A small circle is used to show whether the light is on or off.
group = displayio.Group() board.DISPLAY.root_group = 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) http_text = bitmap_label.Label(sm_font, text=" ") http_text.anchor_point = (1.0, 0.0) http_text.anchored_position = (board.DISPLAY.width, 0) group.append(http_text) status_text = bitmap_label.Label(sm_font, text=" ") status_text.anchor_point = (0.0, 0.5) status_text.anchored_position = (0, board.DISPLAY.height / 2) group.append(status_text) temp_text = bitmap_label.Label(lg_font, text=" K") temp_text.anchor_point = (1.0, 0.5) temp_text.anchored_position = (board.DISPLAY.width, board.DISPLAY.height / 2) group.append(temp_text) bright_text = bitmap_label.Label(lg_font, text=" %", x=board.DISPLAY.width//2, y=90) bright_text.anchor_point = (1.0, 1.0) bright_text.anchored_position = (board.DISPLAY.width, board.DISPLAY.height - 15) group.append(bright_text) onOff_circ = Circle(12, 12, 10, fill=None, stroke = 2, outline = 0xcccc00) group.append(onOff_circ)
Functions
Converting Kelvin to Elgato and Back
A few functions are used throughout the loop. The first are kelvin_to_elgato()
and elgato_to_kelvin()
. This converts degrees Kelvin to the values used by the Elgato API to set the LED temperature and vice versa. The light can range in temperature from 2900K to 7000K and the Elgato API looks from values that range from 143
to 344
. This means that the degrees Kelvin value is multiplied by 0.05
to convert to the API value and the API value is divided by 0.05
to show a more familiar value in degrees Kelvin.
def kelvin_to_elgato(value): t = value * 0.05 t = max(min(344, int(t)), 143) return t def elgato_to_kelvin(value): t = value / 0.05 return t
Control the Light
The ctrl_light()
function sends HTTP requests to the Elgato light. The light can be turned on or off and its brightness and temperature can be set. There is an extensive try
/except
loop in the function to try and handle any errors that can pop-up when communicating with the light. Once the request is successfully sent, the status text on the TFT is updated to read "sent!
".
def ctrl_light(b, t, onOff): url = f"http://{light}:9123/elgato/lights" json = {"numberOfLights":num_lights,"lights":[{"on":onOff,"brightness":b,"temperature":t}]} print(f"PUTting data to {url}: {json}") status_text.text = "sending.." for i in range(5): try: pixel.fill((0, 255, 0)) r = requests.request(method="PUT", url=url, data=None, json=json, headers = {'Content-Type': 'application/json'}, timeout=10) print("-" * 40) print(r.status_code) # if PUT fails, try again if r.status_code != 200: status_text.text = "..sending.." pixel.fill((255, 255, 0)) time.sleep(2) r = requests.request(method="PUT", url=url, data=None, json=json, headers = {'Content-Type': 'application/json'}, timeout=10) if r.status_code != 200: pixel.fill((255, 0, 0)) except Exception: pixel.fill((255, 0, 0)) time.sleep(2) if i < 5 - 1: continue raise break status_text.text = "sent!" light_indicator(onOff) pixel.fill((255, 0, 255))
Read the Light
The read_light()
function makes an HTTP request to the light to read the current status of the light. It returns whether the light is on or off and the current brightness and temperature. This is handy if you are controlling the light with this project and the app so that they can stay in sync. The TFT updates to reflect any changed values.
def read_light(): status_text.text = "reading.." for i in range(5): try: pixel.fill((0, 255, 0)) r = requests.get(f"http://{light}:9123/elgato/lights") j = r.json() if r.status_code != 200: status_text.text = "..reading.." pixel.fill((255, 255, 0)) time.sleep(2) r = requests.get(f"http://{light}:9123/elgato/lights") j = r.json() if r.status_code != 200: pixel.fill((255, 0, 0)) except Exception: pixel.fill((255, 0, 0)) time.sleep(2) if i < 5 - 1: continue raise break status_text.text = "read!" pixel.fill((255, 0, 255)) onOff = j['lights'][0]['on'] light_indicator(onOff) b = round(j['lights'][0]['brightness'] / 10) * 10 bright_text.text = f"{b}%" t = j['lights'][0]['temperature'] color_t = round(elgato_to_kelvin(t) / 100) * 100 temp_text.text = f"{color_t}K" return b, color_t, t, onOff
Toggle the Circle
The last function is light_indicator()
. This function toggles the fill
of the Circle
object on the TFT depending on whether the light is on or off. If the light is off, the fill
of the Circle
is set to None
. Otherwise, it is set to yellow (0xCCCC00
).
def light_indicator(onOff): if onOff: onOff_circ.fill = 0xcccc00 else: onOff_circ.fill = None
Flight Check Request
Right before the loop, an initial call of the read_light()
function occurs. This allows the CircuitPython code to begin in sync with your light. The other purpose of this is to make sure your light is on and connected to your network. If the request fails, then an error message prints to the serial monitor prompting you to check your light and its IP address.
# get on/off state of light on start-up try: brightness, color_temp, temp, light_on = read_light() except Exception: print() print("Could not find your Elgato light on the network..") print("Make sure it is powered on and that its IP address is correct in settings.toml.") print() raise
The Loop
Finally, the loop. The rotary encoder controls setting the values for brightness and temperature. You can toggle between setting the two by pressing the switch in the encoder. The value of adjust_temp
is tracked for this purpose.
if position != last_position: if position > last_position: if adjust_temp: color_temp = color_temp + 100 color_temp = max(min(7000, color_temp), 2900) temp_text.text = f"{color_temp}K" else: brightness = brightness + 10 brightness = max(min(100, brightness), 10) bright_text.text = f"{brightness}%" else: if adjust_temp: color_temp = color_temp - 100 color_temp = max(min(7000, color_temp), 2900) temp_text.text = f"{color_temp}K" else: brightness = brightness - 10 brightness = max(min(100, brightness), 10) bright_text.text = f"{brightness}%" temp = kelvin_to_elgato(color_temp) last_position = position
Control the Light
The rotary encoder does not send this information to the light though, only the buttons control sending and receiving requests from the light. This avoids overwhelming the API. The first and second buttons send the ctrl_light()
function and send the brightness and temperature values. Only the first button toggles the light on or off though. If you want to keep the light in its current power state but update the brightness and temperature, you can press the second button.
# toggle light on/off if not button0.value and not button0_state: button0_state = True light_on = not light_on ctrl_light(brightness, temp, light_on) clock_clock = ticks_add(clock_clock, clock_timer) # update brightness/temp if button1.value and not button1_state: button1_state = True light_on = True ctrl_light(brightness, temp, light_on) clock_clock = ticks_add(clock_clock, clock_timer)
Read the Light
The third button uses the read_light()
function to fetch the current state of the light and its values. ticks
is used to update the status text on the TFT. After read_light()
and ctrl_light()
successfully communicate with the light, the text updates to "sent!
" or "read!
". After three seconds, it reverts back to "Connected
".
# check values if button2.value and not button2_state: button2_state = True brightness, color_temp, temp, light_on = read_light() clock_clock = ticks_add(clock_clock, clock_timer) if ticks_diff(ticks_ms(), clock_clock) >= clock_timer: status_text.text = "Connected" clock_clock = ticks_add(clock_clock, clock_timer)
Text editor powered by tinymce.