Project showcase
A (not so) Electric Bike

A (not so) Electric Bike © GPL3+

A system that gives my bike front and tail lights, a turn signal, and a speedometer, all in a clean and simple design.

  • 1,747 views
  • 1 comment
  • 10 respects

Components and supplies

Ph a000067 iso (1) wzec989qrf
Arduino Mega 2560
×1
1.3" OLED Screen
×2
GPS Module (Generic)
Used a BN-180
×1
M4wmhjju6x4t2pe6gke0
Digilent WS2812 Addressable LED Strip
×1
15p1093 40
Buzzer, Piezo
×1
19mm Stainless Steel Momentary Pushbutton
×1
19mm Stainless Steel Toggle Pushbutton
Used one white LED button and one amber LED button
×2
Limit Switch
Could use other switches as well as long as they're momentary
×2
On Off On Toggle Switch
×1
Bourns pec11r 4215f s0024 image 75px
Rotary Encoder with Push-Button
×1
53m8754 40
Machine Screw, M2
For securing the limit switches, otherwise not needed
×4
Generic Battery Pack
×1

Necessary tools and machines

3drag
3D Printer (generic)
09507 01
Soldering iron (generic)
Hy gluegun
Hot glue gun (generic)

Apps and online services

About this project

Hello! My name is Collin Wentzien, and I am a 15 year old maker and electronics enthusiast. For these past few weeks, I’ve focused my attention away from video games and towards a project which I call the smart bike. I’m going to give you all a quick overview on how it works, and then I’ll talk about why I made it, how I made it, and then show you some footage of using it in the real world. Let’s hop into it.

Now, real quick, I just want to get something out of the way. This is not an electric bike. It has no motor and does not speed up my biking in any shape or form.

Here is a quick video describing the functions of the system.

Alright, now that you see what my project is about, I’m going to briefly talk about why I made it. Aside from building things and doing schoolwork, I also partake in cross country and soccer. However due to a recent injury, I’ve been unable to do either for the past seven weeks. My only form of exercise at this point is biking, and while biking, which I now do nearly every day, I often struggle to communicate with drivers. I mean, think about it: a car has ways to communicate, as you have front and back lights, turn signals, and a horn. On a bike however, you only have two things: your voice and your hands. It’s quite difficult to talk to drivers, especially if their window is rolled up, and hand gestures can often come across as rude. And that’s my problem. I just can’t seem to be able to communicate with drivers.

That’s where the smart bike system comes into play. It features a front light for night time biking, a brake light to alert drivers when you are slowing down or stopping, a turn signal, and a small piezo buzzer to notify nearby pedestrians of your presence. In addition to all that, the bike system also includes a GPS, letting me get time, date, and most importantly, my speed. And I was able to do all of this, for under $120.

Now, to building it. A list of all the parts is available at the top of the page. To power the smart bike system, I chose to use an Arduino MEGA Pro. There were a few reasons behind this approach, the main two being that I needed a decent amount of digital pins, and that my sketch size was almost double the capacity of an Arduino Nano, my original board. To drive the user interface, I used two 1.4 inch displays. I’ve used these little things more times than I can count, and I absolutely love them. To make the lights, I used some RGB addressable LEDs. Not only are they plenty bright for my application, but the fact that they’re multi-colored and addressable means that I can minimize the amount of wires needed. To detect my speed, I went with the NEO6M GPS module. This module works wonderfully with the TinyGPS++ library, which I ended up using in my code. Finally, for the power, lights, and horn buttons, my perfectionism opted into buying three stainless steel, LED-lit buttons. They were a bit pricey, but boy do they look nice. There’s a few other components I used, such as a piezo buzzer for the pedestrian horn, two endstop switches for the brake and alternate horn button, and an on-off-on toggle switch.

Here's an image of me using it at night, with the front light illuminated. It's a lot brighter in person!

And that's practically it! I'm finally going to go over a few improvements I would make next time, and clear up some questions anyone might've had.

For one, there is a reason why I put two horn switches on the bike system. The stainless steel switch on the main housing was originally intended to power a louder horn that cars could hear, while the switch on the handlebars of the bike would control the small piezo buzzer. That way, I could communicate with both cars and people, without scaring people or being too inaudible for cars. However, the horn I picked out drew too much power, and just wouldn’t work with a 9v battery. I would have probably been able to get it to work, but I already had other larger problems to deal with.

Another question you might ask is, “Why not use a motor or a hall effect sensor to detect the amount of times the wheel rotates for speed instead of using a GPS?” The answer is simple. I wanted to keep as many things on the handlebars of the bike as possible. That way, if I needed to remove the system for any reason, it would be a lot easier than removing multiple things from ten different parts of the bike. Another reason, mainly for the motor idea, would be the noise generated. Plus I wouldn’t want anyone thinking that my bike is an electric bike (if they don’t think that already).

