Nov 022017
 
High visibility animated bike lights with raspberry pi and wemos

I’m going to combine days 4 and 5 into one final blog post and video because I want to get it finished and out there. You can find day 1 here, day 2 here and day 3 here.

I’m also publishing the code today in a slightly less documented/polished state than I usually do. But it works pretty well. I’ve been using these lights on my bike since Mid September (~6 weeks at the time of posting) and I’m really pleased with them.

On the road, cars treat me like another car because I am showing them what I’m going to do as if I was another car. I’m trying not to rely on the lights too much in case something goes wrong, but I have to admit that I do sometimes use them instead of sticking out my arm. I really should use them as supplementary rather than alternative to hand signals. If you make some for your bike, I advise you to do that.

On day 4 I did the build and installation. Then, on day 5, I made some software tweaks and added two status indicator LEDs to the back of the front light. I also conformal coated the electronics to rain-proof them.

Here’s the Video

I’ve made a video of the build and installation. At the end of it there’s a bit of GoPro footage from the rider’s perspective so you can see how easy it is to use the controls. You can also see the extra LEDs I added to the back of the front light as status indicators.

If you feel like having a go at this project, you can find the necessary RasPiO InsPiRing LED and driver board kit here

I’ve Really Enjoyed This Project

This has been an extremely rewarding and useful project. I enjoy using these lights every time I go out on my bike (mostly daytime). People comment about them (even teenagers) and say how cool they are and ask me where I got them from. I really enjoy telling them “I made them”.

Other people have told me I should patent it (which I can’t because it’s not novel enough) or make it into a product, which I would like to be able to do, but I get bogged down in the practicalities…

  • If it took me a full day to install, how easily can it be adapted?
  • Will people want to program it?
  • Would people actually want to pay what this would need to cost to be profitable?
  • How would I feel if someone got hurt because it failed?
  • How would I feel if I got sued because of that?

All the detailed stuff like that makes me think it may be a bit too big of a mountain for me to climb. I don’t rule the idea out, but I’m not rushing into it either. I’ve learnt a little bit about things that can go wrong with projects over the last few years and it gives me pause for thought. (Is that wisdom, fear or a healthy mix of the two? I don’t even know.)

Taking it (even) Further

Other ideas to improve/extend/supplement/complify the system are:

  1. add a Wemos controlling extra LEDs on cycle helmet/rucksack/other wearable
  2. implement some kind of UDP communication rather than http (TCP/IP) to eliminate delays from ‘acknowledgements’ (would also enable host device to work fine in the absence of others)
  3. get a Wemos working as the access point
  4. add more LEDs because you can never have enough

Here’s the Code

Here’s the Python 3 script which runs when the Pi boots up. In line 155 you need to add the MAC address of your Wemos

from time import sleep
import apa
from subprocess import call
import subprocess
import RPi.GPIO as GPIO
from threading import Thread
import sys

GPIO.setmode(GPIO.BCM)
ports = [19,13,17,22]   # 19 LEFT. 13 OFF. 17 BRAKE. 22 RIGHT

for port in ports:
    GPIO.setup(port, GPIO.IN) # all pulled up in hardware

numleds = 26    # number of LEDs in our display 24 circle + 2 on back
delay = 0.06
brightness = 0xFF
ledstrip = apa.Apa(numleds)

ledstrip.flush_leds()
ledstrip.zero_leds()
ledstrip.write_leds()

right_turn = [[23,12,25], [22,13],[21,14],[20,15],[19,16],[18,17]]
left_turn  = [ [0,11,24], [1,10], [2,9],  [3,8],  [4,7],  [5,6]]

bgr =[[0,125,255]]     # define green/red mix for yellow
indicator_delay = 0.5  # seconds to hold at "all on"
i = 0

def process_thread(command):
    print ("Thread: ", command)
    cmd = 'rm /home/pi/' + command
    call ([cmd], shell=True)
    cmd = 'wget http://' + ip + '/' + command # set up your wget
    call ([cmd], shell=True)
    sleep(indicator_delay)
    cmd = 'rm /home/pi/OFF'
    call ([cmd], shell=True)
    cmd = 'wget http://' + ip + '/OFF'
    call ([cmd], shell=True)

