The best tools to make your project dreams come true

Login or Signup
USD


Circuit Playground Bluefruit Automatic Bike Brake Light

By Adafruit Industries

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.

 

NeoPixels_1

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!

bike_2a

Parts

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).

UF2_2

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!)

Bluefriut_3

You will see a new disk drive appear called CPLAYBTBOOT.

Drag the adafruit_circuitpython_etc.uf2 file to CPLAYBTBOOT.

drive_4

drive_5

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! :)

CIRCUITPY_6

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.

file_7

Open the lib folder found within.

File_8

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.

list_9

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.

lib_10

Now you're all set to use CircuitPython with the features of the Circuit Playground Bluefruit!

3D Printing

printing_11

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.

settings_12

Design source files

The project assembly was designed in TinkerCAD. You can find them here: top part and bottom part.

design_13

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.

design_13a

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

slicing_13b

supports_13c

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.

attach_14

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.

mount_15

When it's all attached, it should look something like this:

attached_16

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.

switch_17

Design Option 2 Assembly

This second design option mounts to the bike seat post and incorporates an integrated slide switch.

bike_17a

We wired up a slide switch and made a JST adapter so we can easily disconnect it from the battery.

leds_17b

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.

switch_17c

switch_17d

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.

thread_17e

thread_17f

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.

case_18a

case_18b

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.

secure_18c

bike_19

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

Copy Code
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

Copy Code
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

Copy Code
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

Copy Code
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

Copy Code
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

Copy Code
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

Copy Code
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

Copy Code
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.

bike_18

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.

Key Parts and Components

Add all Digi-Key Parts to Cart
  • 1528-4333-ND
  • 1528-2749-ND
  • 1528-1841-ND
  • 1528-1679-ND
  • F14PN-L-ND