With that all being said, there are definitely improvements I would like to make. I would probably replace the two small screens with a larger one, or just make the entire system a touchscreen. There are also other minor details that I would change, such as the color of the paint or the way the system attaches to the handlebars.

Another interesting change would be to make the system itself a bit safer. As of now, anyone can turn on the system and use it, so it would be interesting to try out different methods of unlocking the system, like a key or even an NFC card. That way, I could incorporate a garage door remote or something similar without the worry that someone could use it without authorized access.

However, there’s always room for improvement, and especially for the time being, I’m happy with my current design.

That about wraps it up! A smart bike system that converts my boring mountain bike into almost a car, except without an engine. And while this project definitely wasn’t necessary or easy, it was still very practical and fun to make. Plus, as Mark Rober puts it well, “if something isn’t exactly the way you want it… you just make it exactly the way you want it.”

Code

Bike SystemArduino
Contains code for bike system. Built and tested for Arduino MEGA and MEGA Pro.
#include <FastLED.h>
#include <TinyGPS++.h>
#include <EEPROM.h>
#include <avr/wdt.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH1106.h>
#include <Fonts/nunitoTiny.h>
#include <Fonts/makoSmall.h>
#include <Fonts/gothamBold.h>

static const unsigned char PROGMEM icon[] = {
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x07, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x03, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x01, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x01, 0xfd, 0xfd, 0xff, 0xcf, 0xff, 0xff, 0xff, 0xff, 0xfe, 0xfc, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x03, 0xf9, 0xfc, 0xff, 0x07, 0xff, 0xcf, 0xff, 0xe7, 0xfd, 0xf0, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x07, 0xf3, 0xf8, 0xff, 0x07, 0xff, 0x87, 0xff, 0xc3, 0xfb, 0xf0, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x0f, 0xe7, 0xf0, 0x7e, 0x07, 0xff, 0x07, 0xff, 0xc3, 0xf7, 0xe0, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x1f, 0xff, 0xe0, 0xfc, 0x01, 0xf0, 0x0f, 0xc0, 0x03, 0xe7, 0xc0, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x1f, 0xff, 0xc0, 0xf8, 0x03, 0xe0, 0x1f, 0x80, 0x07, 0xcf, 0x80, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x3f, 0xff, 0x81, 0xf0, 0x07, 0xc0, 0x1f, 0x00, 0x0f, 0xdf, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x7f, 0xff, 0x03, 0xe0, 0x0f, 0xc0, 0x3e, 0x00, 0x1f, 0xbf, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0xff, 0xfc, 0x07, 0xe0, 0x1f, 0x80, 0x7e, 0x00, 0x3f, 0xfe, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x01, 0xfc, 0x00, 0x0f, 0xc0, 0x1f, 0x00, 0xfc, 0x00, 0x3f, 0xfc, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x01, 0xfc, 0x00, 0x1f, 0x80, 0x3e, 0x01, 0xf8, 0x00, 0x7f, 0xf8, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x03, 0xf8, 0x00, 0x1f, 0x00, 0x7c, 0x01, 0xf0, 0x00, 0xff, 0xf0, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x07, 0xf0, 0x00, 0x3e, 0x00, 0xfc, 0x03, 0xe0, 0x01, 0xfb, 0xf0, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x0f, 0xe0, 0x00, 0x7e, 0x01, 0xf8, 0x07, 0xe0, 0x03, 0xf7, 0xe0, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x0f, 0xc0, 0x00, 0xfc, 0x03, 0xf0, 0x0f, 0xc0, 0x07, 0xe7, 0xc0, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x1f, 0xc0, 0x01, 0xf8, 0x03, 0xe0, 0x1f, 0xff, 0x07, 0xcf, 0x80, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x3f, 0x80, 0x01, 0xf0, 0x07, 0xc0, 0x1f, 0xfe, 0x0f, 0x9f, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x7f, 0x00, 0x03, 0xe0, 0x0f, 0xc0, 0x3f, 0xfc, 0x1f, 0xbe, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0xfe, 0x00, 0x07, 0xe0, 0x1f, 0x80, 0x1f, 0xf8, 0x3f, 0x7e, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};

static const unsigned char PROGMEM light[] = {
  0x00, 0x00, 
  0x00, 0x00, 
  0x01, 0x80, 
  0x19, 0x98, 
  0x1d, 0xb8, 
  0x0c, 0x30, 
  0x01, 0x80, 
  0x3b, 0xdc, 
  0x3b, 0xdc, 
  0x01, 0x80, 
  0x0c, 0x30, 
  0x1d, 0xb8, 
  0x19, 0x98, 
  0x01, 0x80, 
  0x00, 0x00, 
  0x00, 0x00
};