def goleft(channel):            # turn left indicators
    while not GPIO.input(19):
        # call left rear in another thread
        t = Thread(target=process_thread, args=("LEFT",))
        t.start()
        print("Go Left")
        i = 0
        ledstrip.zero_leds()
        ledstrip.write_leds()
        for level in left_turn:
            for led in level:
                ledstrip.led_values[led] = [brightness, bgr[i][0], bgr[i][1],bgr[i][2]]
            ledstrip.write_leds()
            print (brightness, bgr[i][0], bgr[i][1],bgr[i][2])
            sleep(delay)
            i += 1
            if i >= len(bgr):
                i = 0
        sleep(indicator_delay)
        ledstrip.zero_leds()
        ledstrip.write_leds()
        sleep(0.1)
    sleep(0.3)
    if not GPIO.input(17):       # read BRAKE port to set brake or tail
        brake(17)
    else:
        tail()

def goright(channel):            # turn right indicators
    while not GPIO.input(22):
        # call right rear in another thread
        t = Thread(target=process_thread, args=("RIGHT",))
        t.start()
        print("GO Right")
        i = 0
        ledstrip.zero_leds()
        ledstrip.write_leds()
        for level in right_turn:
            for led in level:
                ledstrip.led_values[led] = [brightness, bgr[i][0], bgr[i][1],bgr[i][2]]
            ledstrip.write_leds()
            print (brightness, bgr[i][0], bgr[i][1],bgr[i][2])
            sleep(delay)
            i += 1
            if i >= len(bgr):
                i = 0
        sleep(indicator_delay)
        ledstrip.zero_leds()
        ledstrip.write_leds()
        sleep(0.1)
    sleep(0.3)
    if not GPIO.input(17):
        brake(17)
    else:
        tail()

def brake(channel):          # front & rear lights to MAX
    print("Brake")
    cmd = 'rm /home/pi/BRAKE'
    call ([cmd], shell=True)
    cmd = 'wget http://' + ip + '/BRAKE'
    call ([cmd], shell=True)
    for led in range(numleds):
        ledstrip.led_values[led] = [brightness, 255, 255, 255]
    ledstrip.write_leds()    

def tail():        # fall back to dipped front and tail for default
    print("Tail")
    cmd = 'rm /home/pi/TAIL'
    call ([cmd], shell=True)
    cmd = 'wget http://' + ip + '/TAIL'
    call ([cmd], shell=True)
    for led in range(numleds):
        ledstrip.led_values[led] = [240, 255, 255, 255]
    ledstrip.write_leds()

def off():         # all lights off
    print("Off")
    cmd = 'rm /home/pi/OFF'
    call ([cmd], shell=True)
    cmd = 'wget http://' + ip + '/OFF'
    call ([cmd], shell=True)
    ledstrip.zero_leds()
    ledstrip.write_leds()

def shutdown():
    print ("shutting down Pi after 10 flashes")
    for w in range(10):
        for led in range(numleds):
            ledstrip.led_values[led] = [brightness, 0, 0, 255]
        ledstrip.write_leds()
        sleep(0.1)
        ledstrip.zero_leds()
        ledstrip.write_leds()
        sleep(0.1)
    off()
    GPIO.cleanup()
    cmd = 'sudo poweroff'
    call ([cmd], shell=True)
    sys.exit()               # should be redundant, but you never know

GPIO.add_event_detect(19, GPIO.FALLING, callback=goleft, bouncetime=500)
GPIO.add_event_detect(22, GPIO.FALLING, callback=goright, bouncetime=500)
GPIO.add_event_detect(17, GPIO.FALLING, callback=brake, bouncetime=300)

# this part needs to be made more robust in case only front lights on
# check what happens when rear not switched on
# either set a default value or make a loop repeat until there is a connection
# or throw a switch to not send commands to the rear if no rear detected
# could have it try a few times and then give up on the rear? 

logged_in = ''
while not ('INSERT_MAC_ADDRESS_of_WEMOS' in logged_in):
    logged_in = str(subprocess.check_output("arp -a; exit 0",
                                        stderr=subprocess.STDOUT, shell=True))
    print(logged_in)
    for x in range(5):                  #flash a blue warning light
        ledstrip.led_values[24] = [brightness, 255, 0, 0] 
        ledstrip.write_leds()
        sleep(0.5)
        ledstrip.zero_leds()
        ledstrip.write_leds()
        sleep(0.5)

