Aziz! Light!

An AI chatbot leads me on a coding adventure that ultimately ends up ignoring all the work the AI did in the first place.


I think I’m call­ing this one done. It had quite the jour­ney. The goal — build a LED light for this ter­rari­um. Looks like mis­sion accom­plished ‘eh?

Of course, I over-engin­eer­ing this suck­er. The NeoPixel strip is con­trolled by a Rasp­berry Pi Pico W run­ning Cir­cuit Python. Lots of poten­tial there.

Then the design to 3d print the ring to hold the lights, that was an inter­est­ing exer­cise in math, angles, and modi­fic­a­tion as the hand-made nature of the ter­rari­um ensured that angles and meas­ure­ments were all slightly different. 

The power / PCB box was reas­on­ably straight­for­ward though.

Cur­rently, it powers on, and the LEDs light a pleas­ing warm-white light. An extern­al powerbar is on a timer which ener­gizes everything.

… but the jour­ney to get here was inter­est­ing in its own right…

Enter the AI

Since I’m still a rel­at­ive noob, at Python, and Cir­cuit Python is a spe­cial­ized hybrid of Micro Python (which itself is a hybrid of Python for micro­con­trol­lers) is new to me, I thought I’d take the oppor­tun­ity to use Chat­G­PT as an assistant.

The work­flow was some­thing like this:

  • Define the prob­lem as a ques­tion for the AI
  • Test the solu­tion provided by the AI
  • Debug, as it most likely did­n’t work out of the gate

The last point is where I per­son­ally got the most value. What was the code sup­posed to do? Why did it fail? How did we fix it?

Feeping Creaturism

Of course, as things star­ted to work and the light was actu­ally work­ing, more ques­tions popped up and the AI was happy to attempt to provide solu­tions. This was a Rasp­berry Pi Pico W after all, we have a Net­work! So here’s what we tried…

  • Con­nect to my home net­work — because why not? Set this little Pi free!
  • Fig­ure out the time of day so we can turn on and off at a logic­al time? Of course! A simple mat­ter to con­nect to a NTP serv­er. On power up and every 30 mins — got to stay accur­ate for the plants ‘eh?
  • Provide feed­back loops in the form of col­oured lights and blinks to indic­ate status through the boot-up and time sync phases.
  • At sun­rise / sun­set, turn on / off accord­ingly. This was tricky. NTP did­n’t provide sun­rise / set times, so I picked a long day (June 21st), and asked Chat­G­PT to determ­ine sun­rise / set times based on my location.
  • Change col­our of the LEDs based on time of day. Ok, this one was very over-the-top. Even­tu­ally it resolved to the following: 
    • Break the dur­a­tion of day­light (sun­rise to sun­set) into 30 minute chunks
    • Every 30 minutes, set the LED RGB value and bright­ness level appro­pri­ate for that time of day. This is to sim­u­late the col­our tem­per­at­ure of the light through­out the day.

That last point was the most inter­est­ing actu­ally. Col­our tem­per­at­ure of the light var­ies quite a lot through­out the day. Sun­rise / sun­set are warm­er (more orange) than mid­day (more blue).  Yet, Chat­G­PT returned val­ues that seemed reas­on­able dur­ing the course of the day.

Yeah, it was overkill but because I could, I did. Here’s the code that did the work.

Cir­cuit Python
"""
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)



Cir­cuit Python

And here’s what I settled on.  Turns out I did­n’t really need the Rasp­berry Pi Pico W to know what time it was, as the power strip it was plugged into was con­trolled by a timer. 

I just needed it to turn on, and show a nice col­our suit­able for a ter­rari­um. Forty some-odd lines vs two hun­dred and fifty.

Cir­cuit Python
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()
Cir­cuit Python

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.