static const unsigned char PROGMEM stp[] = {
  0x00, 0x00, 
  0x00, 0x00, 
  0x00, 0x00, 
  0x00, 0x80, 
  0x02, 0xa0, 
  0x02, 0xa8, 
  0x02, 0xa8, 
  0x02, 0xa8, 
  0x03, 0xf8, 
  0x33, 0xf8, 
  0x1b, 0xf8, 
  0x0f, 0xf8, 
  0x07, 0xf8, 
  0x03, 0xf0, 
  0x00, 0x00, 
  0x00, 0x00
};

static const unsigned char PROGMEM horn[] = {
  0x00, 0x00, 
  0x00, 0x00, 
  0x00, 0x00, 
  0x01, 0xc4, 
  0x03, 0xae, 
  0x07, 0x2c, 
  0x1f, 0x00, 
  0x7f, 0x6e, 
  0x7f, 0x6e, 
  0x1f, 0x00, 
  0x07, 0x2c, 
  0x03, 0xae, 
  0x01, 0xc4, 
  0x00, 0x00, 
  0x00, 0x00, 
  0x00, 0x00
};

static const unsigned char PROGMEM battery[] = {
  0x00, 0x00, 
  0x00, 0x00, 
  0x00, 0x00, 
  0x00, 0x00, 
  0xff, 0xfe, 
  0x80, 0x02, 
  0xb6, 0xdb, 
  0xb6, 0xdb, 
  0xb6, 0xdb, 
  0xb6, 0xdb, 
  0x80, 0x02, 
  0xff, 0xfe, 
  0x00, 0x00, 
  0x00, 0x00, 
  0x00, 0x00, 
  0x00, 0x00
};

static const unsigned char PROGMEM satellite[] = {
  0x00, 0x00, 
  0x00, 0x00, 
  0x01, 0x00, 
  0x02, 0x80, 
  0x0c, 0x40, 
  0x0e, 0x20, 
  0x17, 0x40, 
  0x23, 0x80, 
  0x11, 0x92, 
  0x0a, 0x12, 
  0x04, 0x62, 
  0x00, 0x04, 
  0x00, 0x08, 
  0x00, 0x70, 
  0x00, 0x00, 
  0x00, 0x00
};

static const unsigned char PROGMEM power[] = {
  0x00, 0x00, 
  0x00, 0x00, 
  0x00, 0x00, 
  0x01, 0x80, 
  0x0d, 0xb0, 
  0x0d, 0xb0, 
  0x19, 0x98, 
  0x19, 0x98, 
  0x18, 0x18, 
  0x18, 0x18, 
  0x0c, 0x30, 
  0x0f, 0xf0, 
  0x03, 0xc0, 
  0x00, 0x00, 
  0x00, 0x00, 
  0x00, 0x00
};

static const unsigned char PROGMEM miles[] = {
  0x00, 0x00, 
  0x00, 0x00, 
  0x00, 0x00, 
  0x00, 0x00, 
  0x00, 0x00, 
  0x6e, 0x78, 
  0x7f, 0xfc, 
  0x3b, 0xdc, 
  0x31, 0x8c, 
  0x31, 0x8c, 
  0x31, 0x8c, 
  0x7b, 0xde, 
  0x00, 0x00, 
  0x00, 0x00, 
  0x00, 0x00, 
  0x00, 0x00
};

static const unsigned char PROGMEM kilos[] = {
  0x00, 0x00, 
  0x00, 0x00, 
  0x1c, 0x00, 
  0x0c, 0x00, 
  0x0c, 0x00, 
  0x0c, 0x70, 
  0x0c, 0xe0, 
  0x0d, 0xc0, 
  0x0f, 0x80, 
  0x0f, 0x00, 
  0x0f, 0x80, 
  0x0d, 0xc0, 
  0x0c, 0xe0, 
  0x1e, 0xf0, 
  0x00, 0x00, 
  0x00, 0x00
};

static const unsigned char PROGMEM reset[] = {
  0x00, 0x00, 
  0x00, 0x00, 
  0x00, 0x78, 
  0x0c, 0x70, 
  0x1c, 0x78, 
  0x18, 0x58, 
  0x30, 0x0c, 
  0x30, 0x0c, 
  0x30, 0x0c, 
  0x30, 0x0c, 
  0x18, 0x18, 
  0x1c, 0x38, 
  0x0f, 0xf0, 
  0x03, 0xc0, 
  0x00, 0x00, 
  0x00, 0x00
};

static const unsigned char PROGMEM back[] = {
  0x00, 0x00, 
  0x00, 0x00, 
  0x00, 0x00, 
  0x03, 0x80, 
  0x07, 0x00, 
  0x0e, 0x00, 
  0x1c, 0x00, 
  0x3f, 0xfc, 
  0x3f, 0xfc, 
  0x1c, 0x00, 
  0x0e, 0x00, 
  0x07, 0x00, 
  0x03, 0x80, 
  0x00, 0x00, 
  0x00, 0x00, 
  0x00, 0x00
};

