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:
- add a Wemos controlling extra LEDs on cycle helmet/rucksack/other wearable
- 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)
- get a Wemos working as the access point
- 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> </p><p> </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> </p><p> </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> </p><p> </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> </p><p> </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.
Alex from @RasPiTV showing off his cool @Raspberry_Pi powered smart LED bike lights #HackadayUncon @DesignSparkRS @RSComponents pic.twitter.com/mHDmXdCDoj
— Pete Wood (@petenwood) September 16, 2017
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!
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.