Electric Longboard

From ESE205 Wiki
Revision as of 20:13, 4 May 2016 by Jacobfrank (talk | contribs) (Solutions)
Jump to: navigation, search

Project Overview

When riding a Longboard for commuting or exercise, there is no way to know exactly how far or how fast one has ridden. Our goal for this project was to modify the Longboard riding experience by enabling someone to measure and record data about their ride. Using a GPS chip and an encoder on the wheel we programmed a Raspberry Pi 2 Model B to collect data from the board. We could then display the data on a map of the area tracing the route taken. This design enhances the Longboard riding experience by making it interactive and goal driven while also yielding tangible results.

Team Members

  • Max Cetta
  • Jacob Frank
  • Alden Welsch (TA)

Objectives

  1. Use GPS data to track distance and location of rides
    1. Use the serial port to connect to the GPS.
    2. Read data and write it into a text file.
    3. Display data
  2. Have an encoder on the wheel to measure top ground speed and average speed over a ride
    1. Designing and printing the encoder
    2. Wiring the ADC and using SPI pins to communicate with the Raspberry Pi
    3. Writing code to filter and interpret the data to locate peaks

Challenges

  1. Receiving data from the GPS chip
  2. Interpreting the data
    • Isolating desired data from the GPS data stream which provided other information that was not useful for our project
  3. Displaying the data
    • How to get the data off the Raspberry Pi
    • Displaying the riders position on a map
    • Displaying the riders velocity on computer screen
  4. Designing Encoder
    • Ensuring mount would attach to the truck of the Longboard
    • Making sure the mounts were able to house the LED and photocell reliably and safely
    • Stabilizing results across all lighting conditions and riding surfaces
  5. Hooking up the Analog to Digital Converter
    • Locating the SPI pins and correctly wiring them to the ADC
    • Building the voltage divider
    • Choosing the resistance to use for more rapid photocell saturation
  6. Writing code to read the Encoder values, filter, and convert them to Kilometers per hour
    • Creating a properly sized filter to stabilize noise in data
    • Locating a peak in the data and ensuring that only 1 peak was counted
  7. Environmental effects on GPS accuracy
    • Concrete, heavy clouds and other structures will obscure view to satellites
    • Locating and eliminating unreliable data
    • Substituting encoder data for these instances
  8. Mounting
    • Taking all of our components and putting them in a compact container which can be secured to the longboard
    • Maintaining as light a design as possible considering the board is being manually powered
    • Keeping all the components safe and dry

Solutions

Receiving Data from the GPS

To receive data from the gps chip we looked at the data sheet for the baud rate, parity, stop bits, and byte size and declared them for the open serial port ttyAMA0. Opening the serial port required a series of terminal commands that disconnected the port from the raspberry pi so it could be hooked up to the GPS. For more info check out our How to page

def set_up_gps():
        ser = serial.Serial(
        port = '/dev/ttyAMA0',
        baudrate = 9600,
        parity = serial.PARITY_NONE,
        stopbits = serial.STOPBITS_ONE,
        bytesize = serial.EIGHTBITS,
        timeout=1
        )
     counter=0
     return ser

Interpreting The Data We Received

Below is an example of what the data we read from the serial port looked like:

$GPGSV,3,2,12,11,55,089,38,13,23,302,22,15,01,328,,17,34,230,25*78
$GPGSV,3,3,12,28,51,307,29,30,82,255,38,46,48,193,,51,42,206,*78
$GPGLL,3838.84048,N,09019.01465,W,220705.00,A,A*7E
$GPRMC,220706.00,A,3838.83954,N,09019.01492,W,3.123,189.68,250416,,,A*78
$GPVTG,189.68,T,,M,3.123,N,5.783,K,A*39
$GPGGA,220706.00,3838.83954,N,09019.01492,W,1,08,1.15,164.5,M,-32.0,M,,*64
$GPGSA,A,3,30,07,08,13,01,17,28,11,,,,,2.33,1.15,2.02*06