static const unsigned char PROGMEM error[] = {
  0x00, 0x00, 
  0x00, 0x00, 
  0x01, 0x80, 
  0x03, 0xc0, 
  0x03, 0xc0, 
  0x06, 0x60, 
  0x06, 0x60, 
  0x0e, 0x70, 
  0x0e, 0x70, 
  0x1e, 0x78, 
  0x1e, 0x78, 
  0x3f, 0xfc, 
  0x3e, 0x7c, 
  0x7e, 0x7e, 
  0x7f, 0xfe, 
  0x00, 0x00
};

#define LCLK   47
#define LMOSI  46
#define LRESET 44
#define LDC    45
#define LCS    42

Adafruit_SH1106 lscr(LMOSI, LCLK, LDC, LRESET, LCS);

#define RCLK   40
#define RMOSI  43
#define RRESET 31
#define RDC    41
#define RCS    29

Adafruit_SH1106 rscr(RMOSI, RCLK, RDC, RRESET, RCS);

#define frontPin     9
#define frontNum    12
#define brakePin    32
#define brakeNum    12

CRGB brakeLEDS[brakeNum];
CRGB frontLEDS[frontNum];

TinyGPSPlus gps;

#define CLK 36
#define DT  35
#define SW  34

int counter = 0;
int currentStateCLK;
int lastStateCLK;
unsigned long lastButtonPress = 0;
unsigned long lastDraw = 0;
unsigned long lastBatt = 0;

int lblkState = 0;
int rblkState = 0;
int brakeState = 0;
int lightState = 0;
int hornState = 0;
int ahornState = 0;
int menuState = 0;

#define CLEAR CRGB(0, 0, 0)
#define LRED CRGB(40, 0, 0)
#define RED CRGB(255, 0, 0)
#define ORANGE CRGB(255, 40, 0)
#define WHITE CRGB(255, 255, 255)

int x, y;
int16_t x1, y1;
uint16_t w, h;

boolean menu = false;
int uph = 0;
double perc = 0;
int strength = 0;
String currTime = "";
String currDate = "";
int battLevel = 0;

int positionsX[] = {81, 79, 77, 76, 75, 75, 75, 76, 77, 79, 81, 83, 86, 89, 92, 95, 98, 101, 104, 107, 109, 111, 113, 114, 115, 115, 115, 114, 113, 111, 109};
int positionsY[] = {45, 43, 40, 37, 34, 31, 28, 25, 22, 19, 17, 15, 13, 12, 11, 11, 11, 12,  13,  15,  17,  19,  22,  25,  28,  31,  34,  37,  40,  43,  45 };

boolean gpsEnabled = true;
boolean lightOn = false;
boolean tailOn = false;

void setup() {
  Serial.begin(9600);
  Serial1.begin(9600);
  pinMode(5, INPUT);
  pinMode(6, INPUT);
  pinMode(7, INPUT);
  pinMode(4, INPUT);
  pinMode(3, INPUT);
  pinMode(33, INPUT);

  pinMode(38, OUTPUT);

  pinMode(CLK,INPUT);
  pinMode(DT,INPUT);
  pinMode(SW, INPUT_PULLUP);

  lastStateCLK = digitalRead(CLK);

  rscr.begin(SH1106_SWITCHCAPVCC);
  lscr.begin(SH1106_SWITCHCAPVCC);
  delay(100);

  FastLED.addLeds<WS2812, brakePin, GRB>(brakeLEDS, brakeNum);
  FastLED.addLeds<WS2812, frontPin, GRB>(frontLEDS, frontNum);
  
  clr();
  FastLED.show();
  
  rscr.clearDisplay();
  rscr.setTextColor(WHITE);
  rscr.setFont(&makoSmall);
  rscr.setCursor(7, 25);
  rscr.print("Designed by");
  rscr.setFont(&nunitoTiny);
  rscr.setCursor(7, 45);
  rscr.print("Collin W");
  rscr.display();

  lscr.clearDisplay();
  lscr.setTextColor(WHITE);
  lscr.drawBitmap(0, 0, icon, 128, 64, WHITE);
  lscr.display();

  delay(2000);

  Serial.println("PITCHsmart by Collin W");

  drawLeftScreen();
  drawRightScreen();
}

