Project in progress
Bicycle Odometer and Speedometer with 99 Lap/Period Recorder

Bicycle Odometer and Speedometer with 99 Lap/Period Recorder © GPL3+

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

  • 2,899 views
  • 8 comments
  • 11 respects

Components and supplies

About this project

Description

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.

Motivation

I cycle quite a bit to stay in shape and want to know how far I cycle, average and current speed, etc. I can not justify buying a device that does this (with probably many features I don't need!) so I decided to build this myself. I did not look to see if someone else created something similar as I wanted to challenge myself and stretch my newly acquired Arduino skills!

Operation

When the Arduino is initially powered on or reset, a "PRESS BUTTON TO START" message will appear on the 16 x 2 LCD screen.

Pressing either the Pause/Resume or Display Mode button will start the 1st lap/period. A "CYCLE SAFELY!" message will initially appear for a 2 second period during which recording is already ongoing (so wheel revolutions are already being counted). The display will then show the kilometers traveled, km/hr (to the right of the "S" on 1st line, S=Speed), elapsed time in hours, minutes and seconds (start of 2nd line) and average km/hr for lap/period (to the right of "A" on 2nd line, A=Average). This is updated in real-time.

A "+" will appear in upper-left-hand corner of the display for 250 milliseconds when a wheel revolution has been sensed.

Pressing the Display Mode button will change the "A" on 2nd line to an "M" indicating maximum km/hr traveled during the lap so far (M=Maximum).

Pressing the Pause/Resume button will stop recording and save the current lap data into memory. The message "PAUSED!" will appear for 2 seconds and then the totals for the lap just finished will appear with the lap number shown in the upper-left-hand corner of the display, followed by "Avg" showing average km/hr for entire lap duration, "Max" for maximum km/hr traveled. On the second line the distance in kilometers is shown followed by duration of lap in hours, minutes and seconds.

Pressing the Display Mode button when in pause mode will cycle through the different laps recorded. The first press will show the grand totals for all laps (with a "T" shown in upper-left-hand corner of the display), while subsequent presses will take you sequentially through lap 1, 2, 3, etc. depending on how many laps you recorded.

Pressing the Pause/Resume button again will put the device back into recording mode, recording a new lap. A "CYCLE SAFELY!" message will appear for 2 seconds first and then the real-time lap information explained previously will appear. If the Pause/Resume button is pressed again while the "CYCLE SAFELY!" message appears (so during initial 2 seconds of new lap), no lap data will be recorded and the device will be put back into pause mode showing the data for the last lap traveled.

Only data for 99 laps/periods can be recorded. Once the 99th lap is recorded, any additional laps will be recorded into the slot for the 99th lap thus overriding the previous data stored. However, grand totals will still get properly updated reflecting duration, distance, etc. for all laps, including those overridden in 99th slot.

The below video is a demo showing all of this functionality.

The software has been tested and debugged quite thoroughly in its current state. Hardware is work in progress still as I have yet to receive the Arduino Nano, Hall sensor, enclosure and mountable push buttons I have ordered.

Thanks for taking an interest in my project. All feedback is appreciated. Have a great day!

Code

Bicycle Odometer and SpeedometerArduino
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;
}

Schematics

Bicycle Odometer and Speedometer
Schematic 10l8ksmoay

Comments

Author

Alan dewindt
alan_dewindt
  • 2 projects
  • 10 followers

Additional contributors

  • Button "debounce" function taken from his great book "exploring arduino - tools and techniques for engineering wizardry". by Jeremy Blum

Published on

July 11, 2018

Members who respect this project

DefaultDudasertorioDefaultPhotoSpace pic backround 2 qgu7i4rm5n86d418fa 54f8 46fe a940 89f73db9af27 y9f5nuqc9oDefault

and 4 others

See similar projects
you might like

Similar projects you might like

Simple Arduino Digital Clock Without RTC

Project in progress by Annlee Fores

  • 99,675 views
  • 24 comments
  • 91 respects

Servo-Controlled, Light-Tracking Solar Panel Platform

Project tutorial by scott mangiacotti

  • 13,575 views
  • 10 comments
  • 45 respects

Simple Arduino Optical Chopper

Project tutorial by Brendan Sweeny

  • 2,086 views
  • 0 comments
  • 4 respects

The Hooray Machine

Project showcase by Dawn Dupriest and Brie Hen

  • 768 views
  • 1 comment
  • 5 respects

Portable Arduino Temp/Humidity Sensor with LCD

Project tutorial by ThothLoki

  • 153,291 views
  • 115 comments
  • 185 respects

Electronic Piano Keyboard With Preset Songs

Project tutorial by Lindsay Fox

  • 77,975 views
  • 60 comments
  • 150 respects
Add projectSign up / Login