Coding the Sun Tracker
Once you've finished setting up your ESP32 Feather 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: 2026 Adafruit Industries
# SPDX-License-Identifier: MIT
"""
Outdoor Light Logger -- Adafruit ESP32-S2 Feather + VEML7700
Reads ambient light in lux, sends the value to an Adafruit IO feed,
then enters deep sleep to save battery. On wake the board resets
and the script runs again from the top.
Required hardware:
- Adafruit ESP32-S2 Feather
- Adafruit VEML7700 Lux Sensor (STEMMA QT / I2C)
Required libraries in the /lib folder:
- adafruit_veml7700.mpy
- adafruit_requests.mpy
- adafruit_connection_manager.mpy
- adafruit_io (folder)
- adafruit_minimqtt (folder)
Required entries in settings.toml:
CIRCUITPY_WIFI_SSID = "your-wifi-name"
CIRCUITPY_WIFI_PASSWORD = "your-wifi-password"
ADAFRUIT_AIO_USERNAME = "your-aio-username"
ADAFRUIT_AIO_KEY = "your-aio-key"
"""
import time
from os import getenv
import alarm
import board
import wifi
import adafruit_connection_manager
import adafruit_requests
import adafruit_veml7700
from adafruit_io.adafruit_io import IO_HTTP
# -- Settings --
SLEEP_INTERVAL = 300 # seconds between readings (5 minutes)
FEED_NAME = "ambient-light" # must match your Adafruit IO feed key
# -- Hardware setup (once, outside the loop) --
i2c = board.I2C()
veml = adafruit_veml7700.VEML7700(i2c)
time.sleep(0.5) # wait for first integration cycle to complete
while True:
try:
# -- Read the light sensor --
lux = veml.lux
print(f"Light: {lux:.1f} lux")
# -- Connect to WiFi and send to Adafruit IO --
if not wifi.radio.ipv4_address:
wifi.radio.connect(
getenv("CIRCUITPY_WIFI_SSID"),
getenv("CIRCUITPY_WIFI_PASSWORD"),
)
print(f"WiFi connected - IP: {wifi.radio.ipv4_address}")
pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio)
ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio)
requests = adafruit_requests.Session(pool, ssl_context)
io = IO_HTTP(
getenv("ADAFRUIT_AIO_USERNAME"),
getenv("ADAFRUIT_AIO_KEY"),
requests,
)
io.send_data(FEED_NAME, lux)
print("Sent to Adafruit IO!")
except Exception as e: # pylint: disable=broad-except
print(f"ERROR: {e}")
# -- Deep sleep (battery) or wait (USB) --
print(f"Sleeping {SLEEP_INTERVAL} seconds...")
time_alarm = alarm.time.TimeAlarm(
monotonic_time=time.monotonic() + SLEEP_INTERVAL
)
alarm.exit_and_deep_sleep_until_alarms(time_alarm)
# On battery: board resets, script runs from the top.
# On USB: pretend sleep returns here, loop continues.
Upload the code and libraries to your ESP32-S2 Feather
After downloading the Project Bundle, plug your ESP32-S2 Feather 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 ESP32-S2 Feather's CIRCUITPY drive.
- lib folder
- code.py
Your ESP32-S2 Feather CIRCUITPY drive should look like this after copying the lib folder and the code.py file.
Add Your settings.toml File
As of CircuitPython 8, there is support for Environment Variables. These Environmental 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 CIRCUITPY_WIFI_SSID, CIRCUITPY_WIFI_PASSWORD, aio_username and aio_key in the file.
CIRCUITPY_WIFI_SSID = "your-wifi-ssid-here" CIRCUITPY_WIFI_PASSWORD = "your-wifi-password-here" aio_username = "your-Adafruit-IO-username-here" aio_key = "your-Adafruit-IO-key-here"
Understanding the Code
At first glance, connecting to WiFi and beaming sensor data to the cloud might sound a bit intimidating, but CircuitPython makes it incredibly straightforward!
The code for the light logger follows a simple, repeatable routine:
- The microcontroller wakes up
- Grabs the current ambient light reading from the VEML7700 sensor
- Connects to your local network to push that data to your Adafruit IO dashboard
- Immediately goes into a deep sleep to conserve battery life
The sections below break down exactly what is happening in each section so you can see how all the pieces fit together.
import board import time import alarm import wifi import adafruit_connection_manager import adafruit_requests import adafruit_veml7700 from os import getenv from adafruit_io.adafruit_io import IO_HTTP
What this does: Microcontrollers don't know how to do everything right out of the box. The import statements tell the board to load specific "instruction manuals" (libraries) so it knows how to talk to your hardware and the internet.
-
board,time, andalarmhandle the physical pins on the board, time delays, and the deep-sleep functionality. -
wifi,adafruit_connection_manager,adafruit_requests, andIO_HTTPgive your board the ability to connect to your local Wi-Fi and send data securely to the Adafruit IO servers. -
adafruit_veml7700contains the specific instructions for reading the light sensor. -
getenvallows your code to securely read the WiFi passwords and API keys you stored in your settings.toml file.
# -- Settings -- SLEEP_INTERVAL = 300 # seconds between readings (5 minutes) FEED_NAME = "ambient-light" # must match your Adafruit IO feed key
What this does: a couple of variables are defined up top so they are easy to find and change without having to dig through the rest of the code.
-
SLEEP_INTERVALtells the board how long to snooze between readings to save battery. -
FEED_NAMEis the exact name of the digital "bucket" on Adafruit IO where you want to send your light data.
# -- Hardware setup (once, outside the loop) -- i2c = board.I2C() veml = adafruit_veml7700.VEML7700(i2c) time.sleep(0.5) # wait for first integration cycle to complete
What this does: Before starting to taking measurements, you have to introduce the sensor to the microcontroller.
-
board.I2C()sets up the communication pipeline (the STEMMA QT cable) between the Feather and the sensor. -
veml = adafruit_veml7700.VEML7700(i2c)creates a code object representing your physical sensor. -
A tiny half-second delay (
time.sleep(0.5)) is added to give the sensor a moment to wake up and take its very first light reading before we ask for the data.
while True:
try:
# -- Read the light sensor --
lux = veml.lux
print(f"Light: {lux:.1f} lux")
What this does:
-
while True:creates an infinite loop. Everything indented under this line will run over and over again. -
try:is the start of our error handling. We are telling the board, "Try to do the following steps, but if something goes wrong (like the WiFi dropping), don't completely crash." -
lux = veml.luxis where the magic happens! We ask the sensor for the current ambient light level and store it in a variable calledlux. We then print it out to the Serial console so you can see it working on your computer.
# -- Connect to WiFi and send to Adafruit IO --
if not wifi.radio.ipv4_address:
wifi.radio.connect(
getenv("CIRCUITPY_WIFI_SSID"),
getenv("CIRCUITPY_WIFI_PASSWORD"),
)
print(f"WiFi connected - IP: {wifi.radio.ipv4_address}")
pool = adafruit_connection_manager.get_radio_socketpool(wifi.radio)
ssl_context = adafruit_connection_manager.get_radio_ssl_context(wifi.radio)
requests = adafruit_requests.Session(pool, ssl_context)
io = IO_HTTP(
getenv("ADAFRUIT_AIO_USERNAME"),
getenv("ADAFRUIT_AIO_KEY"),
requests,
)
io.send_data(FEED_NAME, lux)
print("Sent to Adafruit IO!")
What this does: This chunk gets your data onto the internet.
-
First, it checks if you are already connected to WiFi. If not, it uses the credentials from your settings.toml file to log into your router.
-
Next, it sets up a
poolandssl_context. Think of this as opening a secure browser window so your board can talk to websites safely. -
Then, it logs into your specific Adafruit IO account using your username and secret key.
-
Finally,
io.send_data(FEED_NAME, lux)packages up your light reading and beams it directly into your Adafruit IO dashboard!
except Exception as e:
print(f"ERROR: {e}")
# -- Deep sleep (battery) or wait (USB) --
print(f"Sleeping {SLEEP_INTERVAL} seconds...")
time_alarm = alarm.time.TimeAlarm(
monotonic_time=time.monotonic() + SLEEP_INTERVAL
)
alarm.exit_and_deep_sleep_until_alarms(time_alarm)
# On battery: board resets, script runs from the top.
# On USB: pretend sleep returns here, loop continues.
What this does:
-
except Exception as e:pairs with thetry:from earlier. If the Wi-Fi disconnects or Adafruit IO is unreachable, it simply prints the error to the console instead of freezing the board. -
To make this project run for a long time on a battery, we use Deep Sleep. We set a
time_alarmfor 5 minutes (our 300-second interval). -
alarm.exit_and_deep_sleep_until_alarmsshuts down almost the entire microcontroller to sip the absolute minimum amount of power. -
When the 5 minutes are up, the board wakes up. If it's running on battery, waking from deep sleep acts like pressing the reset button: the code starts entirely over from line 1. (Note: If your board is plugged into your computer via USB, it skips the deep sleep and just waits 5 minutes before looping, so you don't lose your serial connection!).
Page last edited June 17, 2026
Text editor powered by tinymce.