Project in progress
DIY GPS Speedcoach

DIY GPS Speedcoach © GPL3+

Designed for crew rowing, this speedcoach is designed to track split times and total meters traveled.

  • 1,389 views
  • 2 comments
  • 4 respects

Components and supplies

Necessary tools and machines

09507 01
Soldering iron (generic)

About this project

NK Brand speedcoaches, while small, light, and effective, are often in excess of $400, and sometimes affordable for individual coxswains or clubs who spend their money on boats/boat repairs, and oars. For my personal project in high school, I decided to solve that problem by making my own speedcoach, potentially as the basis for future rowing tech. Features include - support for biking and walking, as well as other water sports, a timer, and a battery monitor.

Version 2.0.0 Updates -

- Maxing out the 10 Hz GPS for a whopping 10 data points a second.

- Split averaging optimized for 20 spm to stop jumpy split readings. This results in an excellent 30 data points per split reading displayed!

- Datalogging! Simply plug in a microSD card to the board (Feather Adalogger M0 in my case), flip the data write switch, and you'll get split, meters, and time recordings every 3 seconds, each with a handy timestamp.

- General aesthetics/functionality improvements centered around the user interface. Because, who doesn't love a pretty and useful UI?

Usage-

To start a piece, simply hit the reset button, and after a 3 second delay, the screen should turn on and the piece will start as soon as the screen is lit. To record data, flip the data write permission switch on.

Code

Code (GPSSP)Arduino
Main code for the speedcoach.
#include <SPI.h>
#include <SandTimer.h>
#include <SD.h>
#include <millisDelay.h>
#include <Wire.h>
#include "SSD1306Ascii.h"
#include "SSD1306AsciiWire.h"
#include <Adafruit_GPS.h>   //Libraries
#define GPSSerial Serial1
Adafruit_GPS GPS(&GPSSerial);
File myFile;
SandTimer timer1;
#define GPSECHO false
#define I2C_ADDRESS 0x3C
#define RST_PIN -1//Oled reset pin (onboard reset)
uint32_t timer = millis();
const int chipSelect = 4;
float ms = 0; //Meters/Second variable
float avems = 0;  //Average M/S variable
float mscounter = 0;  //Speed total (for average)
int integcounter = 0; //Counts half-seconds
float meterstraveled = 0; //Total meters
int splittotal = 0;   //Split total (in seconds)  (Essential to be an integer)
int splitseconds = 0; //Seconds on split display  (Essential to be an integer)
int splitminutes = 0; //Minutes on split display  (Essential to be an integer)
int seconds = 0;
int minutes = 0;
int hours = 0;    //Self-explanatory (for timers)
int splitaveragetotal = 0; //All split seconds added together (for average split)
int splitaverageseconds = 0; //Split average seconds
int splitaverageminutes = 0; //Split average minutes
int splitaverage = 0; //Split average
int splitaveragesecondsdisp = 0; //Display variables, fixes a bug
int splitaverageminutesdisp = 0; 
int splitmdisp = 0; 
int splitsdisp = 0;  //Display Variables
int tc = 0; //Counter variable
int dp = 0; //Data-point variable
int dpave = 0; //Average of the data-points
int dprec = 0; //Counter variable
float knot = 0;//Speed in knots recorded from GPS
float aveknots = 0;  //Speed in knots averaged over 1/2 second (equal to 1 data point and 5 knot readings)
int knotcounter = 0; //Counter variable
int sensorValue = analogRead(A3); //For the data write switch
float msfm = 0; //M/S for the meters calculation
float checkBattery(){
  //Function to check battery voltage
  float measuredvbat = analogRead(9);
  measuredvbat *= 2;    // we divided by 2, so multiply back
  measuredvbat *= 3.3;  // Multiply by 3.3V, our reference voltage
  measuredvbat /= 1024; // convert to voltage

  
  return measuredvbat;
}
SSD1306AsciiWire oled;  //Oled type declaration
millisDelay secdelay;   //Timing delay (for timer)



