This guide was motivated by a really cool project we saw go by online. The idea is to create an alarm clock for kids that uses colors instead of an actual clock face. A night time color means "keep sleeping" and a sunrise color means "wake up! time to play!". Great for youngsters who are not quite ready to read a clock (or struggle to in the morning)

Here's a link to the original project which is based on a Raspberry Pi:

The origami used to make the lamp structure is amazing!

The lamp is really nothing more than: (1) grab current time from internet, (2) wait for wake up time, (3) and then do something with NeoPixels. Can an Internet connected Raspberry Pi running a full linux operating system do this? Yes. Easily. Without even breaking a sweat. Might even be overkill to use a Pi. But when a wifi capable full linux compy can be had for $10, why not?

RPI Dark Times

Throwing a $10 linux compy at a trivial task is fine, assuming said hardware is actually available. However, Raspberry Pi's are currently very difficult to come by.

There's an unfortunate combination of limited supply with ongoing high demand. Bot driven resellers are buying up stock and reselling them for crazy marked-up prices. Resellers (like Adafruit) are trying to thwart these efforts with various purchasing limits and requirements. For people that just want to make fun Pi based projects, all this just leads to anguish and heart ache trying to get hands on a Pi. The original tweet that caught our eye bemoans this as well:

Remember when Pi's were given away free on the cover of magazines? Ah...good memories from the before times.

Other Options

But wait! There is hope!

A Raspberry Pi is not the only option for "connecting to the Internet" and doing stuff. There are simpler microcontrollers, like the various ESP chips, that can do this. Sure, they won't be pulling down terabytes of data and running numpy. But they can do relatively simple things - like grab time from the Internet. And they can also readily drive NeoPixels.

In this guide we show how to recreate the Sunrise Lamp project using an ESP32-S2 based board. There are numerous ESP32-S2 options, but to keep things extra smol, we use a QT Py.

There's one with a built in antenna:

Angled shot of small square purple dev board.
What has your favorite Espressif WiFi microcontroller, comes with our favorite connector - the STEMMA QT, a chainable I2C port, and has...
$12.50
In Stock

Or with a uFL connector for attaching an external antenna:

Angled shot of purple square-shaped microcontroller with a uFL antenna attached.
What has your favorite Espressif WiFi microcontroller, comes with our favorite connector - the STEMMA QT, a chainable I2C port, and has...
$12.50
In Stock
2.4GHz Mini Flexible WiFi Antenna with uFL Connector
This 4" / 100mm long flexible uFL 2.4GHz antenna has approx 4DBi gain and a 50Ω impedance so it will work fantastically with just about any 2.4-2.5GHz...
$2.50
In Stock

Just like the original project, we use a 12 NeoPixel ring:

Hand holding NeoPixel Ring with 12  x 5050 RGB LED, lit up rainbow
Round and round and round they go! 12 ultra bright smart LED NeoPixels are arranged in a circle with 1.5" (37mm) outer diameter. The rings are 'chainable' - connect the...
$7.50
In Stock

But any NeoPixels could be used.

CircuitPython is a derivative of MicroPython designed to simplify experimentation and education on low-cost microcontrollers. It makes it easier than ever to get prototyping by requiring no upfront desktop software downloads. Simply copy and edit files on the CIRCUITPY drive to iterate.

CircuitPython Quickstart

Follow this step-by-step to quickly get CircuitPython running on your board.

Click the link above to download the latest CircuitPython UF2 file.

Save it wherever is convenient for you.

Plug your board into your computer, using a known-good data-sync cable, directly, or via an adapter if needed.

Click the reset button once (highlighted in red above), and then click it again when you see the RGB status LED(s) (highlighted in green above) turn red (approximately half a second later). Sometimes it helps to think of it as a "slow double-click" of the reset button.

For this board, tap reset and wait for the LED to turn purple, and as soon as it turns purple, tap reset again. The second tap needs to happen while the LED is still purple.

Once successful, you will see the RGB status LED(s) turn green (highlighted in green above). If you see red, try another port, or if you're using an adapter or hub, try without the hub, or different adapter or hub.

If double-clicking doesn't work the first time, try again. Sometimes it can take a few tries to get the rhythm right!

A lot of people end up using charge-only USB cables and it is very frustrating! Make sure you have a USB cable you know is good for data sync.