output = logged_in.split("'")
clients = output[1].split("\\n")
del clients[-1]                  # get rid of crap

for client in clients:
    print(client)
    ip = client.split(" ")[1][1:-1]
    print("ip:", ip)
    mac = client.split(" ")[3]
    print("mac:", mac)
    if mac == 'INSERT_MAC_ADDRESS_of_WEMOS':
        print("Rear lights ip identified - using: ", ip)
        break

tail()  # Set tail lights on to begin with

try:
    while True:
        print ("Waiting for a button press")
        GPIO.wait_for_edge(13, GPIO.FALLING)  
        print ("Switching ALL Lights OFF")  
        off()
        sleep(0.3)             # bounce time
        if not GPIO.input(13):
            print ("Switching Tail lights ON")
            tail()

        for iterations in range(40): # poll every 0.05s 40 times
            if not GPIO.input(13):    
                sleep(0.05)
            else:                   # if button released, jump to
                break
        if GPIO.input(13):
            continue                # top of loop & restart 
        
        if not GPIO.input(13):
            print ("Exiting program - hold 3s more to shutdown Pi")
            ledstrip.led_values[24] = [brightness, 0, 255, 0]
            ledstrip.write_leds()
            sleep(2)

            if not GPIO.input(13):
                ledstrip.led_values[25] = [brightness, 0, 255, 0]
                ledstrip.write_leds()
                sleep(1)
                shutdown()
            else:
                sys.exit()

finally:
    print("All LEDs OFF - BYE!")
    ledstrip.zero_leds()
    ledstrip.write_leds()
    cmd = 'rm /home/pi/OFF'
    call ([cmd], shell=True)
    cmd = 'wget http://' + ip + '/OFF'
    call ([cmd], shell=True)
    cmd = '/home/pi/cleanup.sh'    # remove on/right/left/tail/brake files
    call ([cmd], shell=True)
    GPIO.cleanup()

# yellow loom wire = LEFT  19
# white loom wire  = OFF   13
# blue loom wire   = BRAKE 17
# green loom wire  = RIGHT 22
# red + one white + one blue joined to GND

# could maybe neaten up right and left into one single more general function
# robustify the ip address thing so if you turn devices on in the wrong
# order it still works
# could also recode BRAKE function so it makes use of latching switch
# sadly this conflicts with the indicators, so leave it unless I can figure out
# a way to AND them together

Wemos Arduino Sketch

This code is running on the Wemos D1 mini to control the rear lights. You need to add the wifi credentials from your Pi access point in lines 3 & 4…

#include <ESP8266WiFi.h>

const char *ssid = "Insert_Your_SSID_Here";
const char *password = "Insert_Your_Password_Here";

const char* host = "IP OF THE ESP8266"; //it will tell you the IP once it starts up
                                        //just write it here afterwards and upload
int ledPin = D4;

WiFiServer server(80); //just pick any port number you like

#define FASTLED_ESP8266_D1_PIN_ORDER
#include<FastLED.h>
#define NUM_LEDS 48
#define DATA_PIN 7
#define CLOCK_PIN 5

CRGBArray<NUM_LEDS> leds;


void setup() {
  Serial.begin(115200);
  delay(10);
Serial.println(WiFi.localIP());
  // prepare GPIO2
  pinMode(ledPin, OUTPUT);
  digitalWrite(D4, LOW);

  // Connect to WiFi network
  Serial.println();
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);

  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi connected");

  // Start the server
  server.begin();
  Serial.println("Server started");

  // Print the IP address
  Serial.println(WiFi.localIP());
  FastLED.addLeds<APA102, DATA_PIN, CLOCK_PIN, BGR, DATA_RATE_MHZ(12)>(leds, NUM_LEDS);
  FastLED.clear();
  for(int j=0; j<NUM_LEDS; j++){         // all LEDs to 50% red
    leds[j].setRGB(127, 0, 0);
  }
  FastLED.show();
}

int delay_ms = 40;     // 40 ms better when both indicators working 
/* animation for left indicator
16 to 23 on
*/
int left_turn[8][2] = {
  {0,15},
  {1,14},
  {2,13},
  {3,12},
  {4,11},
  {5,10},
  {6,9},
  {7,8}
};