void setup() {
  float voltage = sensorValue * (5.0 / 1023.0);   //Converts A3 reading into a 5v equivalent
  Wire.begin();
  Wire.setClock(400000L);
   Serial.begin(115200);
    GPS.begin(9600);
GPS.sendCommand(PMTK_SET_NMEA_OUTPUT_RMCGGA);   
#if RST_PIN >= 0
GPS.sendCommand(PGCMD_ANTENNA);   
delay(1000);    //GPS initialization
  oled.begin(&Adafruit128x64, I2C_ADDRESS, RST_PIN);
#else // RST_PIN >= 0
  oled.begin(&Adafruit128x64, I2C_ADDRESS);
#endif // RST_PIN >= 0  //Oled initialization

  oled.setFont(TimesNewRoman16_bold);  //Standard font
GPSSerial.println(PMTK_Q_RELEASE);
int realhours = GPS.hour - 8;
if(realhours < 0) {realhours = realhours+24;} //For SD timestamp (24 hour format)
if (voltage == 5) {if (!SD.begin(chipSelect)) { //Begins SD card startup
    Serial.println("Card failed, or not present");
    }
  }
  uint32_t m = micros();  //Main timer initialization
  oled.clear();       //Initializing animation
  oled.set1X();
  oled.println("GPSSP V. 2.0.0");
  oled.print("Initializing");
  oled.println(); 
  oled.println(); 
  oled.print("RTI Development");
  delay(500);
  oled.clear();
  oled.println("GPSSP V. 2.0.0");
  oled.print("Initializing.");
  oled.println(); 
  oled.println(); 
  oled.print("RTI Development");
  delay(500);
  oled.clear();
  oled.println("GPSSP V. 2.0.0");
  oled.print("Initializing..");
  oled.println(); 
  oled.println(); 
  oled.print("RTI Development");
  delay(500);
  oled.clear();
  oled.println("GPSSP V. 2.0.0");
  oled.print("Initializing...");
  oled.println(); 
  oled.println(); 
  oled.print("RTI Development");
  delay(500);
  oled.clear();
  oled.println("GPSSP V. 2.0.0");
  oled.print("Initializing..");
  oled.println(); 
  oled.println(); 
  oled.print("RTI Development");
  delay(500);
  oled.clear();
  oled.println("GPSSP V. 2.0.0");
  oled.print("Initializing.");
  oled.println(); 
  oled.println(); 
  oled.print("RTI Development");
  delay(500);
  oled.clear();
  oled.println("GPSSP V. 2.0.0");
  oled.print("Initializing");
  oled.println(); 
  oled.println(); 
  oled.print("RTI Development");
  delay(500);
  secdelay.start(1000); //Start secondary timer delay (1 Second)
  oled.clear();
  oled.println("NO GPS FIX");   //GPS Fix Error
  oled.print("RTI Development");
  File dataFile = SD.open("storage.txt", FILE_WRITE);   //Opens SD card, prints initial piece introduction + timestamp
   if (dataFile) {
    Serial.println("Writing to SD");
    dataFile.println("PIECE BEGIN - - -");
    dataFile.print(realhours);
    dataFile.print("/");
    dataFile.println(GPS.minute);
    dataFile.close();}
    timer1.start(100);
}