void drawLeftScreen() {
  lscr.clearDisplay();
  lscr.drawLine(0, 19, 127, 19, WHITE);

  if(lblkState == HIGH) {
    lscr.drawLine(3, 8, 8, 3, WHITE);
    lscr.drawLine(3, 9, 9, 3, WHITE);
    lscr.drawLine(4, 9, 9, 4, WHITE);
    lscr.drawLine(3, 9, 8, 14, WHITE);
    lscr.drawLine(3, 8, 9, 14, WHITE);
    lscr.drawLine(4, 8, 9, 13, WHITE);
    
  } else if(rblkState == HIGH) {
    lscr.drawLine(119, 3, 124, 8, WHITE);
    lscr.drawLine(118, 3, 124, 9, WHITE);
    lscr.drawLine(118, 4, 123, 9, WHITE);
    lscr.drawLine(119, 14, 124, 9, WHITE);
    lscr.drawLine(118, 14, 124, 8, WHITE);
    lscr.drawLine(118, 13, 123, 8, WHITE);
  }

  if(brakeState == HIGH) {
    lscr.drawBitmap(30, 1, stp, 16, 16, WHITE);
  }

  if(hornState == HIGH || ahornState == HIGH) {
    digitalWrite(38, HIGH);
    lscr.drawBitmap(55, 1, horn, 16, 16, WHITE);
  } else {
    digitalWrite(38, LOW);
  }

  if(lightState == HIGH) {
    lscr.drawBitmap(80 , 1, light, 16, 16, WHITE);
  }

  lscr.drawBitmap(7, 25, battery, 16, 16, WHITE);
  lscr.drawBitmap(7, 44, satellite, 16, 16, WHITE);

  lscr.setFont(&makoSmall);
  lscr.setCursor(27, 36);
  lscr.print("100%");
  lscr.setCursor(27, 55);
  if(gpsEnabled == true)
    lscr.print(String(strength));
  else
    lscr.print("--");

  if(currTime == "")
    currTime = "--:--";
  lscr.getTextBounds(currTime, x, y, &x1, &y1, &w, &h);
  lscr.setCursor(119 - w, 36);
  lscr.print(currTime);
  if(currDate == "")
    currDate = "--/--";
  lscr.getTextBounds(currDate, x, y, &x1, &y1, &w, &h);
  lscr.setCursor(119 - w, 55);
  lscr.print(currDate);

  lscr.display();
}

void drawRightScreen() {
  rscr.clearDisplay();

  rscr.setFont(&nunitoTiny);
  rscr.setCursor(7, 54);
  if(EEPROM.read(0) == 0)
    rscr.print("mph");
  else
    rscr.print("kmph");

  rscr.setFont(&gothamBold16pt7b);
  rscr.setCursor(7, 34);
  if(gpsEnabled == true)
    rscr.print(String(uph));
  else
    rscr.print("--");

  rscr.drawCircle(95, 31, 30, WHITE);
  rscr.fillTriangle(95, 31, 66, 61, 124, 61, BLACK);
  rscr.drawLine(66, 31, 124, 31, WHITE);
  rscr.drawLine(95, 2, 95, 31, WHITE);
  rscr.drawLine(75, 11, 115, 51, WHITE);
  rscr.drawLine(75, 51, 115, 11, WHITE);
  rscr.fillCircle(95, 31, 23, BLACK);
  rscr.fillCircle(95, 31, 2, WHITE);

  rscr.setFont(&makoSmall);
  rscr.setCursor(81, 45);
  rscr.print("0");
  rscr.setCursor(99, 45);
  if(EEPROM.read(0) == 0)
    rscr.print("30");
  else
    rscr.print("60");

  rscr.getTextBounds(String(perc) + "%", x, y, &x1, &y1, &w, &h);
  rscr.setCursor(95 - (w / 2), 60);
  rscr.print(String(perc) + "%");

  if(uph <= 30 && EEPROM.read(0) == 0)
    rscr.drawLine(95, 31, positionsX[uph], positionsY[uph], WHITE);
  else if(uph <= 60 && EEPROM.read(0) == 1)
    rscr.drawLine(95, 31, positionsX[uph/2], positionsY[uph/2], WHITE);
  
  rscr.display();
}

void drawMenu(int option) {
  if(menu == false) {
    counter = option;
    menu = true;
    animate(true);

    lscr.clearDisplay();
    rscr.clearDisplay();
    lscr.display();
    rscr.display();
  }

  lscr.drawBitmap(3, 13, power, 16, 16, WHITE);
  lscr.drawBitmap(67, 13, reset, 16, 16, WHITE);
  if(EEPROM.read(0) == 0) {
    lscr.fillRect(3, 42, 16, 16, BLACK);
    lscr.drawBitmap(3, 42, miles, 16, 16, WHITE);
  } else {
    lscr.fillRect(3, 42, 16, 16, BLACK);
    lscr.drawBitmap(3, 42, kilos, 16, 16, WHITE);
  }
  lscr.drawBitmap(67, 42, back, 16, 16, WHITE);

  lscr.drawRect(1, 11, 61, 20, BLACK);
  lscr.drawRect(65, 11, 61, 20, BLACK);
  lscr.drawRect(1, 40, 61, 20, BLACK);
  lscr.drawRect(65, 40, 61, 20, BLACK);

  if(option == 0)
    lscr.drawRect(1, 11, 61, 20, WHITE);
  else if(option == 1)
    lscr.drawRect(65, 11, 61, 20, WHITE);
  else if(option == 2)
    lscr.drawRect(1, 40, 61, 20, WHITE);
  else if(option == 3)
    lscr.drawRect(65, 40, 61, 20, WHITE);

  lscr.setFont(&makoSmall);
  lscr.setCursor(22, 24);
  lscr.print("Power");
  lscr.setCursor(86, 24);
  lscr.print("Reset");
  lscr.setCursor(22, 53);
  lscr.print("Units");
  lscr.setCursor(86, 53);
  lscr.print("Back");

  lscr.display();

  if(menu == true && option == -1) {
    animate(false);
    menu = false;
  }
}

