- 1 Project Overview
- 2 Team Members
- 3 Objectives
- 4 Challenges
- 5 Solutions
- 5.1 Receiving Data from the GPS
- 5.2 Interpreting The Data We Received
- 5.3 Displaying the Data
- 5.4 Designing Encoder
- 5.5 Hooking up the Analog to Digital Converter
- 5.6 Mounting
- 6 GPS
- 7 Encoder
- 8 Budget
- 9 Results
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.
- Max Cetta
- Jacob Frank
- Alden Welsch (TA)
- Use GPS data to track distance and location of rides
- Use the serial port to connect to the GPS.
- Read data and write it into a text file.
- Display data
- Have an encoder on the wheel to measure top ground speed and average speed over a ride
- Designing and printing the encoder
- Wiring the ADC and using SPI pins to communicate with the Raspberry Pi
- Writing code to filter and interpret the data to locate peaks
- Receiving data from the GPS chip
- Interpreting the data
- Isolating desired data from the GPS data stream which provided other information that was not useful for our project
- Displaying the data
- How to get the data off the Raspberry Pi
- Displaying the rider's position on a map
- Displaying the rider's velocity on computer screen
- 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
- 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
- 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
- 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
- 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
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
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:
At first we didn't understand what these values were, but the GPS chip that we bought came with athat 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
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=='$GPVTG'): if(array!= ''): current_speed=array #stores the value in array, 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 rider's 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=="$GPVTG". Array 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.
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 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.
At the end of the ride the user can plug the USB drive into their computer and send the gps.txt to http://www.gpsvisualizer.com which displays where they went on a map. Below is an example of what this looks like using data we collected in one of our test rides:
For the encoder first we started with a six slit disk for the wheel. When spun the wheel would reveal the LED to the photocell 6 times a rotation. We picked six for no reason other than we were unsure in the beginning of how much precision we would need, or be capable of having, when counting the wheel rotations. The size of the slits or the amount of them could blur peaks depending on the experimental readings from the ADC. We also 3D printed simple blocks for the LED and photocell mounts that would have been glued to the truck. It quickly became apparent the simple box was going to easily be knocked off or set askew, and that we were going to need much more secure spacing between the parts on the truck.
To solve the problem of security 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 the mounts would not move or rotate, but they would need to very precisely placed. If the spacing was off by just 1 mm the disk attached to the wheel would have been incapable of spinning freely around the wheel and laying flat on it, 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 disk for the wheel with one larger hole for more defined peaks.
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. By taking those averages and standard deviations we tried to locate constantly changing threshold values that would allow us to locate peaks in data based on the data itself. None of these filters seemed to work so we switched to something mechanical and 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.
Hooking up the Analog to Digital Converter
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. In order to read input from the ADC we needed to install spidev through the terminal and use it to open the SPI pins as explained in our. 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 for the Encoder
After writing code to get filtered readings from the ADC we had to figure how we could detect a rotation for the skateboards wheel and then convert rotations to kilometers per hour. Here is the code for our encoder method, which returns the rider's speed:
def encoder(): 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>queue) and (queue>queue)): rotations=rotations+1 queue=queue queue=queue queue=reading() if((current_millis-previous_millis)>1000): current_speed=rotations*.95 print current_speed previous_millis=current_millis return current_speed
To detect a peak, we intialized an array called queue to hold three filtered readings. We took a filtered reading() and stored it in queue then another filtered reading and stored it in queue and a third reading that we stored in queue. We then checked if(queue>queue and queue>queue). We set this condition because the reading in queue would be greater than queue and queue if the readings for queue and queue occurred when the led was in line with the covered part of the disk and the reading for queue came in when the LED was in line with the slit in our disk. The value in queue would be greatest because the photo cell returns greater values when exposed to more light. So, If this condition was met we could say the wheel had undergone a rotation so we incremented our rotations variable by one. Whether or not the condition in the if statement was met, meaning we had detected a rotation, we shifted the readings in our queue over: queue=queue, queue=queue and then we took a new filtered reading() and stored it in queue and checked the if statement again. When we first wrote the code for our encoder we would take three new reading()s every time and compare them. We realized, however, that we were missing peaks this way. We really needed to check every three consecutive readings to detect a peak. Reassigning the values in the queue solved this problem.
Now that we could count a rotation of the wheel we had to convert rotations to kilometers per hour. In order to do so we needed to be able to detect the time that occurred between a certain amount of rotations. To keep track of time we had to first import time at the beginning of our program. Python's time library includes a method time() which, when called, returns the amount of time that has elapsed since the program started running. We wanted to detect the milliseconds that had passed since so we created a method mills() that returned time.time()*1000--milliseconds elapsed since the start of the program. After creating this method we wanted to use it to check the size of the rotations variable every second. To do so we created a variable previous_millis which was initially set to zero and a variable current_millis which we set it equal mills() every time we took a new reading(). Every time, after checking for a rotation and then taking a new reading, we checked if((current_millis-previous_millis)>1000). If this condition was met it meant that a second had passed. When the program first runs current_millis will be roughly equal to zero because the program had just started. Every time we take new reading()s the value of current_millis grows because time continues to elapse since the program was ran. Eventually a second will pass and current_millis will be greater than 1000 (1000 milliseconds=1 second) so the condition in the if statement will be met. After we enter the if statement we set previous_millis=current_millis. We need to update previous_millis so we can check the value of rotations every second and not just the first second.
Now that we were able to check the size of rotations every second we needed to be able to convert rotations per second to kilometers per hour. To do we set current_speed=rotations*.95. We multiplied by .95 because the diameter of the wheel was 84 millimeters; 84 multiplied by pi is the circumference in mm, divided by 1,000,000 converts the circumference to kilometers. Assuming one rotation per second we multiply by 3,600 to convert to kilometers an hour, which is 0.95 kph per one rotation a second. Then we multiply by our sampled rotations per second to get speed. ((km/hr) / (rotations/second)) * (sampled rotations/second)
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. To detect when we were not getting GPS values for velocity we had to search for the $GPVTG message Id as usual and then looked for the condition if(array==" " ). If this condition was met, it meant we weren't getting velocity values because the index of the array where there should be a velocity value--array--was blank. So, if the velocity value in array was blank we ran encoder() and appended the value it returned to our speeds array that we were previously appending the values returned by GPS to.
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.
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.
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.
|GPS Receiver - GP-20U7 (56 Channel)||1||$15.95||Sparkfun||https://www.sparkfun.com/products/13740|
|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|
Once we had our filters set and the code to locate peaks working with the shield over the LED the encoder worked very reliably. The only problem was when we were stopped we got extremely high numbers. To solve this we have started a filter to take out extraneous values when stopped. When stopped we received high counts in the hundreds so they are easy to filter out. We set the threshold as 20 kph seeing as that was our calculated maximum reliable speed.
The GPS ended up working essentially perfectly for our purposes, in addition to being supplemented by the encoder in times of low visibility. Concrete caused minor discrepancies, but ended up being less of an issue than originally anticipated.
We got the WiFi dongle working with laptops, and had the data live streaming to them. Unfortunately we experienced tragedy during a test run and broke the WiFI dongle, learning too late to secure all of our USB connections and wires.
Gantt Chart and Timeline
At the beginning of the semester we thought we were going to be making a motorized electric longboard. After a month of searching for parts and trying to learn about motors and batteries we found out we were not going to be able to do it. At that point we changed our whole project outline to match a new vision going forward. We moved to the encoder and GPS idea, while also hoping to add headlights towards the end of the semester. We did not correctly anticipate the amount of time and energy simply setting up a development board, designing a part, importing libraries and making sure we had the correct hardware for connecting parts could take. After all the dirty work was done we were finally able to get to the interesting part of the project and were able to condense our timeline.
Our Final Code
from __future__ import division import time import serial import webbrowser import spidev 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 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() def close_gps(f): f.close() def bitstring(n): s = bin(n)[2:] return '0'*(8-len(s)) + s def read(adc_channel=0, spi_channel=0): spi = spidev.SpiDev() spi.open(0, spi_channel) spi.max_speed_hz = 1200000 # 1.2 MHz cmd = 128 if adc_channel: cmd += 32 reply_bytes = spi.xfer2([cmd, 0]) reply_bitstring = ''.join(bitstring(n) for n in reply_bytes) reply = reply_bitstring[5:15] spi.close() return int(reply, 2) / 2**10 #spi.close(0,spi_channel) def reading(): size=12000 filter=[0 for i in range(size)] while(i<size): x=read() filter[i]=x i=i+1 total=0 for reading in filter: total=total+reading return total/len(filter) def millis(): return time.time()*1000 def encoder(): speeds=[0 for i in range(5)] end_counter=0 f=open('speeds.txt','w') f.write('starting...') f.close() counter=0 queue=[0 for i in range(3)] while(i<3): queue[i]=reading() i=i+1 previous_millis=0 while(True): try: current_millis=millis() if((queue>queue) and (queue>queue)): counter=counter+1 queue=queue queue=queue queue=reading() #print queue if((current_millis-previous_millis)>1000): if(counter*.95<20): speeds[end_counter]=counter*.95 else: if(end_counter>1): speeds[end_counter]=speeds[end_counter-1] else: speeds[end_counter]=0 end_counter=end_counter+1 previous_millis=current_millis print counter*.95 f=open('speeds.txt','a') f.write(str(counter*.95)) f.write('\n') f.close() counter=0 if(end_counter==4): return speeds except KeyboardInterrupt: return speeds #main() def main(): create_gps() speeds= f=open('gps_speeds.txt','w') f.write('starting... \n') f.close() while(True): try: ser=set_up_gps() x=ser.readline() save_gps_to_file(x) array=x.split(',') if(array=='$GPVTG'): #print ' Velocity: ' + array #print array## main() if(array!= ''): current_speed=array print current_speed f=open('gps_speeds.txt','a') f.write(current_speed) f.write('\n') f.close speeds.append(float(current_speed)) max_speed=max(speeds) total_speeds=0 for x in speeds: total_speeds=x+total_speeds average=total_speeds/len(speeds) else: encoder_speeds=encoder() speeds.append(encoder_speeds) speeds.append(encoder_speeds) speeds.append(encoder_speeds) speeds.append(encoder_speeds) except KeyboardInterrupt: print 'Hope you had a nice ride! \n' max_speed=max(speeds) print 'Your max speed this trip was' print max_speed print 'kilometers per hour. \n' total_speeds=0 for x in speeds: total_speeds=x+total_speeds average=total_speeds/len(speeds) print 'Your average speed was ' print average print 'kilometers per hour' break if __name__ == '__main__': try: main() except KeyboardInterrupt: print 'Hope you had a nice ride! \n' max_speed=max(speeds) print 'Your max speed this trip was' print max_speed print 'kilometers per hour. \n' total_speeds=0 for x in speeds: total_speeds=x+total_speeds average=total_speeds/len(speeds) print 'Your average speed was ' print average print 'kilometers per hour' #sys.exit(0)