void loop() {

  sensorValue = analogRead(A3);
  float voltage = sensorValue * (5.0 / 1023.0);

  float batVoltage = checkBattery();  //get battery voltage
  if (secdelay.justFinished()) {seconds++; secdelay.repeat();} //Checks if 1 second has passed, updates seconds variable
  if (seconds == 60) {seconds = 0; minutes++;}                 //Checks if 60 seconds have passed, resets seconds, increases minutes
  if (minutes == 60) {minutes = 0; hours++;}                   //Checks if 60 minutes have passed, resets minutes, increases hours
  char c = GPS.read();
  if (GPSECHO)
    if (c) Serial.print(c);
  if (GPS.newNMEAreceived()) {
    Serial.println(GPS.lastNMEA()); 
    if (!GPS.parse(GPS.lastNMEA()));  //Serial GPS info for debugging
  }
 
  if (timer > millis()) timer = millis();   //Restarts main timer
  
    
    if (timer1.finished()) {knot = knot+GPS.speed; knotcounter++;
    timer1.startOver();} 
  
  if (millis() - timer > 500) {   //Runs every half-second (responsible for screen-flicker)
    timer = millis(); // Resets timer
     if (GPS.fix) {
      oled.clear();
      aveknots = knot/knotcounter;
      Serial.println(aveknots);
      ms = (aveknots*0.5144444); //Converts knots to meters/second
      Serial.println(ms);
      knot = 0;
      tc = 0;
      knotcounter = 0;
      aveknots = 0;
      if (ms <= 0.75) {oled.clear();
                       oled.println("Error: IDLE");
                       oled.print("Time: "); oled.print(hours); oled.print(":"); oled.print(minutes); oled.print(":"); oled.println(seconds);
                       oled.print("Battery: ");
                       oled.println(batVoltage);
                       oled.print("Meters: ");
                       oled.print(meterstraveled); }//Idle screen 
                   
                   
      if (ms > 0.75) 
      {
       
      msfm = ms;
      mscounter = (mscounter+msfm);  //Continually adds speed to total (for averages)
      integcounter = (integcounter+1); //Adds +1 to variable every half second (for averages)
      avems = mscounter/integcounter; //Generates average meters/second (for total)
      meterstraveled = avems*(integcounter/2); //Calculates total (meters/second divided by seconds)
      splittotal = 500/(ms); //Generates seconds per 500m
      dp = dp + splittotal;  //Adds data-points for dpave variable
      dprec++;
      if (dprec >= 6) {dpave = dp/6; //Calculates average
                       dprec = 0; 
                       dp = 0; //Resets data-points and counter variable
                       if(voltage > 4.5) {File dataFile = SD.open("storage.txt", FILE_WRITE);
                       //Opens SD card if write switch is on

  if (dataFile) {
    Serial.println("Writing to SD");
    dataFile.println("S: ");
    dataFile.print(splitminutes);
    dataFile.print(":");
    dataFile.println(splitseconds);
    dataFile.print("Ave. S ");
    dataFile.print(splitaverageminutes); 
    dataFile.print(":");
    dataFile.println(splitaverageseconds);
    dataFile.print("M: ");
    dataFile.println(meterstraveled);
    dataFile.print(hours);
    dataFile.print(":");
    dataFile.print(minutes);
    dataFile.print(":");
    dataFile.println(seconds);
    dataFile.println();
    dataFile.close();}}   //Prints data to SD card
    }
      splitminutes = dpave/60; //Generates split minutes from split seconds 
      splitseconds = dpave - (splitminutes*60); //Displays remaining seconds
      splitaveragetotal = splitaveragetotal+splittotal;   //All split times (in seconds) added together
      splitaverage = splitaveragetotal/(integcounter);  //Averages out split times w/ seconds
      splitaverageminutes = splitaverage/60;   //Calculates split minutes
      splitaverageseconds = splitaverage-splitaverageminutes*60;   //Calculates split seconds
      splitaverageminutesdisp = splitaverageminutes;
      splitaveragesecondsdisp = splitaverageseconds;
      splitmdisp = splitminutes;
      splitsdisp = splitseconds;      //Display variables
      oled.set1X();  //Sets text size
      oled.print("M: "); oled.println(meterstraveled);  //Displays total meters
      oled.print("S: "); oled.print(splitmdisp); oled.print(":"); oled.print(splitsdisp);  oled.print("   AS: "); oled.print(splitaverageminutesdisp); oled.print(":");  oled.println(splitaveragesecondsdisp);
        //Displays split and average split
      oled.print("T: "); oled.print(hours); oled.print(":"); oled.print(minutes); oled.print(":"); oled.println(seconds); //Displays timer
      if (voltage >= 4.5) {oled.print("SD Write ON");}
      if (voltage < 4.5) {oled.print("SD Write OFF");} //Shows write permission status
      }}}}
For SSD1309Arduino
For u/tomsharpe.
#include <SPI.h>
#include <SandTimer.h>
#include <SD.h>
#include <millisDelay.h>
#include <Adafruit_GPS.h>   //Libraries
#define GPSSerial Serial1
Adafruit_GPS GPS(&GPSSerial);
File myFile;
SandTimer timer1;
#define GPSECHO false
#include <Arduino.h>
#include <U8g2lib.h>
#ifdef U8X8_HAVE_HW_SPI
#include <SPI.h>
#endif
#ifdef U8X8_HAVE_HW_I2C
#include <Wire.h>
#endif
U8G2_SSD1309_128X64_NONAME0_F_4W_SW_SPI u8g2(U8G2_R0, /* clock=*/ 13, /* data=*/ 11, /* cs=*/ 10, /* dc=*/ 9, /* reset=*/ 8);
uint32_t timer = millis();
const int chipSelect = 4;
float ms = 0; //Meters/Second variable
float avems = 0;  //Average M/S variable
float mscounter = 0;  //Speed total (for average)
int integcounter = 0; //Counts half-seconds
float meterstraveled = 0; //Total meters
int splittotal = 0;   //Split total (in seconds)  (Essential to be an integer)
int splitseconds = 0; //Seconds on split display  (Essential to be an integer)
int splitminutes = 0; //Minutes on split display  (Essential to be an integer)
int seconds = 0;
int minutes = 0;
int hours = 0;    //Self-explanatory (for timers)
int splitaveragetotal = 0; //All split seconds added together (for average split)
int splitaverageseconds = 0; //Split average seconds
int splitaverageminutes = 0; //Split average minutes
int splitaverage = 0; //Split average
int splitaveragesecondsdisp = 0; //Display variables, fixes a bug
int splitaverageminutesdisp = 0; 
float splitmdisp = 0; 
float splitsdisp = 0;  //Display Variables
int tc = 0; //Counter variable
int dp = 0; //Data-point variable
int dpave = 0; //Average of the data-points
int dprec = 0; //Counter variable
float knot = 0;//Speed in knots recorded from GPS
float aveknots = 0;  //Speed in knots averaged over 1/2 second (equal to 1 data point and 5 knot readings)
int knotcounter = 0; //Counter variable
int sensorValue = analogRead(A3); //For the data write switch
float msfm = 0; //M/S for the meters calculation
float checkBattery(){
  //Function to check battery voltage
  float measuredvbat = analogRead(9);
  measuredvbat *= 2;    // we divided by 2, so multiply back
  measuredvbat *= 3.3;  // Multiply by 3.3V, our reference voltage
  measuredvbat /= 1024; // convert to voltage

  
  return measuredvbat;
}
millisDelay secdelay;   //Timing delay (for timer)