/* animation for right indicator
40 to 47 on
*/
int right_turn[8][2] = {
  {24,39},
  {25,38},
  {26,37},
  {27,36},
  {28,35},
  {29,34},
  {30,33},
  {31,32}
};


void tail(){
      for(int j=0; j<NUM_LEDS; j++){         // all LEDs to 50% red
        leds[j].setRGB(127, 0, 0);
      }
      FastLED.show();
}

void brake(){
      for(int j=0; j<NUM_LEDS; j++){         // all LEDs to 100% red
        leds[j].setRGB(255, 0, 0);
      }
      FastLED.show();
}

void arrow(int red, int green, int blue, int dir){    
      if (dir == 1){
        for(int j=16; j<24; j++){
          leds[j].setRGB(red, green, blue);
        }
      }

      else if (dir == 2){
        for(int j=40; j<48; j++){
          leds[j].setRGB(red, green, blue);
        }        
      }
      
      for(int i=0; i<8; i++){
          if (dir == 1) {
            leds[left_turn[i][0]].setRGB(red, green, blue);
            leds[left_turn[i][1]].setRGB(red, green, blue);         
          }

          else if (dir == 2) {
            leds[right_turn[i][0]].setRGB(red, green, blue);
            leds[right_turn[i][1]].setRGB(red, green, blue);         
          }
                  
          else {     // this part still to be done - will turn all leds red using loop
            leds[right_turn[i][0]].setRGB(255, 0, 0);
            leds[right_turn[i][1]].setRGB(0, 0, 0);         
          }          
          delay(delay_ms);
          FastLED.show();
      }
      FastLED.show();
      delay(delay_ms);
}

String cmd = ""; 


void loop() {
  // Check if a client has connected
  
  WiFiClient client = server.available();
  if (!client) {
    return;
  }

  // Wait until the client sends some data
  while (!client.available()) {
    delay(1);
  }

  // Read the first line of the request
  String req = client.readStringUntil('\r');
  client.flush();

  // Match the request
  if (req.indexOf("") != -10) {  //checks if you're on the main page

    if (req.indexOf("/TAIL") != -1) { //checks if you clicked TAIL
      digitalWrite(ledPin, HIGH);
      Serial.println("You clicked TAIL");
      cmd = "TAIL";
      tail();
    }
    if (req.indexOf("/OFF") != -1) { //checks if you clicked OFF
      digitalWrite(ledPin, LOW);
      Serial.println("You clicked OFF");
      cmd = "OFF";
      FastLED.clear();
      FastLED.show();
    }

    if (req.indexOf("/BRAKE") != -1) { //checks if you clicked BRAKE
      digitalWrite(ledPin, HIGH);
      Serial.println("You clicked BRAKE");
      cmd = "BRAKE";
      brake();
    }
    
    if (req.indexOf("/LEFT") != -1) { //checks if you clicked LEFT
      digitalWrite(ledPin, HIGH);
      Serial.println("You clicked LEFT");
      cmd = "LEFT";
    }
    if (req.indexOf("/RIGHT") != -1) { //checks if you clicked RIGHT
      digitalWrite(ledPin, HIGH);
      Serial.println("You clicked RIGHT");
      cmd = "RIGHT";
    }
  }

  else {
    Serial.println("invalid request");
   client.stop();
    return;
  }

  // Prepare the response
  String s = "HTTP/1.1 200 OK\r\n";
  s += "Content-Type: text/html\r\n\r\n";
  s += "<!DOCTYPE HTML>\r\n<html>\r\n";
  
  s += "<br><input type=\"button\" name=\"b1\" value=\"Turn LEFT \" onclick=\"location.href='/LEFT'\" style=\"height:200px;width:98%;color: black; background-color: yellow; font-size: 150px;border-radius: 60px;\">";
  s += "<p>&nbsp;</p><p>&nbsp;</p>";
  s += "<input type=\"button\" name=\"b2\" value=\"Tail Lights\" onclick=\"location.href='/TAIL'\"style=\"height:200px;width:98%;color: white; background-color: red; font-size: 150px;border-radius: 60px;\">";
  s += "<p>&nbsp;</p><p>&nbsp;</p>";
  s += "<input type=\"button\" name=\"b3\" value=\"Lights OFF\" onclick=\"location.href='/OFF'\"style=\"height:200px;width:98%;color: white; background-color: black; font-size: 150px;border-radius: 60px;\">";
  s += "<p>&nbsp;</p><p>&nbsp;</p>";
  s += "<input type=\"button\" name=\"b4\" value=\"Brake Lights\" onclick=\"location.href='/BRAKE'\"style=\"height:200px;width:98%;color: white; background-color: red; font-size: 150px;border-radius: 60px;\">";
  s += "<p>&nbsp;</p><p>&nbsp;</p>";
  s += "<input type=\"button\" name=\"b5\" value=\"Turn RIGHT\" onclick=\"location.href='/RIGHT'\"style=\"height:200px;width:98%;color: black; background-color: yellow; font-size: 150px;border-radius: 60px;\">";

  s += "</html>\n";

  client.flush();
  // Send the response to the client
  client.print(s);
  delay(1);

  if (cmd == "LEFT"){
      FastLED.clear();
      FastLED.show();
      for(int k=0; k<1; k++){               //10 to 1
        arrow(255, 125, 0, 1);    //YELLOW
        //FastLED.clear();                  //
        //FastLED.show();                   //
        //delay(delay_ms);                  //
      }
      //tail();             //
      cmd = "TAIL";  // stops a duplicate indication next iteration
  }
  if (cmd == "RIGHT"){
      FastLED.clear();
      FastLED.show();
      for(int k=0; k<1; k++){              //10 to 1
        arrow(255, 125, 0, 2);    //YELLOW
        //FastLED.clear();                 //
        //FastLED.show();                  //
        //delay(delay_ms);                 //
      }
      //tail();                            // 
      cmd = "TAIL";  // stops a duplicate indication next iteration
  }
}

