Hackster is hosting Hackster Holidays, Ep. 6: Livestream & Giveaway Drawing. Watch previous episodes or stream live on Monday!Stream Hackster Holidays, Ep. 6 on Monday!
Alan De Windt
Published © GPL3+

Bicycle Odometer and Speedometer with 99 Lap/Period Recorder

Bicycle odometer and speedometer showing distance traveled, average and current speed (in km/hr), time and can store up to 99 laps.

BeginnerWork in progress34,161
Bicycle Odometer and Speedometer with 99 Lap/Period Recorder

Things used in this project

Story

Read more

Schematics

Bicycle Odometer and Speedometer

Code

Bicycle Odometer and Speedometer

Arduino
This is an odometer and speedometer for bicycles which keeps track of distance traveled (in km), time traveled in hours, minutes and seconds, average speed (in km/hr) and maximum speed attained during any one minute period (in km/hr). It can keep track of a maximum 99 laps/cycling periods. It can be built with parts from the Arduino Starter Kit, with the exception of a Hall sensor and enclosure box which need to be purchased separately.
//  ---------------------------------------------------------------------------------------------------------------------
//  Bicycle Odometer & Speedometer
//  Written by Alan De Windt for the Arduino Uno
//  alan_dewindt@yahoo.com
//  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, 8, 7, 6, 5);

// 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);
  pinMode (displayModeButton, INPUT);

  // 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;
}

Credits

Alan De Windt

Alan De Windt

4 projects • 23 followers
Currently a Business Analyst and UI/UX Designer. Started career as a developer, still coding but as a hobby only.

Comments