Circuit Playground Bluefruit Automatic Bike Brake Light
2020-07-28 | By Adafruit Industries
License: See Original Project 3D Printer 3D Printing
Courtesy of Adafruit
Guide by Dylan Herrada
Overview
This project makes use of two of the many features available on Circuit Playgrounds; the accelerometer and the NeoPixels. It uses the accelerometer to sense if the bike is braking and then uses the NeoPixels to indicate that the bike is slowing down.
For this project, you will need one Circuit Playground (Bluefruit or Express), one Circuit Playground case, one 500mAh LiPo battery, and one JST extension cable with an on/off switch. You can print a mount that goes on the rails of a bike saddle, and then attach a Circuit Playground and a battery to it.
Two Design Options!
We included two mounting options.
The first design mounts under the seat, while the second design mounts to the seat post.
Both designs include source files to fully customize an exact fit to your bike!
Parts
- Circuit Playground Bluefruit - Bluetooth Low Energy
- Adafruit Circuit Playground Express or Bluefruit Enclosure
- Lithium Ion Polymer Battery - 3.7v 500mAh
- JST 2-pin Extension Cable with On/Off Switch - JST PH2
Additional Items
- One 1/4" 20 nut to secure the two halves of the mount together.
- One 1/4" 20 X 1/2" bolt to attach the Circuit Playground to the mount.
- One 1/4" 20 X 1 1/2" to attach to the nut, securing the two halves of the mount.
- A few 1/4" 20 washers as spacers for the half-inch bolt, since it is slightly too long.
- A 3D Printer to print the mount for the light.
If you use a Circuit Playground Express, it will work fine, just not quite as well as it would on the faster Bluefruit version.
The JST on/off switch isn't necessary, but it is very helpful.
CircuitPython on Circuit Playground Bluefruit
Install or Update CircuitPython
Follow this quick step-by-step to install or update CircuitPython on your Circuit Playground Bluefruit.
Download the latest version of CircuitPython for this board via circuitpython.org
Click the link above and download the latest UF2 file
Download and save it to your Desktop (or wherever is handy).
Plug your Circuit Playground Bluefruit into your computer using a known-good data-capable USB cable.
A lot of people end up using charge-only USB cables and it is very frustrating! So make sure you have a USB cable you know is good for data sync.
Double-click the small Reset button in the middle of the CPB (indicated by the red arrow in the image). The ten NeoPixel LEDs will all turn red, and then will all turn green. If they turn all red and stay red, check the USB cable, try another USB port, etc. The little red LED next to the USB connector will pulse red - this is ok!
If double-clicking doesn't work the first time, try again. Sometimes it can take a few tries to get the rhythm right!
(If double-clicking doesn't do it, try a single-click!)
You will see a new disk drive appear called CPLAYBTBOOT.
Drag the adafruit_circuitpython_etc.uf2 file to CPLAYBTBOOT.
The LEDs will turn red. Then, the CPLAYBTBOOT drive will disappear and a new disk drive called CIRCUITPY will appear.
That's it, you're done! :)
Circuit Playground Bluefruit CircuitPython Libraries
The Circuit Playground Bluefruit is packed full of features like Bluetooth and NeoPixel LEDs. Now that you have CircuitPython installed on your Circuit Playground Bluefruit, you'll need to install a base set of CircuitPython libraries to use the features of the board with CircuitPython.
Follow these steps to get the necessary libraries installed.
Installing CircuitPython Libraries on Circuit Playground Bluefruit
If you do not already have a lib folder on your CIRCUITPY drive, create one now.
Then, download the CircuitPython library bundle that matches your version of CircuitPython from CircuitPython.org.
Download the latest library bundle from circuitpython.org
The bundle download as a .zip file. Extract the file. Open the resulting folder.
Open the lib folder found within.
Once inside, you'll find a lengthy list of folders and .mpy files. To install a CircuitPython library, you drag the file or folder from the bundle lib folder to the lib folder on your CIRCUITPY drive.
Copy the following folders and files from the bundle lib folder to the lib folder on your CIRCUITPY drive:
- adafruit_ble
- adafruit_bluefruit_connect
- adafruit_bus_device
- adafruit_circuitplayground
- adafruit_gizmo
- adafruit_hid
- adafruit_lis3dh.mpy
- adafruit_thermistor.mpy
- neopixel.mpy
Your lib folder should look like the image below.
Now you're all set to use CircuitPython with the features of the Circuit Playground Bluefruit!
3D Printing
Print Files
STL files for 3D printing are oriented to print "as-is" on FDM style machines. Original design source may be downloaded using the link below.
- Adafruit_Auto_Bike_Light_Bottom.stl
- Adafruit_Auto_Bike_Light_Top.stl
Download 3D-Print Files From Thingiverse
Slicing
Supports were used on the bottom part, but not on the top.
The parts were sliced using CURA using the slice settings below.
- PLA filament 210c extruder
- 0.1mm layer height
- 30% infill infill
- 60mm/s print speed
The supports for the bottom half were set to have an 80-degree support overhang angle to avoid them being used in the hole that connects the Circuit Playground, as it wasn't necessary. However, that may depend on the printer being used.
Design source files
The project assembly was designed in TinkerCAD. You can find them here: top part and bottom part.
3D Print Design Option 2
This second design option mounts to the bike seat post and incorporates an integrated slide switch. It uses a M5 screw to secure the mount in place.
Edit Design for Bike Mount Option 2
Download STLs for Bike Mount Option 2
Slicing
No Supports are required, we just added 5 top layers to ensure the slide switch roof prints with enough layers.
The parts were sliced using CURA using the slice settings below.
- PLA filament 210c extruder
- 0.2mm layer height
- 20% infill infill
- 60mm/s print speed
- 5 Top Layers
Assembly
Mounting the battery
The battery goes in the slot on the back of the bracket. If you have anything larger than the 500mah LiPo, it will not fit in there. The interior dimensions of the battery holder are 31 x 5mm. The holder is 19mm deep. If you don't have that battery, you can simply take a different LiPo and use adhesive velcro to attach it to the bracket or attach it under the seat with a velcro strap, rubber band, or something else.
Attaching the mount
Once you've printed the mount, use a 1/4 20 bolt and a 1/4 20 nut to attach it to the saddle rails. I used a nylon lock nut to avoid it shaking loose, but a normal nut should work fine as well.
The mount was designed to fit most saddle rails, so if your saddle has oval rails (saddles with carbon fiber rails often do), it might not work as well, not to mention that you have to be very careful clamping anything to carbon oval rails.
There is a chance that when you attach it to the rails, it won't grip well and will be able to be moved around easily with your hands. In that case, take a tiny bit of rubber from an old inner tube or from a light clamp shim, and put it in-between the 3d printed part and the saddle rails.
Then, take the 1/4" 20 X 1/2" bolt and connect the Circuit Playground to the mount, making sure that the JST battery connection is facing up. You'll probably have to put some washers or other spacers in-between the head of the bolt and the mount. I used a convex brake washer since I've got a bunch of bike parts lying around, but just about any washer that fits will work.
When it's all attached, it should look something like this:
Optional: Making a switch
If you don't have a JST extension cable with a switch built-in, now might be a good time to make one. They're quite simple to make, simply cut and strip one side of any JST extension cable (or even a lipo itself, although this isn't ideal) and solder the two of the ends to a switch. Make sure to tin the wires and the switch contacts beforehand as this makes it much easier. In the example below, I cut both ends since I wanted something rather short.
Design Option 2 Assembly
This second design option mounts to the bike seat post and incorporates an integrated slide switch.
We wired up a slide switch and made a JST adapter so we can easily disconnect it from the battery.
A tripod screw adapter is secured to the mount and features a threaded hole for screwing into.
A quarter twenty screw adapter is used to attach to the clear case.
The slide switch is press fitted into a built in holder behind the battery.
Move the switch to the center and then gently insert at an angle. The two metal sides will press fit between the wall on the case.
The case attaches into the tripod screw and easily connects to the JST adapter on the slide switch.
The battery is secured to the mount by sliding it into the pocket.
The slide switch can then plug into the battery.
The mount is designed to flex open so it can fit over the bike frame.
You can clip it right under the seat and slide up and down to adjust the position.
To secure the mount in place, insert an M5 screw and tightly fastened to a hex nut.
CircuitPython Setup and Code
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.
Download: Project Zip or code.py | View on Github
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.
Download: file
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.
Download: file
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.
Download: file
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.
Download: file
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.
Download: file
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.
Download: file
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.
Download: file
time.sleep(0.02)
Use
So, now that you know how the code works, it could be useful to know how it actually behaves.
Normally, it is in the mode where the brightness increases and decreases in a wave-shaped way, creating what I call a 'breathing' effect.
When the bike starts decelerating, the brake light turns on. This takes about 100-200 ms to happen from the second you start braking. The light can also be triggered by actions such as going over a speed bump. It works best on paved roads or smoother trails. It does not work very well on mountain biking trails and other rough terrains.
From the time the light was last turned on, and this can happen multiple times during a braking event, to when the breathing effect resumes, there are at least 0.4 seconds. This number can be greater than 0.4 since even when the light is on, the code is still checking to see if the bike is still decelerating and will reset the timer if it is.
As far as battery life goes, using the 500 mAh battery, you should probably get around 1.5 hours of usage from it. Because of that, you may want to consider bringing extra batteries for longer rides or attaching a larger battery under the seat using tape or velcro.
Final thoughts
Now that you've got the light all hooked up and working, you should be ready to go riding with it. Just keep in mind that this light is much more visible to other cyclists than to drivers of cars, so you should still be careful when riding at night. Riding bikes is a lot of fun, and I hope you enjoy building and using this as much as I did.
Have questions or comments? Continue the conversation on TechForum, DigiKey's online community and technical resource.
Visit TechForum