At first we didn't understand what these values were, but the GPS chip that we bought came with a data sheet that explained them. The values at the beginning of the lines are called message IDs. So to find velocity, for example, we had to look for the lines that started with $GPVTG which spits out values that correspond to course over ground speed. Lines with this message ID had 9 comma separated values that followed it (sometimes some of these values were blank, as in the example above). The value that corresponded to kilometers per hour was the value 7 down from $GPVTG: In the example above the rider was going 5.783 kilometers per hour. A similar process was used to find Longitude and Latitude.

Displaying the Data

Velocity

 speeds=[] #initialize array to store speeds
 ser=set_up_gps() #set_up_gps() connects to and returns values from the serial port. These values are stored in ser
 x=ser.readline() #readline() reads a line from the serial port and we store this value in x
             
 save_gps_to_file(x) 
             
 array=x.split(',') #the split function stores the values from each line in an array. *See below for more detailed description*
             
 if(array[0]=='$GPVTG'):
      if(array[7]!= ''):
          current_speed=array[7] #stores the value in array[7], which is kilometers per hour, in current speed
          print current_speed
          speeds.append(float(current_speed)) #Current_speed is a string so we need to convert it to a float. 
                                              #Then we add the value of the current speed to our speeds array.
          
          max_speed=max(speeds)  #every time we add a value to the speeds array we use the max function 
                                 #for arrays which returns the largest value in the array. We store this value in variable max_speed 

          total_speeds=0
          for x in speeds: #we iterate through all the values in speeds and add them up to get total speed
              total_speeds=x+total_speeds
          average=total_speeds/len(speeds) #to get average speed we divide total_speeds by the amount of speeds recorded

We wanted to display the riders current velocity during the ride and at the end of the ride display their max speed and average speed. To display the data the rider's velocity, we had to use pythons split() method for strings. The split method takes in a string as a parameter:let's call this string parameter the separator. When you call the split method on a string it separates the string by the separator you input as its parameter and the method returns an array with every element between the separators in the string stored in a different index in the array.So, In our program, we read a line from the serial port and stored it in a string x. Then we called x.split(",") which stored each line we read as an array with each value that was separated by a comma in a different index in the array. To find velocity we created an if stated that checked for the condition array[0]=="$GPVTG". Array[0] is what each line would start with--the message ID. Once this condition was met we knew that the 7th index in the array contained the rider's velocity in kilometers per hour. Every Time we read a velocity value we stored it in a variable current_speed and then printed it out. Then we stored this value in an array called speeds. At the end of the ride we called the max function (a built-in python function which returns the greatest value in an array) on speeds which returned the greatest value in our array--the rider's max speed--and printed it. To get the rider's average speed we iterated through speeds and added up all its values to get total_speeds and then then divided it by len(speeds) which returns the size of the array to get the rider's average speed and printed it.

Mapping

We also wanted to display the rider's route on a map. To do so we first had to save our GPS data to a text file. We did this by creating these methods:

def create_gps():
    filepath_GPS = '/media/pi/USB DISK/gps/GPS.txt'
    f=open(filepath_GPS,'w')
    f.write('starting...')
    f.write('\n\n')
    f.close()
    
    
def save_gps_to_file(x):
    filepath_GPS = '/media/pi/USB DISK/gps/GPS.txt'
    f=open(filepath_GPS,'a')
    f.write(x)
    f.write('\n\n')
    f.close()