As Shown at Hackaday Unconference

In September I temporarily removed my lights from my bike (it saddened me to do it) so that I could exhibit them at the Hackaday Unconference in London.

  7 Responses to “High Visibility Cycle Lights with Raspberry Pi, ESP8266 & RasPiO InsPiRing – Day 4 Build Log”

  1. Hi,
    An interesting project, I notice in one of the earlier posts you tried it with Stretch and it did not work.
    Did you ever get to the bottom of why it did not work?

    Before Christmas I tried to set up some lights using my Inspiring circle and triangle running from a PI zero W with stretch . I could not get it to work, the wrong LEDS seem to be controlled. Running the clock.py example on a PI3 with Jessie works OK, moving the hardware to a PI 0W with stretch and the same example results in lots of leds flickering.

    I have got another PI0W running JESSIE controlling some pocketmoneyelectronics xmas trees, I will try that when the decorations come down to prove it is not the PI0W.

    cheers

    Steve

    • Stretch has messed up spi and I haven’t managed to get inspiring working with it.

      • Hi Alex,
        Since posting the above I found a post on the forum about spi where someone’s problem was solved by setting the clock frequency. I tried this and it solved my issue with the clock example. So I raised an issue on your GitHub showing what I changed. Did some further testing and all the examples work apart from 03. I was going to have another look at it today.

        • Thanks. I’d tried that already, but used example 3 as my base, so had assumed it was a fail. 😀 If it works with other examples that’s interesting.

          • Hi Alex,
            Have a look on GitHub at the issue I raised I have just added some more information. I reckon that in apa.py write_leds after the first for loop all values of self.led_values have been set to [0,0,0,0] so after that only the first led will be addressed. It also happens on example02, but because this example sets all elements of led_values and then writes them out it does not matter. If this makes sense!

  2. Just been doing some reading on spi and came across this
    The wiringPiSPIDataRW() function needs to be passed a bytes object in Python 3. In Python 2, it takes a string. The following should work in either Python 2 or 3:

    wiringpi.wiringPiSPISetup(channel, speed)
    buf = bytes([your data here])
    retlen, retdata = wiringpi.wiringPiSPIDataRW(0, buf)
    Now, retlen will contain the number of bytes received/read by the call. retdata will contain the data itself, and in Python 3, buf will have been modified to contain it as well (that won’t happen in Python 2, because then buf is a string, and strings are immutable).

    Full details of the API at: http://www.wiringpi.com

    The comment about buf being overwritten is interesting because this is the sort of thing that is happening.

    • the problem is caused by spider copying the received data into the object holding the transmitted data. I have raised an issue on github and described my solution.

Leave a Reply