void animate(boolean in) {
  if(in == true) {
    drawLeftScreen();
    
    for(int x = 0; x < 64; x += 4) {
      lscr.drawLine(0, x, x * 2, 0, WHITE);
      if(x != 0) {
        lscr.drawLine(0, x - 1, (x * 2) - 2, 0, BLACK);
        lscr.drawLine(0, x - 2, (x * 2) - 4, 0, BLACK);
        lscr.drawLine(0, x - 3, (x * 2) - 6, 0, BLACK);
        lscr.drawLine(0, x - 4, (x * 2) - 8, 0, BLACK);
      }
      
      lscr.display();
    }

    lscr.drawLine(0, 62, 126, 0, BLACK);
    lscr.drawLine(0, 61, 124, 0, BLACK);
    lscr.drawLine(0, 60, 122, 0, BLACK);
    lscr.drawLine(0, 59, 120, 0, BLACK);
    
    for(int x = 0; x < 64; x += 4) {
      lscr.drawLine(x * 2, 63, 127, x, WHITE);
      if(x != 0) {
        lscr.drawLine((x * 2) - 2, 63, 127, x - 1, BLACK);
        lscr.drawLine((x * 2) - 4, 63, 127, x - 2, BLACK);
        lscr.drawLine((x * 2) - 6, 63, 127, x - 3, BLACK);
        lscr.drawLine((x * 2) - 8, 63, 127, x - 4, BLACK);
      }
      
      lscr.display();
    }
    
  } else if(in == false) {
    for(int x = 63; x > -1; x -= 4) {
      lscr.drawLine(x * 2, 63, 127, x, WHITE);
      if(x != 63) {
        lscr.drawLine((x * 2) + 2, 63, 127, x + 1, BLACK);
        lscr.drawLine((x * 2) + 4, 63, 127, x + 2, BLACK);
        lscr.drawLine((x * 2) + 6, 63, 127, x + 3, BLACK);
        lscr.drawLine((x * 2) + 8, 63, 127, x + 4, BLACK);
      }
      
      lscr.display();
    }

    lscr.drawLine(2, 63, 127, 1, BLACK);
    lscr.drawLine(4, 63, 127, 2, BLACK);
    lscr.drawLine(6, 63, 127, 3, BLACK);
    lscr.drawLine(8, 53, 127, 4, BLACK);
    
    for(int x = 63; x > -1; x -= 4) {
      lscr.drawLine(0, x, x * 2, 0, WHITE);
      if(x != 63) {
        lscr.drawLine(0, x + 1, (x * 2) + 2, 0, BLACK);
        lscr.drawLine(0, x + 2, (x * 2) + 4, 0, BLACK);
        lscr.drawLine(0, x + 3, (x * 2) + 6, 0, BLACK);
        lscr.drawLine(0, x + 4, (x * 2) + 8, 0, BLACK);
      }
      
      lscr.display();
    }

    drawLeftScreen();
    drawRightScreen();
  }
}

