Required libraries

This project does not require any libraries on top of the standard libraries for Circuit Playground boards.

Installing the Project Code

Download a zip of the project by clicking 'Download: Project Zip' in the preview of code.py below.

Copy code.py to the CIRCUITPY drive which appears when the Circuit Playground is connected to your computer via a USB cable.

import time
import math
from adafruit_circuitplayground import cp

brightness = 0

# List that holds the last 10 z-axis acceleration values read from the accelerometer.
# Used for the n=10 moving average
last10 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

# List that holds the last 50 z-axis acceleration values read from the accelerometer.
# Used for the n=50 moving average
last50 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
          0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
          0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
          0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
          0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

consecutive_triggers = 0
cp.pixels.fill((255, 0, 0))

light_on = False

while True:
    x, y, z = cp.acceleration

    # moving average n=10, not super smooth, but it substantially lowers the amount of noise
    last10.append(z)
    last10.pop(0)
    avg10 = sum(last10)/10

    # moving average n=50, very smooth
    last50.append(z)
    last50.pop(0)
    avg50 = sum(last50)/50

    # If the difference between the moving average of the last 10 points and the moving average of
    # the last 50 points is greater than 1 m/s^2, this is true
    if avg10 - avg50 > 1:
        if consecutive_triggers > 3: # Is true when avg10-avg50 > 1m/s^2 at least 3 times in a row
            # Detects shake. Due to the very low shake threshold, this alone would have very low
            # specificity. This was mitigated by having it only run when the acceleration
            # difference is greater than 1 m/s^2 at least 3 times in a row.
            if not cp.shake(shake_threshold=10):
                # Set brightness to max, timestamp when it was set to max, and set light_on to true
                cp.pixels.brightness = 1
                start = time.monotonic()
                light_on = True
        consecutive_triggers += 1 # increase it whether or not the light is turned on

    # light_on variable is for short circuiting. Not really necessary, just makes things run faster
    elif not light_on or time.monotonic() - start > 0.4:
        # Sine wave used for the color breathing effect.
        # Max brightness can be adjusted with the coefficient.
        cp.pixels.brightness = abs(math.sin(brightness)) * 0.5
        brightness += 0.05
        consecutive_triggers = 0
        light_on = False

    time.sleep(0.02)

Code Run Through

First, the code imports the required libraries.

import time
import math
from adafruit_circuitplayground import cp

Then, it defines the variables it'll need in the main loop. brightness is used to control the brightness for the breathing effect. last10 becomes a 10-point moving average of the z-axis acceleration, and last50 does the same but with 50 points instead. After that, consecutive_triggers is defined. It is used to make sure the brake light only turns on when the acceleration threshold is met multiple times in a row to prevent false positives. Then, the NeoPixels are set to red. If you'd like to change the color the light uses, just change the RGB values there. light_on is also defined. It's used to bypass an if statement later on in the code to improve performance.

brightness = 0

last10 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

last50 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
          0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
          0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
          0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
          0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

consecutive_triggers = 0
cp.pixels.fill((255, 0, 0))

light_on = False

This next part just starts the while loop and gets the values from the accelerometer every time it runs.

while True:
    x, y, z = cp.acceleration

These next few lines are used to take the moving average. For each moving average, the last value gathered is appended to the list, and then the first is removed. Then, the average of each list is taken. This decreases noise coming from the accelerometer, which makes the brake light function more reliably, and prevents false positives.

last10.append(z)
last10.pop(0)
avg10 = sum(last10)/10

last50.append(z)
last50.pop(0)
avg50 = sum(last50)/50

This block decides whether or not the brake light is to be activated. First, it determines if the Z acceleration has been increasing by comparing the two moving averages. If that increase is in excess of 1 m/s², it will then check to see how many times that has recently happened.

If that has happened 3 or more times in a row, it then goes to check if the accelerometer in the board has detected being shaken. If the accelerometer is detecting being shaken, it won't turn on the light.

If it hasn't been shaken, and the previous conditions have all been met, then it sets the brightness to maximum and makes a timestamp for when that happened. That timestamp will later be compared to the current time. This method was used instead of time.sleep to preserve the validity of the moving averages, as sleeping would keep them from updating.

The variable light_on is then set to true. This will disable the if statement in the next block.

Regardless of what happens, as long as the initial condition (avg10 - avg50 > 1) was satisfied, the variable consecutive_triggers is increased by 1.

if avg10 - avg50 > 1:
    if consecutive_triggers > 3:
        if not cp.shake(shake_threshold=10):
            cp.pixels.brightness = 1
            start = time.monotonic()
            light_on = True
    consecutive_triggers += 1

The first line here is true when light_on is False, or when start subtracted from the current time is greater than 0.4 seconds. light_on uses short-circuiting to only have the second half of the if statement evaluated if light_on is True, which means that the brake light is currently on.

Then, if the statement evaluates to true, the breathing effect will continue. It uses a sine wave to generate the wave-shaped brightness curve. If you want to make it slower, decrease the number brightness is increased by, and if you want to adjust the max brightness, change the coefficient outside of the absolute value function. 

Then, consecutive_triggers is reset and light_on is set to False.

If the condition does not evaluate to True, then the code repeats.

elif not light_on or time.monotonic() - start > 0.4:
    cp.pixels.brightness = abs(math.sin(brightness)) * 0.5
    brightness += 0.05
    consecutive_triggers = 0
    light_on = False

After this, there's a time.sleep. This was added in because when there wasn't one, something wacky happened in the serial console that kept crashing my computer.

time.sleep(0.02)

This guide was first published on Jun 16, 2020. It was last updated on Jun 16, 2020.

This page (CircuitPython Setup and Code) was last updated on Jun 16, 2021.

Text editor powered by tinymce.