I think I’m calling this one done. It had quite the journey. The goal — build a LED light for this terrarium. Looks like mission accomplished ‘eh?
Of course, I over-engineering this sucker. The NeoPixel strip is controlled by a Raspberry Pi Pico W running Circuit Python. Lots of potential there.
Then the design to 3d print the ring to hold the lights, that was an interesting exercise in math, angles, and modification as the hand-made nature of the terrarium ensured that angles and measurements were all slightly different.
The power / PCB box was reasonably straightforward though.
Currently, it powers on, and the LEDs light a pleasing warm-white light. An external powerbar is on a timer which energizes everything.
… but the journey to get here was interesting in its own right…
Enter the AI
Since I’m still a relative noob, at Python, and Circuit Python is a specialized hybrid of Micro Python (which itself is a hybrid of Python for microcontrollers) is new to me, I thought I’d take the opportunity to use ChatGPT as an assistant.
The workflow was something like this:
- Define the problem as a question for the AI
- Test the solution provided by the AI
- Debug, as it most likely didn’t work out of the gate
The last point is where I personally got the most value. What was the code supposed to do? Why did it fail? How did we fix it?
Feeping Creaturism
Of course, as things started to work and the light was actually working, more questions popped up and the AI was happy to attempt to provide solutions. This was a Raspberry Pi Pico W after all, we have a Network! So here’s what we tried…
- Connect to my home network — because why not? Set this little Pi free!
- Figure out the time of day so we can turn on and off at a logical time? Of course! A simple matter to connect to a NTP server. On power up and every 30 mins — got to stay accurate for the plants ‘eh?
- Provide feedback loops in the form of coloured lights and blinks to indicate status through the boot-up and time sync phases.
- At sunrise / sunset, turn on / off accordingly. This was tricky. NTP didn’t provide sunrise / set times, so I picked a long day (June 21st), and asked ChatGPT to determine sunrise / set times based on my location.
- Change colour of the LEDs based on time of day. Ok, this one was very over-the-top. Eventually it resolved to the following:
- Break the duration of daylight (sunrise to sunset) into 30 minute chunks
- Every 30 minutes, set the LED RGB value and brightness level appropriate for that time of day. This is to simulate the colour temperature of the light throughout the day.
That last point was the most interesting actually. Colour temperature of the light varies quite a lot throughout the day. Sunrise / sunset are warmer (more orange) than midday (more blue). Yet, ChatGPT returned values that seemed reasonable during the course of the day.
Yeah, it was overkill but because I could, I did. Here’s the code that did the work.
"""
Daylight for NeoPixels
A program that will simulate a bright summer day, every day, from sunrise to sunset
Daylight will:
- connect hourly to a time server to get the current time
- ever 30 minutes, change the colour of the LEDs to simulate correct daylight at the current time of day
The colour data is based on colour temperatures between 2K and 10K Kelvin.
The time/colour relationship is based on a rising and falling S-bend curve to simulate a longer 'bright' condition over the mid-day hours
The brightness is also simulated along the S-bend curves.
REQUIRED HARDWARE:
* RGB NeoPixel LEDs connected to pin GP0.
* Raspberry Pi Pico W
LAST UPDATE: 2023-03-12
TODO:
* add code to put Pico into DeepSleep mode:
import machine
# Put the Pico into deep sleep for 10 seconds
machine.deepsleep(10000)
* >> added code to look for pushbutton
* pushbutton will change 'modes'
* modes = default or cooler or warmer
* make default mode slightly warmer (10%)
* >> added code to watch pushbutton for long press (5 sec)
* if longpress, reboot
"""
import os
import supervisor
import ipaddress
import wifi
import socketpool
import time
import rtc
import board
import digitalio
import busio
import neopixel
import adafruit_requests as requests
import adafruit_ntp
# Define the NeoPixel strip
NUM_PIXELS = 17 # for Terrarium - 12 for ring
pixels = neopixel.NeoPixel(board.GP0, NUM_PIXELS)
bright_mod = 1.5 # modifier to reduce the brightness
# set up GP7 as an input with a pull-up resistor
button = digitalio.DigitalInOut(board.GP7)
button.direction = digitalio.Direction.INPUT
button.pull = digitalio.Pull.UP
mode = 0
# Define the RGB colors and brightness levels - mode 0 default
COLORS_0 = [(178,127,96), (185,124,106), (207,161,120), (225,179,150), (229,192,166), (237,207,184), (246,218,222), (247,233,241), (240,236,236), (228,241,241), (223,245,252), (207,246,255), (200,255,254), (183,255,255), (190,239,255), (184,237,255), (178,239,255), (172,230,255), (166,239,255), (158,225,255), (154,239,255), (147,220,255), (141,239,255), (135,218,255), (128,239,255), (122,208,255), (116,239,255), (113,207,255), (107,235,255), (103,197,255), (97,235,255), (90,175,255), (83,239,255), (76,178,255), (69,239,255), (62,210,255), (55,239,255), (48,191,255)]
BRIGHTNESSES_0 = [0.0041, 0.0081, 0.0155, 0.0268, 0.0531, 0.0975, 0.1758, 0.2694, 0.3639, 0.4762, 0.5937, 0.7068, 0.8092, 0.903, 0.947, 0.9661, 0.9856, 0.9958, 0.9988, 0.9956, 0.9856, 0.969, 0.947, 0.9196, 0.8861, 0.8466, 0.8092, 0.7534, 0.7068, 0.6439, 0.5741, 0.4978, 0.4154, 0.3274, 0.2343, 0.1369, 0.0362, 0.0041]
COLORS_1 = [(214, 140, 76), (222, 137, 83), (249, 176, 89), (255, 197, 127), (255, 214, 150), (255, 237, 178), (255, 250, 217), (255, 247, 233), (249, 246, 246), (230, 252, 252), (221, 255, 255), (204, 255, 255), (190, 255, 255), (220, 233, 255), (215, 229, 255), (208, 231, 255), (202, 222, 255), (196, 229, 255), (188, 214, 255), (184, 229, 255), (176, 207, 255), (171, 227, 255), (164, 201, 255), (159, 227, 255), (151, 195, 255), (145, 226, 255), (142, 191, 255), (136, 220, 255), (131, 178, 255), (126, 218, 255), (117, 156, 255), (108, 227, 255), (99, 162, 255), (90, 226, 255), (81, 185, 255), (72, 226, 255), (63, 168, 255), (54, 225, 255)]
BRIGHTNESSES_1 = [0.0041, 0.0081, 0.0155, 0.0268, 0.0531, 0.0975, 0.1758, 0.2694, 0.3639, 0.4762, 0.5937, 0.7068, 0.8092, 0.903, 0.947, 0.9661, 0.9856, 0.9958, 0.9988, 0.9956, 0.9856, 0.969, 0.947, 0.9196, 0.8861, 0.8466, 0.8092, 0.7534, 0.7068, 0.6439, 0.5741, 0.4978, 0.4154, 0.3274, 0.2343, 0.1369, 0.0362, 0.0041]
COLORS_2 = [(249, 162, 57), (255, 157, 70), (255, 207, 84), (255, 234, 127), (255, 250, 151), (255, 237, 171), (255, 251, 207), (255, 243, 224), (255, 243, 243), (239, 255, 255), (230, 255, 255), (212, 255, 255), (196, 255, 255), (237, 212, 255), (232, 205, 255), (225, 207, 255), (219, 196, 255), (212, 203, 255), (204, 187, 255), (200, 202, 255), (191, 178, 255), (186, 198, 255), (178, 168, 255), (173, 197, 255), (164, 159, 255), (158, 195, 255), (152, 152, 255), (145, 179, 255), (140, 139, 255), (134, 167, 255), (124, 115, 255), (115, 181, 255), (105, 126, 255), (96, 180, 255), (87, 147, 255), (78, 180, 255), (69, 113, 255), (60, 179, 255)]
BRIGHTNESSES_2 = [0.0041, 0.0081, 0.0155, 0.0268, 0.0531, 0.0975, 0.1758, 0.2694, 0.3639, 0.4762, 0.5937, 0.7068, 0.8092, 0.903, 0.947, 0.9661, 0.9856, 0.9958, 0.9988, 0.9956, 0.9856, 0.969, 0.947, 0.9196, 0.8861, 0.8466, 0.8092, 0.7534, 0.7068, 0.6439, 0.5741, 0.4978, 0.4154, 0.3274, 0.2343, 0.1369, 0.0362, 0.0041]
# Define the appointment times as tuples of hour and minute
APPOINTMENTS = [(5, 4), (5, 34), (6, 4), (6, 34), (7, 4), (7, 34), (8, 4), (8, 34), (9, 4), (9, 34), (10, 4), (10, 34), (11, 4), (11, 34), (12, 4), (12, 34), (13, 4), (13, 34), (14, 4), (14, 34), (15, 4), (15, 34), (16, 4), (16, 34), (17, 4), (17, 34), (18, 4), (18, 34), (19, 4), (19, 34), (20, 4), (20, 34), (21, 4), (21, 34), (22, 4), (22, 34), (23, 4), (23, 34)]
# Define the blank and blink routine
def blank_and_blink():
print("Flashing LEDs")
# Set all pixels to off (black)
pixels.brightness = .01
pixels.fill((0, 0, 0))
pixels.show()
# Blink the pixels 2 times
for i in range(2):
pixels.fill((255, 255, 255))
pixels.show()
time.sleep(0.1)
pixels.fill((0, 0, 0))
pixels.show()
time.sleep(0.1)
# Set all pixels to off (black) again
pixels.fill((0, 0, 0))
pixels.show()
blank_and_blink()
print("+++ SETTING UP +++")
pixels.fill((128, 128, 0)) # Yellow for setup stage
pixels.brightness = .01
pixels.show()
time.sleep(2)
# ---
print()
print("Connecting to WiFi")
# connect to your SSID
while True:
try:
myssid = os.getenv('CIRCUITPY_WIFI_SSID')
wifi.radio.connect((myssid), os.getenv('CIRCUITPY_WIFI_PASSWORD'))
print("Connected to", myssid)
pixels.fill((0, 128, 0)) # Green for WiFi connect
pixels.brightness = .01
pixels.show()
time.sleep(2)
break
except Exception as e:
print("=== Failed to connect to Wi-Fi network === ", e)
print("=== Failed to connect to Wi-Fi network:", myssid, " ===")
print("Retrying in 5 seconds...")
pixels.fill((255, 0, 0)) # Red for WiFi failure
pixels.brightness = .01
pixels.show()
time.sleep(5)
pixels.fill((0, 0, 128)) # Blue for Retry WiFi
pixels.brightness = .01
pixels.show
myssid = ""
time.sleep(2)
pixels.fill((128, 0, 128)) # Magenta - Update the Realtime Clock
pixels.brightness = .01
pixels.show()
time.sleep(2)
pool = socketpool.SocketPool(wifi.radio)
# prints IP address to REPL
print("My IP address is", wifi.radio.ipv4_address)
# pings Google
print("Testing internet speed...")
ipv4 = ipaddress.ip_address("8.8.4.4")
print("Ping from google.com: %f ms" % (wifi.radio.ping(ipv4)*1000))
# Show date and time timezone offset for Edmonton and update the onboard real time clock
while True:
try:
ntp = adafruit_ntp.NTP(pool, tz_offset=-7)
rtc.RTC().datetime = ntp.datetime
print("RTC updated")
pixels.fill((0, 128, 0)) # Green for clock update
pixels.brightness = .01
pixels.show()
time.sleep(2)
break
except Exception as e:
print("Failed to set RTC:", e)
print("Retrying...")
pixels.fill((255, 0, 0)) # Red for failure
pixels.brightness = .01
pixels.show()
time.sleep(5)
pixels.fill((0, 0, 128)) # Blue for Retry
pixels.brightness = .01
pixels.show
time.sleep(2)
print("+++ SETUP COMPLETE +++")
# Get the current time
current_time = time.localtime()
print(f"Current time: {current_time.tm_hour:02d}:{current_time.tm_min:02d}")
print("++++++++++++++++++++++")
# blank_and_blink()
def watch_button():
global mode # use the global variable mode
if not button.value: # Pin pulled low
mode += 1
process_button()
if mode > 2:
mode = 0 # reset mode to 0
else:
# print("button not pressed") # Pin stays high
time.sleep(5) # sleep for 0.25 seconds to conserve power
return mode
def process_button():
blank_and_blink()
print("Button pressed -- Processing button...")
print("Mode:", mode)
# Start the timer
start_time = None
elapsed_time = 0
while True:
# Check if the button is pressed
if not button.value:
# Start the timer if it hasn't been started yet
if start_time is None:
start_time = time.monotonic()
else:
# Calculate the elapsed time
elapsed_time = time.monotonic() - start_time
# Check if the button has been held for 5 seconds or more
if elapsed_time >= 5:
print("Button was held for 5 seconds or more! Rebooting...")
pixels.fill((255, 0, 0)) # LEDS to Red
pixels.show()
time.sleep(3) # Pause for 3 seconds
supervisor.reload()
break
else:
# Reset the timer if the button is released
start_time = None
elapsed_time = 0
break
# ####### PROGRAM STARTS HERE
while True:
current_mode = watch_button()
current_time = time.localtime()
# Check if the current time matches any of the appointment times
for i, appointment in enumerate(APPOINTMENTS):
hour, minute = appointment
if current_time.tm_hour > hour or (current_time.tm_hour == hour and current_time.tm_min >= minute):
if i == len(APPOINTMENTS) - 1 or (i < len(APPOINTMENTS) - 1 and (current_time.tm_hour < APPOINTMENTS[i+1][0] or (current_time.tm_hour == APPOINTMENTS[i+1][0] and current_time.tm_min < APPOINTMENTS[i+1][1]))):
# Set the NeoPixels to the corresponding color and brightness level
if mode == 1:
pixels.fill(COLORS_1[i])
pixels.brightness = BRIGHTNESSES_1[i] / bright_mod # Set the brightness and apply modifier
print(f"Mode: {mode}, NeoPixel color: {COLORS_1[i]}, brightness: {BRIGHTNESSES_1[i]}, / bright_mod: {bright_mod}, time: {current_time.tm_hour:02d}:{current_time.tm_min:02d}")
elif mode == 2:
pixels.fill(COLORS_2[i])
pixels.brightness = BRIGHTNESSES_2[i] / bright_mod # Set the brightness and apply modifier
print(f"Mode: {mode}, NeoPixel color: {COLORS_2[i]}, brightness: {BRIGHTNESSES_2[i]}, / bright_mod: {bright_mod}, time: {current_time.tm_hour:02d}:{current_time.tm_min:02d}")
else:
pixels.fill(COLORS_0[i])
pixels.brightness = BRIGHTNESSES_0[i] / bright_mod # Set the brightness and apply modifier
print(f"Mode: {mode}, NeoPixel color: {COLORS_0[i]}, brightness: {BRIGHTNESSES_0[i]}, / bright_mod: {bright_mod}, time: {current_time.tm_hour:02d}:{current_time.tm_min:02d}")
break
# Wait for 1 second before checking again
time.sleep(1)
Circuit PythonAnd here’s what I settled on. Turns out I didn’t really need the Raspberry Pi Pico W to know what time it was, as the power strip it was plugged into was controlled by a timer.
I just needed it to turn on, and show a nice colour suitable for a terrarium. Forty some-odd lines vs two hundred and fifty.
import board
import neopixel
import time
from digitalio import DigitalInOut, Direction, Pull
# Configure the NeoPixel strip
pixel_pin = board.GP0
num_pixels = 17
pixels = neopixel.NeoPixel(pixel_pin, num_pixels, brightness=1, auto_write=False)
# Define the RGB colors
warm_white_color = (255, 170, 80)
cool_white_color = (150, 200, 255)
cactus_color = (255, 80, 0)
# Set the initial color
color = warm_white_color
# Define a button
button_pin = board.GP7
button = DigitalInOut(button_pin)
button.direction = Direction.INPUT
button.pull = Pull.UP
# Define a function to cross-fade between colors
def crossfade(start_color, end_color, steps):
for i in range(steps):
r = int(start_color[0] * (steps-i) / steps + end_color[0] * i / steps)
g = int(start_color[1] * (steps-i) / steps + end_color[1] * i / steps)
b = int(start_color[2] * (steps-i) / steps + end_color[2] * i / steps)
pixels.fill((r, g, b))
pixels.show()
time.sleep(0.05)
# Main loop
while True:
if not button.value:
if color == warm_white_color:
crossfade(warm_white_color, cool_white_color, 10)
color = cool_white_color
elif color == cool_white_color:
crossfade(cool_white_color, cactus_color, 10)
color = cactus_color
else:
crossfade(cactus_color, warm_white_color, 10)
color = warm_white_color
else:
pixels.fill(color)
pixels.show()
Circuit Python
Leave a Reply