void checkData() {
  brakeState = digitalRead(7);
  lblkState = digitalRead(6);
  rblkState = digitalRead(5);
  lightState = digitalRead(4);
  hornState = digitalRead(3);
  ahornState = digitalRead(33);
  menuState = digitalRead(SW);

  if(gpsEnabled == true) {
    if(Serial1.available() > 0) {
      if(gps.encode(Serial1.read())) {
        if (gps.speed.isValid()) {
          if(EEPROM.read(0) == 0)
            uph = gps.speed.mph();
          else
            uph = gps.speed.kmph();
  
          if(EEPROM.read(0) == 0)
            perc = gps.speed.mph() / 0.3;
          else
            perc = gps.speed.kmph() / 0.6;
  
          int hr = 0;
          if(gps.time.hour() > 12)
            hr = gps.time.hour() - 12;
          else
            hr = gps.time.hour();
          if(hr < 9)
            hr = hr + 5;
          else
            hr = hr - 7;
            
          if(gps.time.minute() < 10)
            currTime = String(hr) + ":0" + String(gps.time.minute());
          else
            currTime = String(hr) + ":" + String(gps.time.minute());
  
          if(gps.time.hour() < 13)
            currDate = String(gps.date.month()) + "/" + String(gps.date.day() - 1);
          else
            currDate = String(gps.date.month()) + "/" + String(gps.date.day());
          
          strength = gps.satellites.value();
          
          drawRightScreen();
        }
      }
    }
    
    if(millis() > 5000 && gps.charsProcessed() < 10) {
      gpsEnabled = false;
      drawLeftScreen();
      drawRightScreen();
      lscr.display();
    }
  }

  if(brakeState == HIGH && lblkState == LOW && rblkState == LOW && tailOn == false) {
    tailOn = true;
    brake();
  } else if(brakeState == LOW && lblkState == LOW && rblkState == LOW && tailOn == true) {
    tailOn = false;
    if(lightState == HIGH) {
      for(int x = 0; x < brakeNum; x++) {
        brakeLEDS[x] = LRED;
      }
    } else {
      for(int x = 0; x < brakeNum; x++) {
        brakeLEDS[x] = CLEAR;
      }
    }

    FastLED.show();
  }

  if(lblkState == HIGH) {
    drawLeftScreen();
    lblink();
    
  } else if(rblkState == HIGH) {
    drawLeftScreen();
    rblink();
  }

  if(lightState == HIGH && lblkState == LOW && rblkState == LOW && lightOn == false) {
    lightOn = true;
    frontLight();
    if(lblkState == LOW && rblkState == LOW && brakeState == LOW)
      tailLight();
    
  } else if(lightState == LOW && lblkState == LOW && rblkState == LOW && lightOn == true) {
    lightOn = false;
    for(int x = 0; x < frontNum; x++) {
      frontLEDS[x] = CLEAR;
    }
    
    for(int x = 0; x < brakeNum; x++) {
      brakeLEDS[x] = CLEAR;
    }
    
    FastLED.show();
  }

  if(menuState == LOW) {
    if(millis() - lastButtonPress > 50) {
      drawMenu(0);
    }
    lastButtonPress = millis();
  }

  if(millis() - lastDraw > 100 && menu == false) {
    drawLeftScreen();
    lastDraw = millis();
  }
}

void clr() {
  for(int x = 0; x < brakeNum; x++) {
    brakeLEDS[x] = CLEAR;
  }

  for(int x = 0; x < frontNum; x++) {
    frontLEDS[x] = CLEAR;
  }
  
  FastLED.show();
}

void frontLight() {
  for(int x = 0; x < frontNum; x++) {
    frontLEDS[x] = WHITE;
  }

  FastLED.show();
}

void tailLight() {
  for(int x = 0; x < brakeNum; x++) {
    brakeLEDS[x] = LRED;
  }

  FastLED.show();
}

void brake() {
  for(int i = 0; i < 12; i++) {
    brakeLEDS[i] = RED;
  }
  
  FastLED.show();
}

void lblink() {
  for(int i = 7; i > -2; i -= 1) {
    for(int x = 0; x < brakeNum; x++) {
      brakeLEDS[x] = CLEAR;
    }
    
    for(int x = 0; x < frontNum; x++) {
      frontLEDS[x] = CLEAR;
    }
  
    if(i < 6 && i != -1) {
      brakeLEDS[i] = ORANGE;
      brakeLEDS[11 - i] = ORANGE;
    }
    
    if(i > 0) {
      brakeLEDS[i - 1] = ORANGE;
      brakeLEDS[12 - i] = ORANGE;
    }

    if(i > 1) {
      brakeLEDS[i - 2] = ORANGE;
      brakeLEDS[13 - i] = ORANGE;
    }

    if(i == 5 || i == 4) {
      frontLEDS[0] = ORANGE;
      frontLEDS[7] = ORANGE;
      frontLEDS[8] = ORANGE;
    }

    if(i == 4 || i == 3) {
      frontLEDS[1] = ORANGE;
      frontLEDS[6] = ORANGE;
      frontLEDS[9] = ORANGE;
    }

    if(i == 3 || i == 2) {
      frontLEDS[2] = ORANGE;
      frontLEDS[5] = ORANGE;
      frontLEDS[10] = ORANGE;
    }

    if(i == 2 || i == 1) {
      frontLEDS[3] = ORANGE;
      frontLEDS[4] = ORANGE;
      frontLEDS[11] = ORANGE;
    }

    FastLED.show();
    delay(75);
  }

  lightOn = false;
  tailOn = false;
}

