// ---------------------------------------------------------------------------------------------------------------------
// Bicycle Odometer & Speedometer
// Written by Alan De Windt for the Arduino Uno
// [email protected]
// July 2018
//
// Hardware requirements:
// * 16 x 2 character LCD screen found in Arduino Starter Kit
// * Push button for pause/resume (on digital pin 2)
// * Push button for cycling (no pun intended!) through display modes (on digital pin 3)
// * Hall sensor which should be attached to bicycle wheel to sense when wheel has made a revolution (on digital pin 4)
//
// Noteworthy features:
// * Computes time traveled (in hours, minutes and seconds), distance in kilometers, average kilometers per hour for
// entire lap/period, average kilometers per hour during last minute, maximum kilometers per hour cycled during
// lap/period
// * Stores up to 99 laps/periods which can be viewed when in pause mode by pressing the Display Mode button
// NOTE: 100th lap gets recorded in position for lap 99 thus overriding data for 99th lap
// * Computes total time, kilometers traveled, average kilometers per hour and maximum kilometers per hour cycled for
// all laps/periods recorded (shown in "T" data when looking at lap data in pause mode)
// * No data is being recorded while in pause mode
// * Safety features:
// - "CYCLE SAFELY!" message appearing at start of every lap
// - Not possible to cycle through different display modes while lap is ongoing to minimize risk
// of cyclist "toying around/being distracted". Safety on the roads is paramount, so cycle safely!!!
//
// See the following YouTube video for demonstration and additional explanations:
// https://youtu.be/31X-BA0ff4o
//
// NOTE: You should calculate exact circumference of bicycle wheel (in meters) and update value initialized below
// in bicycleWheelCircumference
// ---------------------------------------------------------------------------------------------------------------------
#include <LiquidCrystal.h>
LiquidCrystal lcd(12, 11, 10, 9, 8, 7);
// Circumference of bicycle wheel expressed in meters
float bicycleWheelCircumference = 2.1206;
const int pauseButton = 2;
boolean lastPauseButton = LOW;
boolean currentPauseButton = LOW;
const int displayModeButton = 3;
boolean lastDisplayModeButton = LOW;
boolean currentDisplayModeButton = LOW;
const int revolutionButton = 4;
boolean lastRevolutionButton = LOW;
boolean currentRevolutionButton = LOW;
boolean startShown = HIGH;
boolean paused = LOW;
boolean pausedShown = LOW;
unsigned long pausedStartTime = 0;
boolean wheelTurningShown = LOW;
unsigned long wheelTurningStartTime = 0;
boolean cycleSafelyShown = LOW;
unsigned long cycleSafelyStartTime = 0;
unsigned long lastRevolutionStartTime = 0;
unsigned long revolutionTime = 0;
int currentDisplayMode = 0;
int showLap = 0;
int lapCurrentlyShown = 100;
int currentLap = 0;
float currentDistance;
unsigned long currentDuration;
int currentMaximumKPH;
int currentAverageKPH;
int currentKPH;
float arrayDistance[100];
unsigned long arrayDuration[100];
int arrayMaximumKPH[100];
int arrayAverageKPH[100];
unsigned long revolutionCount = 0;
unsigned long currentTime = 0;
unsigned long lapStartTime = 0;
float km = 0.00;
float kph = 0.00;
int intHours;
int intMinutes;
int intSeconds;
unsigned long milliSecondsInSecond = 1000;
unsigned long milliSecondsInMinute = 60000;
unsigned long milliSecondsInHour = 3600000;
void setup()
{
// Configure digital input pins for push buttons and Hall sensor
pinMode (revolutionButton, INPUT);
pinMode (pauseButton, INPUT_PULLUP);
pinMode (displayModeButton, INPUT_PULLUP);
// Initialize maximum KPH in totals as this may not be calculated if no maximum was computed for laps
// and there may be random data in memory location
arrayMaximumKPH[0] = 0;
// Initialize LCD screen & show "PRESS BUTTON TO START"
lcd.begin(16, 2);
lcd.clear();
lcd.setCursor(2, 0);
lcd.print("PRESS BUTTON");
lcd.setCursor(4, 1);
lcd.print("TO START");
}
void loop() {
// Get current millis
currentTime = millis();
// Read revolution Hall sensor
currentRevolutionButton = debounce(lastRevolutionButton, revolutionButton);
if (lastRevolutionButton == HIGH && currentRevolutionButton == LOW) {
// If initial "PRESS BUTTON TO START" is not displayed and not currently paused...
if (!startShown && !paused) {
// Increase wheel revolution count
revolutionCount++;
// Display "+" to show that one revolution was recorded
lcd.setCursor(0, 0);
lcd.print("+");
wheelTurningShown = HIGH;
wheelTurningStartTime = currentTime;
// Compute millis it took for this latest revolution
if (lastRevolutionStartTime > 0) {
revolutionTime = currentTime - lastRevolutionStartTime;
// Compute current speed in kilometers per hour based on time it took to complete last wheel revolution
kph = (3600000 / revolutionTime) * bicycleWheelCircumference / 1000;
currentKPH = kph;
// If current speed is new maximum speed for this lap then store it
if (currentMaximumKPH < currentKPH) {
currentMaximumKPH = currentKPH;
}
}
lastRevolutionStartTime = currentTime;
}
}
lastRevolutionButton = currentRevolutionButton;
// Read PAUSE/RESUME push button
currentPauseButton = debounce(lastPauseButton, pauseButton);
if (lastPauseButton == LOW && currentPauseButton == HIGH) {
// If "PRESS BUTTON TO START" message has been showing then we now need to start 1st lap/period
if (startShown) {
startShown = LOW;
// Show "CYCLE SAFELY!" message
showCycleSafely();
cycleSafelyShown = HIGH;
cycleSafelyStartTime = currentTime;
currentLap = 1;
resetLapVariables();
currentDisplayMode = 1;
}
else {
// Otherwise if pause is active then we need to take it out of pause and start new lap/period
if (paused) {
paused = LOW;
// Show "CYCLE SAFELY!" message
showCycleSafely();
cycleSafelyShown = HIGH;
cycleSafelyStartTime = currentTime;
// Increment lap counter
currentLap++;
// If we are starting a 100th lap/period then we should write data into 99th array position (overwriting this lap)
// as we can only keep track of 99 laps/periods in total
if (currentLap > 99) {
currentLap = 99;
// Pretend lap 100 (out-of-bounds value) is currently shown (even though 99 is currently shown)
// to force display of new data for lap 99
lapCurrentlyShown = 100;
}
resetLapVariables();
currentDisplayMode = 1;
}
// Otherwise pause is not currently active so we need to save lap/period data and activate pause
else {
paused = HIGH;
// Calculate duration
currentDuration = currentTime - lapStartTime;
// If lap duration is less than 2 seconds (which means user pressed the pause button while "CYCLE SAFELY!" message
// was shown) then do not store the lap/ignore it
if (currentDuration < 2000) {
currentLap--;
}
// Otherwise store the lap
else {
// Compute distance and average kilometers per hour if bicycle moved
if (revolutionCount > 0) {
currentDistance = revolutionCount * bicycleWheelCircumference / 1000;
currentAverageKPH = currentDistance * 3600000 / currentDuration;
}
// Store data for lap/period into array
arrayDistance[currentLap] = currentDistance;
arrayDuration[currentLap] = currentDuration;
arrayAverageKPH[currentLap] = currentAverageKPH;
arrayMaximumKPH[currentLap] = currentMaximumKPH;
// Update totals for all laps/periods
arrayDistance[0] = arrayDistance[0] + currentDistance;
arrayDuration[0] = arrayDuration[0] + currentDuration;
arrayAverageKPH[0] = arrayDistance[0] * 3600000 / arrayDuration[0];
if (currentMaximumKPH > arrayMaximumKPH[0]) {
arrayMaximumKPH[0] = currentMaximumKPH;
}
}
// In case "CYCLE SAFELY!" has been showing, turn it off now since we want to show "PAUSED!" message
// and we don't want it to be removed when "CYCLE SAFELY!" times out
cycleSafelyShown = LOW;
// Show "PAUSED!" message
showPaused();
pausedShown = HIGH;
pausedStartTime = currentTime;
// We will need to show data for lap which was just finished
showLap = currentLap;
currentDisplayMode = 3;
// Set out-of-bounds value to lapCurrentlyShown to force lap data to be shown
lapCurrentlyShown = 100;
}
}
}
lastPauseButton = currentPauseButton;
// Read DISPLAY MODE push button
currentDisplayModeButton = debounce(lastDisplayModeButton, displayModeButton);
if (lastDisplayModeButton == LOW && currentDisplayModeButton == HIGH) {
// If "PRESS BUTTON TO START" message has been showing then we now need to start 1st lap/period
if (startShown) {
startShown = LOW;
// Show "CYCLE SAFELY!" message
showCycleSafely();
cycleSafelyShown = HIGH;
cycleSafelyStartTime = currentTime;
currentLap = 1;
resetLapVariables();
currentDisplayMode = 1;
}
else {
// Otherwise if "CYCLE SAFELY!" message is not shown nor is "PAUSED!" message shown...
if (!cycleSafelyShown && !pausedShown) {
// If not currently paused (so lap is ongoing)...
if (!paused) {
// Flip between the two different display modes available
if (currentDisplayMode == 1) {
currentDisplayMode = 2;
}
else {
currentDisplayMode = 1;
}
// Clear display and show appropriate labels
showLabels(currentDisplayMode);
}
// Otherwise we are in paused mode so cycle through lap data available, including totals page
else {
currentDisplayMode = 3;
showLap++;
if (showLap > currentLap) {
showLap = 0; // Show totals
}
}
}
}
}
lastDisplayModeButton = currentDisplayModeButton;
// If wheel revolution indicator has been showing, take if off if it has been 250 millis or more
if (wheelTurningShown && !startShown && !paused && (currentTime >= (wheelTurningStartTime + 250))) {
wheelTurningShown = LOW;
lcd.setCursor(0, 0);
lcd.print(" ");
}
// If wheel revolution indicator has been showing, take if off if it has been 250 millis or more
if (!startShown && !paused && (currentTime >= (lastRevolutionStartTime + 10000)) && currentKPH > 0) {
currentKPH = 0;
}
// If "Cycle Safely!" has been showing, take it off if it has been 2 seconds or more
if (cycleSafelyShown && (currentTime >= (cycleSafelyStartTime + 2000))) {
cycleSafelyShown = LOW;
showLabels(currentDisplayMode);
}
// If "Paused!" has been showing, take it off if it has been 2 seconds or more
if (pausedShown && (currentTime >= (pausedStartTime + 2000))) {
pausedShown = LOW;
showLabels(currentDisplayMode);
}
// If "PUSH BUTTON TO START" is not showing and not currently paused...
if (!startShown && !paused) {
// Compute milliseconds since start of lap
currentDuration = currentTime - lapStartTime;
// Compute distance and average kilometers per hour if bicycle has moved
if (revolutionCount > 0) {
// Compute kilometers traveled
// Circumference of wheel is in meters
currentDistance = revolutionCount * bicycleWheelCircumference / 1000;
// Compute average kilometers per hour since start of lap
currentAverageKPH = currentDistance * 3600000 / currentDuration;
}
}
// If no messages are currently showing then update data on display
if (!startShown && !cycleSafelyShown && !pausedShown) {
if (currentDisplayMode < 3) {
lcd.setCursor(1, 0);
lcd.print(currentDistance);
lcd.print(" km");
lcd.setCursor(14, 0);
if (currentKPH < 10) {
lcd.print(" ");
}
lcd.print(currentKPH);
computeHMS(currentDuration);
lcd.setCursor(1, 1);
if (intHours < 10) {
lcd.print("0");
}
lcd.print(intHours);
lcd.print(":");
if (intMinutes < 10) {
lcd.print("0");
}
lcd.print(intMinutes);
lcd.print(":");
if (intSeconds < 10) {
lcd.print("0");
}
lcd.print(intSeconds);
lcd.setCursor(12, 1);
lcd.print("A");
if (currentDisplayMode == 1) {
lcd.setCursor(12, 1);
lcd.print("A");
lcd.setCursor(14, 1);
if (currentAverageKPH < 10) {
lcd.print(" ");
}
lcd.print(currentAverageKPH);
}
else {
lcd.setCursor(12, 1);
lcd.print("M");
lcd.setCursor(14, 1);
if (currentMaximumKPH < 10) {
lcd.print(" ");
}
lcd.print(currentMaximumKPH);
}
}
// Otherwise device is paused so show historical lap information
else {
// Update display only if we need to show data for different lap to that currently shown
// this way display is not constantly cleared and refreshed with same data which would
// cause display to flicker and is not needed anyway as data is not changing
if (lapCurrentlyShown != showLap) {
lapCurrentlyShown = showLap;
lcd.clear();
lcd.setCursor(0, 0);
if (showLap == 0) {
lcd.print("T ");
}
else {
lcd.print(showLap);
}
lcd.setCursor(3, 0);
lcd.print("Avg");
lcd.setCursor(7, 0);
lcd.print(arrayAverageKPH[showLap]);
if (arrayAverageKPH[showLap] < 10) {
lcd.print(" ");
}
lcd.setCursor(10, 0);
lcd.print("Max");
lcd.setCursor(14, 0);
lcd.print(arrayMaximumKPH[showLap]);
if (arrayMaximumKPH[showLap] < 10) {
lcd.print(" ");
}
lcd.setCursor(0, 1);
lcd.print(" ");
lcd.setCursor(0, 1);
lcd.print(arrayDistance[showLap]);
computeHMS(arrayDuration[showLap]);
lcd.setCursor(8, 1);
if (intHours < 10) {
lcd.print("0");
}
lcd.print(intHours);
lcd.print(":");
if (intMinutes < 10) {
lcd.print("0");
}
lcd.print(intMinutes);
lcd.print(":");
if (intSeconds < 10) {
lcd.print("0");
}
lcd.print(intSeconds);
}
}
}
}
// Compute hours, minutes and seconds for given duration expressed in milliseconds
void computeHMS(unsigned long duration) {
float floatHours;
float floatMinutes;
float floatSeconds;
intHours = 0;
intMinutes = 0;
intSeconds = 0;
if (duration >= 1000) {
floatSeconds = duration / milliSecondsInSecond % 60;
intSeconds = floatSeconds;
floatMinutes = duration / milliSecondsInMinute % 60;
intMinutes = floatMinutes;
floatHours = duration / milliSecondsInHour % 24;
intHours = floatHours;
}
}
// Reset all variables used for calculating current/ongoing lap
void resetLapVariables() {
revolutionCount = 0;
lapStartTime = currentTime;
currentDistance = 0;
currentDuration = 0;
currentMaximumKPH = 0;
currentAverageKPH = 0;
}
// Show "CYCLE SAFELY!"
void showCycleSafely() {
lcd.clear();
lcd.setCursor(5, 0);
lcd.print("CYCLE");
lcd.setCursor(4, 1);
lcd.print("SAFELY!");
}
// Show "PAUSED!"
void showPaused() {
lcd.clear();
lcd.setCursor(4, 0);
lcd.print("PAUSED!");
}
// Show appropriate labels for current mode
void showLabels(int currentDisplayMode) {
lcd.clear();
switch (currentDisplayMode) {
case 1:
lcd.setCursor(12, 0);
lcd.print("S");
lcd.setCursor(12, 1);
lcd.print("A");
break;
case 2:
lcd.setCursor(12, 0);
lcd.print("S");
lcd.setCursor(12, 1);
lcd.print("M");
break;
}
}
//A debouncing function that can be used for any button
boolean debounce(boolean last, int pin)
{
boolean current = digitalRead(pin);
if (last != current) {
delay(5);
current = digitalRead(pin);
}
return current;
}