If after several tries, and verifying your USB cable is data-ready, you still cannot get to the bootloader, it is possible that the bootloader is missing or damaged. Check out the Install UF2 Bootloader page for details on resolving this issue.

You will see a new disk drive appear called QTPYS2BOOT.

 

 

Drag the adafruit_circuitpython_etc.uf2 file to QTPYS2BOOT.

The BOOT drive will disappear and a new disk drive called CIRCUITPY will appear.

That's it!

One of the great things about the ESP32 is the built-in WiFi capabilities. This page covers the basics of getting connected using CircuitPython.

The first thing you need to do is update your code.py to the following. Click the Download Project Bundle button below to download the necessary libraries and the code.py file in a zip file. Extract the contents of the zip file, and copy the entire lib folder and the code.py file to your CIRCUITPY drive.

# SPDX-FileCopyrightText: 2020 Brent Rubell for Adafruit Industries
#
# SPDX-License-Identifier: MIT

import ipaddress
import ssl
import wifi
import socketpool
import adafruit_requests

# URLs to fetch from
TEXT_URL = "http://wifitest.adafruit.com/testwifi/index.html"
JSON_QUOTES_URL = "https://www.adafruit.com/api/quotes.php"
JSON_STARS_URL = "https://api.github.com/repos/adafruit/circuitpython"

# 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("ESP32-S2 WebClient Test")

print("My MAC addr:", [hex(i) for i in wifi.radio.mac_address])

print("Available WiFi networks:")
for network in wifi.radio.start_scanning_networks():
    print("\t%s\t\tRSSI: %d\tChannel: %d" % (str(network.ssid, "utf-8"),
            network.rssi, network.channel))
wifi.radio.stop_scanning_networks()

print("Connecting to %s"%secrets["ssid"])
wifi.radio.connect(secrets["ssid"], secrets["password"])
print("Connected to %s!"%secrets["ssid"])
print("My IP address is", wifi.radio.ipv4_address)

ipv4 = ipaddress.ip_address("8.8.4.4")
print("Ping google.com: %f ms" % (wifi.radio.ping(ipv4)*1000))

pool = socketpool.SocketPool(wifi.radio)
requests = adafruit_requests.Session(pool, ssl.create_default_context())

print("Fetching text from", TEXT_URL)
response = requests.get(TEXT_URL)
print("-" * 40)
print(response.text)
print("-" * 40)

print("Fetching json from", JSON_QUOTES_URL)
response = requests.get(JSON_QUOTES_URL)
print("-" * 40)
print(response.json())
print("-" * 40)

print()

print("Fetching and parsing json from", JSON_STARS_URL)
response = requests.get(JSON_STARS_URL)
print("-" * 40)
print("CircuitPython GitHub Stars", response.json()["stargazers_count"])
print("-" * 40)

print("done")

Your CIRCUITPY drive should resemble the following.

CIRCUITPY

To get connected, the next thing you need to do is update the secrets.py file.

Secrets File

We expect people to share tons of projects as they build CircuitPython WiFi widgets. What we want to avoid is people accidentally sharing their passwords or secret tokens and API keys. So, we designed all our examples to use a secrets.py file, that is on your CIRCUITPY drive, to hold secret/private/custom data. That way you can share your main project without worrying about accidentally sharing private stuff.

The initial secrets.py file on your CIRCUITPY drive should look like this:

# SPDX-FileCopyrightText: 2020 Adafruit Industries
#
# SPDX-License-Identifier: Unlicense

# This file is where you keep secret settings, passwords, and tokens!
# If you put them in the code you risk committing that info or sharing it

secrets = {
    'ssid' : 'home_wifi_network',
    'password' : 'wifi_password',
    'aio_username' : 'my_adafruit_io_username',
    'aio_key' : 'my_adafruit_io_key',
    'timezone' : "America/New_York", # http://worldtimeapi.org/timezones
    }

Inside is a Python dictionary named secrets with a line for each entry. Each entry has an entry name (say 'ssid') and then a colon to separate it from the entry key ('home_wifi_network') and finally a comma (,).

At a minimum you'll need to adjust the ssid and password for your local WiFi setup so do that now!

As you make projects you may need more tokens and keys, just add them one line at a time. See for example other tokens such as one for accessing GitHub or the Hackaday API. Other non-secret data like your timezone can also go here, just cause its called secrets doesn't mean you can't have general customization data in there!

