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 riders position on a map
- Displaying the riders 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 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=="$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 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.
- 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.
- 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.
- 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 calculated a maximum filter size as about 6000 samples. To detect a peak, we intialized an array to hold three values. We took a filtered reading and stored it in array then another filtered reading and stored it in array and a third reading that we stored in array. We then checked if(array>array and array>array) we incremented a rotations variable by one. After this we shifted our readings over: array=array, array=array and then we took a new filtered reading and stored it in array 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==0) run our encoder method.
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
- 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|