Python's open method takes in two strings as parameters. The first is the name of a text file. The second is either a 'w' or a 'a' . The 'w' parameter tells the computer to create a new file with the name of the file listed in the first parameter; if a file with the same name already exists the computer will overwrite it. The open method by default will search for and create the file in the same folder that the python script is saved in. However, we wanted the file to be stored on our usb. To do so we had to direct it there. We did so by calling filepath_GPS = '/media/pi/USB DISK/gps/GPS.txt' .When the method is called with the "a" parameter, the computer checks for a file with the name given in the first parameter and if it exists, when f.write() is called, it appends to it. So, create_gps() creates the text file that we will later append to it by calling our save_gps_to_file(x) method. save_gps_to_file(x) takes in a parameter x, opens a text file for appending data and and writes whatever value is stored in x to it. After defining save_gps_to_file(x) we call it in the main method of our program. In the main method, x holds the data read from the GPS chip through the serial port.

  1. For the encoder first we started with a six slit disk for the wheel and simple block for the LED and photocell mounts that would have been glued to the truck. It quickly became apparent the simple box was not going to be sufficiently secure. To solve this we added a ring to go around the truck with a box on top to house the LED and photocell respectively. Once on the truck we wanted to secure the spacing so we created specific mm spacers to go in between the wheel, photocell mount, and LED mount. After we started getting readings from the ADC we realized we only needed to measure single rotations as opposed to fractions of rotations and so we printed another ring with one larger hole to go around the wheel. We were still not getting peaks with enough definition across surfaces and in different lighting conditions so we tried a number of filters that calculated standard deviations and means of data and tried to locate values that would cross a constantly updated filter, but none of these worked so we 3D printed another piece to act as a shield for the LED. This shield was lined with aluminum foil to reflect the LED light and once secured we finally got defined peaks over surfaces and in different lighting conditions.
  2. Hooking up the mcp3002 ADC required finding data sheets for the raspberry pi and the ADC in order to match the SPI pins and correctly wire the channel, voltage and ground. Once we had everything wired we could manipulate the circuit connected to channel 0. Initially we just had the photocell connected in series with a resistor to a voltage source, but our values we not highly responsive and made little sense. After a number of trials we discovered that we needed a voltage divider and connected one lead of the resistor to the ground. This way the photocell had a resistor pull-up that increases the saturation rate of the cell and gave us much more defined data from the ADC. Once this was all hooked up we could experiment with different resistor values based on the average ambient light we were experiencing and the subsequent average photocell resistance.

Reading ADC Values

Once we read ADC values we realized there was noise in the data. To correct this, we had to create a filter that used a rolling average. Below is the code we used to take ADC readings:

def reading():
    filter_size=12000
    filter=[0 for i in range(filter_size)]
    while(i<filter_size):
        x=read()
        filter[i]=x
        i=i+1
    total=0
    for reading in filter:
        total=total+reading
    return total/len(filter)

To decide on the filter size for the data we had to calculate how fast our max speed would require we read values. We thought 20 kph would be a fair max ground speed; based on this max speed, we divided by .95kph to get approximately 21 rotations per second. To record all these peaks we needed at least 3 times that many readings for the array to work. So multiplied by 3 was 63, then 1 divided by 63 was the time of the sample multiplied by 1.2 MHz yielded about 19,000 readings. Accounting for calculation time and just to be safe we brought the filter_size down to 12,000. Once we determined our filter_size, we repeatedly took readings from the ADC in our while loop and stored them in an array called filter. Once we had 12,000 readings we iterated through all the values in filter added them up and then returned their average to get our filtered ADC reading.

Writing code to read the Encoder values

Here is the code for our encoder method, which returns the rider's speed:

def encoder():
    speeds=[0 for i in range(5)]
    end_counter=0 
    rotations=0
    queue=[0 for i in range(3)]
    while(i<3):
        queue[i]=reading()
        i=i+1
    
    previous_millis=0
    while(True):
             current_millis=millis()
             if((queue[1]>queue[0]) and (queue[1]>queue[2])):
                 rotations=rotations+1
             queue[0]=queue[1]
             queue[1]=queue[2]
             queue[2]=reading()
             if((current_millis-previous_millis)>1000):
                current_speed=rotations*.95
                print current_speed
                previous_millis=current_millis
                return current_speed
  1. To decide on the filter size for the data we had to calculate how fast our max speed would require we read values. Based on a 20 kph max ground speed we divided by .95kph to get approximately 21 rotations per second. To record all these peaks we needed at least 3 times that many readings for the array to work. So multiplied by 3 was 63, then 1 divided by 63 was the time of the sample multiplied by 1.2 MHz yielded about 19,000 readings. Accounting for calculation time and just to be safe we brought the filter size down to 12,000. To detect a peak, we intialized an array to hold three values. We took a filtered reading and stored it in array[0] then another filtered reading and stored it in array[1] and a third reading that we stored in array[2]. We then checked if(array[1]>array[0] and array[1]>array[2]) we incremented a rotations variable by one. After this we shifted our readings over: array[0]=array[1], array[1]=array[2] and then we took a new filtered reading and stored it in array[2] and checked the if statement again. We then checked the size of our rotations variable every second and multiple this value by .95. We multiplied this by .95 because the diameter of the wheel was 84 millimeters so multiplied by pi is the circumference then multiplied by 360 and divided by 1,000,000 converts rotations per second to kilometers per hour.