void setup() {
  float voltage = sensorValue * (5.0 / 1023.0);   //Converts A3 reading into a 5v equivalent
  Wire.begin();
  Wire.setClock(400000L);
   Serial.begin(115200);
    GPS.begin(9600);
GPS.sendCommand(PMTK_SET_NMEA_OUTPUT_RMCGGA);   
GPS.sendCommand(PGCMD_ANTENNA);   
delay(1000);    //GPS initialization
u8g2.begin();
GPSSerial.println(PMTK_Q_RELEASE);
int realhours = GPS.hour - 8;
if(realhours < 0) {realhours = realhours+24;} //For SD timestamp (24 hour format)
if (voltage == 5) {if (!SD.begin(chipSelect)) { //Begins SD card startup
    Serial.println("Card failed, or not present");
    }
  }
  uint32_t m = micros();  //Main timer initialization
  u8g2.clearBuffer();          // clear the internal memory
  u8g2.setFont(u8g2_font_ncenB08_tr); // choose a suitable font
  u8g2.drawStr(0,0,"Initializing...");  // write something to the internal memory
  u8g2.sendBuffer();          // transfer internal memory to the display
  delay(3000);  
  secdelay.start(1000); //Start secondary timer delay (1 Second)
  u8g2.clearBuffer();          // clear the internal memory
  
  File dataFile = SD.open("storage.txt", FILE_WRITE);   //Opens SD card, prints initial piece introduction + timestamp
   if (dataFile) {
    Serial.println("Writing to SD");
    dataFile.println("PIECE BEGIN - - -");
    dataFile.print(realhours);
    dataFile.print("/");
    dataFile.println(GPS.minute);
    dataFile.close();}
    timer1.start(100);
}