void rblink() {
  for(int i = 0; i < 9; i++) {
    for(int x = 0; x < brakeNum; x++) {
      brakeLEDS[x] = CLEAR;
    }

    for(int x = 0; x < frontNum; x++) {
      frontLEDS[x] = CLEAR;
    }
  
    if(i < 6) {
      brakeLEDS[i] = ORANGE;
      brakeLEDS[11 - i] = ORANGE;
    }
    
    if(i != 0 && i < 7) {
      brakeLEDS[i - 1] = ORANGE;
      brakeLEDS[12 - i] = ORANGE;
    }

    if(i > 1 && i != 8) {
      brakeLEDS[i - 2] = ORANGE;
      brakeLEDS[13 - i] = ORANGE;
    }

    if(i == 2 || i == 3) {
      frontLEDS[3] = ORANGE;
      frontLEDS[4] = ORANGE;
      frontLEDS[11] = ORANGE;
    }

    if(i == 3 || i == 4) {
      frontLEDS[2] = ORANGE;
      frontLEDS[5] = ORANGE;
      frontLEDS[10] = ORANGE;
    }

    if(i == 4 || i == 5) {
      frontLEDS[1] = ORANGE;
      frontLEDS[6] = ORANGE;
      frontLEDS[9] = ORANGE;
    }

    if(i == 5 || i == 6) {
      frontLEDS[0] = ORANGE;
      frontLEDS[7] = ORANGE;
      frontLEDS[8] = ORANGE;
    }

    FastLED.show();
    delay(75);
  }
  
  lightOn = false;
  tailOn = false;
  
}

void lowPowerMode() {
  lscr.clearDisplay();
  rscr.clearDisplay();
  lscr.display();
  rscr.display();
  frontLight();
  tailLight();
  
  while(true) {}
}

void loop() {
  if(menu == false) {
    checkData();
    
  } else  {
    currentStateCLK = digitalRead(CLK);
    menuState = digitalRead(SW);
    
    if(currentStateCLK != lastStateCLK  && currentStateCLK == 1) {
      if(digitalRead(DT) != currentStateCLK) {
        if(counter != 3)
          counter ++;
      } else {
        if(counter != 0)
          counter --;
      }
      
      drawMenu(counter);
    }
  
    lastStateCLK = currentStateCLK;

    if(menuState == LOW && counter == 0) {
      if(millis() - lastButtonPress > 50) {
        lowPowerMode();
      }
      lastButtonPress = millis();
      
    } else if(menuState == LOW && counter == 1) {
      if(millis() - lastButtonPress > 50) {
        wdt_disable();
        wdt_enable(WDTO_15MS);
      }
      lastButtonPress = millis();
      
    } else if(menuState == LOW && counter == 2) {
      if(millis() - lastButtonPress > 50) {
        if(EEPROM.read(0) == 0)
          EEPROM.write(0, 1);
        else
          EEPROM.write(0, 0);
        drawMenu(2);
      }
      lastButtonPress = millis();
      
    } else if(menuState == LOW && counter == 3) {
      if(millis() - lastButtonPress > 50) {
        drawMenu(-1);
      }
      lastButtonPress = millis();
    }

    delay(1);
  }
}

Custom parts and enclosures

Front Light Holder
May be different for other bike models / brands.
Front Light Diffuser
To be printed with white or clear filament, used as a cover for Front Light Holder.
Back Light Holder
May be different for other bike models / brands.
Back Light Diffuser
To be printed with white or clear filament, used as a cover for Back Light Holder.
Turn Signal Mount
May be different for other bike models / brands.
Alt. Horn Button Mount
May be different for other bike models / brands.
Brake Button Mount
May be different for other bike models / brands. May also change depending on how stiff the brake is.
Control Panel Left Front
May be different for other bike models / brands.
Control Panel Left Case
May be different for other bike models / brands.
Control Panel Right Front
May be different for other bike models / brands.
Control Panel Right Case
May be different for other bike models / brands.

Schematics

Main Schematic
Incomplete and is missing a couple of wires. Will update soon with updated wiring and more traditional layout.
Schematic bike 2021 03 23 13 44 14 stnidnihvz

Comments

Similar projects you might like

Electric Bike

Project tutorial by ifeghali

  • 5,011 views
  • 2 comments
  • 13 respects

Leduindo - Smart Led Bike Lightning

Project tutorial by Marius Dima

  • 6,868 views
  • 1 comment
  • 22 respects

An Alternate RFID Key for Bike Security

Project tutorial by Shubham Shinganapure

  • 6,907 views
  • 2 comments
  • 15 respects

Arduino Bike Blink Lights

Project showcase by Sam

  • 7,990 views
  • 3 comments
  • 15 respects

The "Tennis Ball" Garage Stop Light

Project tutorial by Stuart Mace

  • 11,952 views
  • 2 comments
  • 39 respects

Bike Route Data Gatherer

Project tutorial by Paul Trebilcox-Ruiz

  • 11,887 views
  • 0 comments
  • 39 respects
Add projectSign up / Login