Environmental effects on GPS accuracy

Concrete, clouds and other environmental factors can block the GPS signal. When this happened we would not get any velocity values. We wanted to record the rider's velocity throughout the whole trip so when the gps chip was unable to receive reliable data we started storing the velocity values recorded by our encoder. The code we wrote searched for the $GPVTG message Id and then we said if(array[7]==0) run our encoder method.

  1. Mounting all of the components meant 3D printing a plate to hold the raspberry pi and an enclosure for the breadboard and wires. In addition holes for the GPS wires and a mount of the GPS on the outside of the cover were added. After the pi was secure we were able to solder the wires to the encoder on the wheel and attach them to the breadboard.

GPS

In order to track the path and distance traveled during a ride we needed a GPS chip to acquire coordinate and velocity data from satellites. Receiving this data required first opening the serial port, and then sending it through the serial port to the Raspberry Pi. A little bit of python code allowed us to readline() data at 1 Hz, which we could print to the serial monitor and write to text files we opened and appended. which would then save the data to a Text file. Once saved, we could parse the data, separated by commas, into an array and locate by array idex the coordinates and velocities we desired. We were able to display our data on a map of Washington University's campus using a website. In order to live stream our data from a remote source we then had to connect the Raspberry Pi to WiFi. Using an Edimax N150 we could configure the USB ports through the terminal allowing for consistent WIFi connectivity during a ride. Once this was done we could remote shell login and receive live data on our laptops.


Encoder

The encoder began as a matter of 3D printing. Making sure that the housings for the LED and photocell were secure and precisely spaced was an issue we faced through many iterations of the design. Once it was printed it became a matter of wiring and coding. Ensuring that the Encoder circuit worked reliably ended up being a matter of mechanics, circuitry, and programing. The Raspberry Pi does not take analog inputs through its GPIO pins so we had to use the mcp3002 Analog to Digital Converter to interpret analog signal from the photocell and convert them to readable values for the SPI pins. In addition to wiring the ADC we had to build a voltage divider that would give reliable values in light spaces and indicate ticks on the wheel with significant disparities. Once we had those values printing reliably and steadily we had to create a number of filters to average and interpret those values. By nesting while statements with filters and if statements we were able to create a counter that would count once for every time a stream of values went above a threshold. By putting that inside a delta time loop we could print the number of rotations per second. Finding that threshold was another matter entirely consisting of numerous trials with filter size and calculation rates. In the end by adjusting the value of the resistor in the voltage divider and creating a shield for the LED we were able to stabilize the values and locate a steady threshold.


Budget

Item Quantity Price Vendor Link
GPS Receiver - GP-20U7 (56 Channel) 1 $15.95 Sparkfun https://www.sparkfun.com/products/13740
Mini Photocell 2 $1.50 Sparkfun https://www.sparkfun.com/products/9088
Analog to Digital Converter - MCP3002 1 $2.30 Sparkfun https://www.sparkfun.com/products/8636
Raspberry Pi 2 - Model B (8GB Bundle) 1 $49.95 Sparkfun https://www.sparkfun.com/products/13724
Resistor Kit - 1/4W (500 total) 1 $7.95 Sparkfun https://www.sparkfun.com/products/10969
LED - Basic Green 5mm 2 $0.35 Sparkfun https://www.sparkfun.com/products/9592
Tontec® Raspberry Pi Case 1 $7.98 Amazon http://www.amazon.com/Tontec%AE-Raspberry-Black-Enclosure-Transparent/dp/B00NUN98UW?ie=UTF8&psc=1&redirect=true&ref_=od_aui_detailpages00
KMASHI 10000mAh Battery 1 $13.99 Amazon http://www.amazon.com/KMASHI-10000mAh-External-Battery-Portable/dp/B00JM59JPG?ie=UTF8&psc=1&redirect=true&ref_=oh_aui_detailpage_o04_s00
Total $101.82


Gantt Chart
Timeline