For the correct time zone string, look at http://worldtimeapi.org/timezones and remember that if your city is not listed, look for a city in the same time zone, for example Boston, New York, Philadelphia, Washington DC, and Miami are all on the same time as New York.

Of course, don't share your secrets.py - keep that out of GitHub, Discord or other project-sharing sites.

Don't share your secrets.py file, it has your passwords and API keys in it!

If you connect to the serial console, you should see something like the following:

In order, the example code...

Checks the ESP32's MAC address.

print("My MAC addr:", [hex(i) for i in wifi.radio.mac_address])

Performs a scan of all access points and prints out the access point's name (SSID), signal strength (RSSI), and channel.

print("Avaliable WiFi networks:")
for network in wifi.radio.start_scanning_networks():
    print("\t%s\t\tRSSI: %d\tChannel: %d" % (str(network.ssid, "utf-8"),
            network.rssi, network.channel))
wifi.radio.stop_scanning_networks()

Connects to the access point you defined in the secrets.py file, prints out its local IP address, and attempts to ping google.com to check its network connectivity. 

print("Connecting to %s"%secrets["ssid"])
wifi.radio.connect(secrets["ssid"], secrets["password"])
print(print("Connected to %s!"%secrets["ssid"]))
print("My IP address is", wifi.radio.ipv4_address)

ipv4 = ipaddress.ip_address("8.8.4.4")
print("Ping google.com: %f ms" % wifi.radio.ping(ipv4))

The code creates a socketpool using the wifi radio's available sockets. This is performed so we don't need to re-use sockets. Then, it initializes a a new instance of the requests interface - which makes getting data from the internet really really easy.

pool = socketpool.SocketPool(wifi.radio)
requests = adafruit_requests.Session(pool, ssl.create_default_context())

To read in plain-text from a web URL, call requests.get - you may pass in either a http, or a https url for SSL connectivity. 

print("Fetching text from", TEXT_URL)
response = requests.get(TEXT_URL)
print("-" * 40)
print(response.text)
print("-" * 40)

Requests can also display a JSON-formatted response from a web URL using a call to requests.get

print("Fetching json from", JSON_QUOTES_URL)
response = requests.get(JSON_QUOTES_URL)
print("-" * 40)
print(response.json())
print("-" * 40)

Finally, you can fetch and parse a JSON URL using requests.get. This code snippet obtains the stargazers_count field from a call to the GitHub API.

print("Fetching and parsing json from", JSON_STARS_URL)
response = requests.get(JSON_STARS_URL)
print("-" * 40)
print("CircuitPython GitHub Stars", response.json()["stargazers_count"])
print("-" * 40)

OK you now have your ESP32 board set up with a proper secrets.py file and can connect over the Internet. If not, check that your secrets.py file has the right ssid and password and retrace your steps until you get the Internet connectivity working!

Be creative here! Other NeoPixel products could be used - like strips. And the lamp housing can be anything fun. We document what we used just to provide the info.

Wiring

To wire up the NeoPixels:

  • QT Py 5V to NeoPixel PWR
  • QT Py GND to NeoPixel GND
  • QT Py SCK* to NeoPixel IN

* or any available GPIO pin

Lamp Assembly

Honestly, we were hoping to find something a little more fun than a generic dome. Anything with good neutral diffusion would work, like a plastic dinosaur! But when we went looking, the thrift store gods only offered up this 50 cent battery powered closet light. It'll work to demonstrate the general idea though, which is to basically just shove everything into whatever is used for the lamp.

Four screws were removed to allow removing the bottom plate (black plastic).

The NeoPixel ring was hot glued on the inside of the bottom plate in the center.

The original lamp socket can be seen in the center of the NeoPixel ring.

Then the QT Py ESP32-S2 was wired to the NeoPixels.

These could be direct wired. The wiring harness shown here is not necessary.

And then the top was put back on. Done!

For power, a USB cable was fed through the open battery compartment and connected to the QT Py ESP32-S2's USB C connector.

There are a couple of ways to grab time from the Internet. First we'll show how this can be done using Network Time Protocol (NTP). This has the benefit of not requiring any accounts or logins - just grab the time from an NTP server. There's even an NTP CircuitPython library that can be used.

However, the offset to local time must be manually set and adjusted for daylight savings (if you are affected)

To get the NTP code, click on the Download Project Bundle button in the window below. It will download as a zipped folder.