void loop() {

  sensorValue = analogRead(A3);
  float voltage = sensorValue * (5.0 / 1023.0);

  float batVoltage = checkBattery();  //get battery voltage
  if (secdelay.justFinished()) {seconds++; secdelay.repeat();} //Checks if 1 second has passed, updates seconds variable
  if (seconds == 60) {seconds = 0; minutes++;}                 //Checks if 60 seconds have passed, resets seconds, increases minutes
  if (minutes == 60) {minutes = 0; hours++;}                   //Checks if 60 minutes have passed, resets minutes, increases hours
  char c = GPS.read();
  if (GPSECHO)
    if (c) Serial.print(c);
  if (GPS.newNMEAreceived()) {
    Serial.println(GPS.lastNMEA()); 
    if (!GPS.parse(GPS.lastNMEA()));  //Serial GPS info for debugging
  }
 
  if (timer > millis()) timer = millis();   //Restarts main timer
  
    
    if (timer1.finished()) {knot = knot+GPS.speed; knotcounter++;
    timer1.startOver();} 
  
  if (millis() - timer > 500) {   //Runs every half-second (responsible for screen-flicker)
    timer = millis(); // Resets timer
     if (GPS.fix) {
      
      aveknots = knot/knotcounter;
      Serial.println(aveknots);
      ms = (aveknots*0.5144444); //Converts knots to meters/second
      Serial.println(ms);
      knot = 0;
      tc = 0;
      knotcounter = 0;
      aveknots = 0;
      if (ms <= 0.75) {u8g2.clearBuffer();         // clear the internal memory
                       u8g2.setFont(u8g2_font_ncenB08_tr); // choose a suitable font
                       u8g2.drawStr(0,10,"Error: IDLE");  // write something to the internal memory
                       u8g2.drawStr(1,10,"Battery: ");
                       u8g2.print(1,80,batVoltage);
                       u8g2.drawStr(2,10,"Meters:");
                       u8g2.print(2,80,meterstraveled);
                       u8g2.sendBuffer();          // transfer internal memory to the display  
        
        
        
                        
                   
                   
      if (ms > 0.75) 
      {
       
      msfm = ms;
      mscounter = (mscounter+msfm);  //Continually adds speed to total (for averages)
      integcounter = (integcounter+1); //Adds +1 to variable every half second (for averages)
      avems = mscounter/integcounter; //Generates average meters/second (for total)
      meterstraveled = avems*(integcounter/2); //Calculates total (meters/second divided by seconds)
      meterstraveled = round(meterstraveled);
      splittotal = 500/(ms); //Generates seconds per 500m
      dp = dp + splittotal;  //Adds data-points for dpave variable
      dprec++;
      if (dprec >= 6) {dpave = dp/6; //Calculates average
                       dprec = 0; 
                       dp = 0; //Resets data-points and counter variable
                       if(voltage > 4.5) {File dataFile = SD.open("storage.txt", FILE_WRITE);
                       //Opens SD card if write switch is on

  if (dataFile) {
    Serial.println("Writing to SD");
    dataFile.println("S: ");
    dataFile.print(splitminutes);
    dataFile.print(":");
    dataFile.println(splitseconds);
    dataFile.print("Ave. S ");
    dataFile.print(splitaverageminutes); 
    dataFile.print(":");
    dataFile.println(splitaverageseconds);
    dataFile.print("M: ");
    dataFile.println(meterstraveled);
    dataFile.print(hours);
    dataFile.print(":");
    dataFile.print(minutes);
    dataFile.print(":");
    dataFile.println(seconds);
    dataFile.println();
    dataFile.close();}}   //Prints data to SD card
    }
      splitminutes = dpave/60; //Generates split minutes from split seconds 
      splitseconds = dpave - (splitminutes*60); //Displays remaining seconds
      splitaveragetotal = splitaveragetotal+splittotal;   //All split times (in seconds) added together
      splitaverage = splitaveragetotal/(integcounter);  //Averages out split times w/ seconds
      splitaverageminutes = splitaverage/60;   //Calculates split minutes
      splitaverageseconds = splitaverage-splitaverageminutes*60;   //Calculates split seconds
      splitaverageminutesdisp = splitaverageminutes;
      splitaveragesecondsdisp = splitaverageseconds;
      splitmdisp = splitminutes;
      splitsdisp = splitseconds;      //Display variables
      u8g2.clearBuffer();          // clear the internal memory
      u8g2.setFont(u8g2_font_ncenB08_tr); // choose a suitable font --------------------CHANGE THIS FONT TO SOMETHING LARGER
      u8g2.drawStr(0,10,"M:");
      u8g2.print(0,25,meterstraveled);
      u8g2.drawStr(1,10,"S:");
      u8g2.print(1,25,splitmdisp);
      u8g2.drawStr(1,50,":");
      u8g2.print(1,75,splitsdisp);//adjust pixels to get a good display
      u8g2.sendBuffer();          // transfer internal memory to the display  


      
     
        //Displays split and average split
      //oled.print("T: "); oled.print(hours); oled.print(":"); oled.print(minutes); oled.print(":"); oled.println(seconds); //Displays timer
      //if (voltage >= 4.5) {oled.print("SD Write ON");}
      //if (voltage < 4.5) {oled.print("SD Write OFF");} //Shows write permission status
      }}}}

Schematics

Schematics
Wire Diagram for the GPSSP

Comments

Similar projects you might like

GPS Location Display With GPS And TFT Display Shields

Project tutorial by Boian Mitov

  • 12,623 views
  • 5 comments
  • 33 respects

Kayak GPS Navigation

Project in progress by Chrisroy

  • 3,082 views
  • 3 comments
  • 13 respects

GPS Tracking Using Helium, Azure IoT Hub, and Power BI

Project tutorial by Team Helium

  • 14,088 views
  • 18 comments
  • 36 respects

GPS Tracker with Arduino MKR FOX 1200

by jgallar

  • 14,421 views
  • 16 comments
  • 51 respects

Log GPS Information to MicorSD Card with Visuino

Project tutorial by Boian Mitov

  • 9,871 views
  • 10 comments
  • 32 respects
Add projectSign up / Login