# SPDX-FileCopyrightText: 2022 Carter Nelson for Adafruit Industries
#
# SPDX-License-Identifier: MIT

import time
import board
import rtc
import socketpool
import wifi
import adafruit_ntp
import neopixel

# --| User Config |--------------------------------
TZ_OFFSET = -7  # time zone offset in hours from UTC
WAKE_UP_HOUR = 7  # alarm time hour (24hr)
WAKE_UP_MIN = 30  # alarm time minute
SLEEP_COLOR = (0, 25, 150)  # sleepy time color as tuple
WAKEUP_COLOR = (255, 100, 0)  # wake up color as tuple
FADE_STEPS = 100  # wake up fade animation steps
FADE_DELAY = 0.1  # wake up fade animation speed
NEO_PIN = board.SCK  # neopixel pin
NEO_CNT = 12  # neopixel count
# -------------------------------------------------

# Set up NeoPixels
pixels = neopixel.NeoPixel(NEO_PIN, NEO_CNT)

# 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

# Connect to local network
try:
    wifi.radio.connect(secrets["ssid"], secrets["password"])
except ConnectionError:
    print("Wifi failed to connect.")
    while True:
        pixels.fill(0)
        time.sleep(0.5)
        pixels.fill(0x220000)
        time.sleep(0.5)

print("Wifi connected.")

# Get current time using NTP
print("Fetching time from NTP.")
pool = socketpool.SocketPool(wifi.radio)
ntp = adafruit_ntp.NTP(pool, tz_offset=TZ_OFFSET)
rtc.RTC().datetime = ntp.datetime

# Fill with sleepy time colors
pixels.fill(SLEEP_COLOR)

# Wait for wake up time
now = time.localtime()
print("Current time: {:2}:{:02}".format(now.tm_hour, now.tm_min))
print("Waiting for alarm {:2}:{:02}".format(WAKE_UP_HOUR, WAKE_UP_MIN))
while not (now.tm_hour == WAKE_UP_HOUR and now.tm_min == WAKE_UP_MIN):
    # just sleep until next time check
    time.sleep(30)
    now = time.localtime()

# Sunrise animation
print("Waking up!")
r1, g1, b1 = SLEEP_COLOR
r2, g2, b2 = WAKEUP_COLOR
dr = (r2 - r1) / FADE_STEPS
dg = (g2 - g1) / FADE_STEPS
db = (b2 - b1) / FADE_STEPS

for _ in range(FADE_STEPS):
    r1 += dr
    g1 += dg
    b1 += db
    pixels.fill((int(r1), int(g1), int(b1)))
    time.sleep(FADE_DELAY)

print("Done.")

Open the zip file and copy code.py to your CIRCUITPY folder. Also copy the contents of the lib folder to the CIRCUITPY/lib folder.

Now checkout the Using the Alarm section for how to configure things.

Using Adafruit IO makes things even easier for getting local time. All the time zone, day light saving time, etc. details are taken care of by the AIO server. This does require setting up an AIO account. However, the time query service is available with even the free level account.

Add AIO info to secrets.py

In addition to the wifi information that was previously added to the secrets.py file, be sure it also includes your AIO username and API key. These should be added in the secrets dictionary as aio_username and aio_key.

Checkout this Learn Guide page for more information:

Then, to get the AIO based code, click on the Download Project Bundle button in the window below. It will download as a zipped folder.

# SPDX-FileCopyrightText: 2022 Carter Nelson for Adafruit Industries
#
# SPDX-License-Identifier: MIT

import time
import ssl
import board
import rtc
import wifi
import socketpool
import adafruit_requests
import neopixel

# --| User Config |--------------------------------
WAKE_UP_HOUR = 7  # alarm time hour (24hr)
WAKE_UP_MIN = 30  # alarm time minute
SLEEP_COLOR = (0, 25, 150)  # sleepy time color as tuple
WAKEUP_COLOR = (255, 100, 0)  # wake up color as tuple
FADE_STEPS = 100  # wake up fade animation steps
FADE_DELAY = 0.1  # wake up fade animation speed
NEO_PIN = board.SCK  # neopixel pin
NEO_CNT = 12  # neopixel count
# -------------------------------------------------

# Set up NeoPixels
pixels = neopixel.NeoPixel(NEO_PIN, NEO_CNT)

# 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

# Setup AIO time query URL
TIME_URL = "https://io.adafruit.com/api/v2/"
TIME_URL += secrets["aio_username"]
TIME_URL += "/integrations/time/strftime?x-aio-key="
TIME_URL += secrets["aio_key"]
TIME_URL += "&fmt=%25Y%3A%25m%3A%25d%3A%25H%3A%25M%3A%25S"

# Connect to local network
try:
    wifi.radio.connect(secrets["ssid"], secrets["password"])
except ConnectionError:
    print("Wifi failed to connect.")
    while True:
        pixels.fill(0)
        time.sleep(0.5)
        pixels.fill(0x220000)
        time.sleep(0.5)

print("Wifi connected.")

# Get current time using AIO
print("Fetching time.")
pool = socketpool.SocketPool(wifi.radio)
requests = adafruit_requests.Session(pool, ssl.create_default_context())
response = [int(x) for x in requests.get(TIME_URL).text.split(":")]
response += [0, 0, -1]
rtc.RTC().datetime = time.struct_time(response)

# Fill with sleepy time colors
pixels.fill(SLEEP_COLOR)

# Wait for wake up time
now = time.localtime()
print("Current time: {}:{}".format(now.tm_hour, now.tm_min))
print("Waiting for alarm {}:{}".format(WAKE_UP_HOUR, WAKE_UP_MIN))
while not (now.tm_hour == WAKE_UP_HOUR and now.tm_min == WAKE_UP_MIN):
    # just sleep until next time check
    time.sleep(30)
    now = time.localtime()

# Sunrise animation
print("Waking up!")
r1, g1, b1 = SLEEP_COLOR
r2, g2, b2 = WAKEUP_COLOR
dr = (r2 - r1) / FADE_STEPS
dg = (g2 - g1) / FADE_STEPS
db = (b2 - b1) / FADE_STEPS

for _ in range(FADE_STEPS):
    r1 += dr
    g1 += dg
    b1 += db
    pixels.fill((int(r1), int(g1), int(b1)))
    time.sleep(FADE_DELAY)

print("Done.")

Open the zip file and copy code.py to your CIRCUITPY folder. Also copy the contents of the lib folder to the CIRCUITPY/lib folder.

Now checkout the Using the Alarm section for how to configure things.

Both versions of the code work in essentially the same way. The only difference is in how they grab time from the Internet.

Connect the QT Py to a PC with a good data capable USB cable so that the CIRCUITPY folder shows up. Then open up code.py in a text editor.

Look for these lines near the top of the code listing:

# --| User Config |--------------------------------
WAKE_UP_HOUR = 7  # alarm time hour (24hr)
WAKE_UP_MIN = 30  # alarm time minute
SLEEP_COLOR = (0, 25, 150)  # sleeping time color as tuple
WAKEUP_COLOR = (255, 100, 0)  # wake up color as tuple
FADE_STEPS = 100  # wake up fade animation steps
FADE_DELAY = 0.1  # wake up fade animation speed
NEO_PIN = board.SCK  # neopixel pin
NEO_CNT = 12  # neopixel count
# -------------------------------------------------

To set the alarm time, change WAKE_UP_HOUR and WAKE_UP_MIN as desired. Note that the hour uses 24 hour format.

While waiting for the alarm time, the SLEEP_COLOR will be shown on the NeoPixels. Once the alarm time is reached, the color will fade to WAKEUP_COLOR. Use (red, green, blue) tuples to change either of these colors.

The speed of the fade effect can be altered by changing FADE_STEPS and FADE_DELAY.

If you end up using different NeoPixels or attach them to a different pin, change NEO_PIN and NEO_CNT as needed.

NTP Version Only

For the NTP version of the alarm, there is also a TZ_OFFSET parameter. Use that to set your local time in terms of hourly offset from UTC.

Arming and Resetting

Once the alarm time is set, simply reset the board or cycle power. The board will fetch the current time and then wait for the alarm time.

Once the alarm triggers, it will stay lit. To re-arm the alarm, simply power cycle the board. Pressing the reset button would also work, if the reset button is reachable.

Troubleshooting

If the board fails to connect to the local wifi network, the NeoPixels should blink red. If that happens, double check the ssid and password settings in your secrets.py file.

For other issues, connecting to the serial console is the best option. There are several print statements in the code. Being able to see these, and any other error messages, will help determine where and why things are not working. For details on connecting to the serial console, see here:

This guide was first published on Jul 12, 2022. It was last updated on Jul 12, 2022.