I just feel better when I have a certain level of fitness. It has less to do with bringing my body into shape for the speedo season and more with general happiness. For me ‘mens sana’ (a sound mind) lives indeed ‘in corpore sano’ (in a healthy body). Whenever possible I try to get my exercise out in nature, away from distractions, enjoying the fresh air and the occasional wildlife encounter. However, between rubbing the sleep from my face and going to work I find it more convenient to get down to my basement and jump on my bicycle or my rowing machine, especially when it is still dark outside. I also get bored easily and sweating just for the sake of sweating isn’t my cup of tea. Reading eBooks during the workout worked for a while, but it wasn’t very satisfying in the long run so I needed to come up with something that was a bit of a challenge along with some form of entertainment. So, I came up with the idea of the ‘Donkey Bike’.
The idea was to create a virtual trip to visit my parents in Germany and track the progress with every rotation of my wheel and every stroke of the rower. I’d rather choose a scenic route than the direct one, so the first part of the journey is a 6,886 km ride from Redmond, WA to Nova Scotia, Canada.
With 1,081 km down, a flat tire (yes, that can happen even in the basement), a few interesting scenes discovered on the imagery and a few pounds lighter than when I started the journey, I thought it’s time to share some details on the somewhat unusual ‘Donkey Bike’.
The Setup
I already had an indoor bicycle trainer with adjustable resistance to put my road bike on. From an old bicycle computer I re-used the reed-switch. I bought an Arduino Uno with a proto shield, dug out my good old soldering station and got to work. The reed switch needs to be connected to ground and digital pin 3 (D3) on the Arduino and then we can connect the board via the USB port to the laptop and start coding.
Arduino Firmware
I kept this part very simple and just count the rotations, i.e. the number of times that the reed-switch closed and write that number to the serial port that is being read by the USB connection on the laptop.
The Arduino IDE took a bit getting used to, and for more complex code I would be looking at something like Arduino for Visual Studio.
int reedPin = 3; //digital pin connected to reed switch int rotationCount = 0; // will be used in interrrupt void rotation() { byte buff = 0x00; for (byte i = 0; i < 8; i++) { buff |= !digitalRead(reedPin) << i; } if (buff == 0xff) { rotationCount += 1; } } void setup(){ Serial.begin(9600); // set pin connected to reed switch to input pinMode(reedPin, INPUT); // set reed pin pull up resistor digitalWrite(reedPin, HIGH); // attach interrupt to reed pin attachInterrupt(1, rotation, FALLING); } void loop(){ Serial.println(rotationCount, DEC); delay(100); }
Windows Application
Rather than going through the 200-odd lines of code of the WPF application, I will just point out a few things and put the complete source code on my OneDrive here.
For the mapping I use the Bing Maps WPF Control.
To plot the initial route I use the Bing Maps REST Services, avoid highways and return the route-geometry. I then insert the route as a SqlGeography in a SQL Server table. The create table scripts for the tables I use are also in the download. Note that Bing Maps returns coordinates in the order (latitude, longitude) while the Well Known Text (WKT) in which we describe the SqlGeography expects coordinates in the order (longitude latitude) so we have to flip the order for each coordinate.
Private Sub btnGetRoute_Click(sender As Object, e As RoutedEventArgs) myMap.CredentialsProvider.GetCredentials( Function(bmKey) Dim routeRequest As New Uri( _ "http://dev.virtualearth.net/REST/V1/Routes/Driving" _ + "?wp.0=[Your Start]" _ + "&wp.1=[Your End]" _ + "&avoid=highways" _ + "&optimize=distance" _ + "&distanceUnit=km" _ + "&travelMode=Driving" _ + "&routePathOutput=Points" _ + "&key=" + bmKey.ApplicationId) Dim wc As New WebClient AddHandler wc.DownloadStringCompleted, AddressOf routeCallback wc.DownloadStringAsync(routeRequest) Return 0 End Function) End Sub Private Sub routeCallback(sender As Object, e As DownloadStringCompletedEventArgs) Dim jsonText = e.Result Dim jsp = New JsonParser() Dim json As Object = jsp.Parse(jsonText) Dim routePath As String = "LINESTRING(" For i = 0 To json.resourceSets(0).resources(0).routePath.line.coordinates.count - 1 routePath += json.resourceSets(0).resources(0).routePath.line.coordinates(i)(1).ToString + _ " " + json.resourceSets(0).resources(0).routePath.line.coordinates(i)(0).ToString + "," Next routePath = routePath.Substring(0, routePath.Length - 2) + ")" myGeog = SqlGeography.Parse(routePath) myGeog.STSrid = 4326 myConn.Open() Dim myCMD As New SqlCommand myCMD.Connection = myConn myCMD.CommandText = "INSERT INTO Routes (Geog, StartLat, StartLon) VALUES (@Route, " + json.resourceSets(0).resources(0).routePath.line.coordinates(0)(0).ToString + ", " + json.resourceSets(0).resources(0).routePath.line.coordinates(0)(1).ToString + ")" Dim myGeogParam As SqlParameter = myCMD.Parameters.Add("@Route", Sys-tem.Data.SqlDbType.Udt) myGeogParam.UdtTypeName = "Geography" myGeogParam.Value = myGeog myCMD.ExecuteNonQuery() myConn.Close() MessageBox.Show("Done") End Sub
In order to track the progress I made along the route I calculate the distance based on the wheel size and the number of rotations that I receive from the Arduino and add it to the total distance I have travelled in previous workouts. Then I determine the corresponding location along the route using the function LocateAlongGeog from the SQL Server Spatial Tools and set the center of the map and the location of the pushpin accordingly.
Private Sub myTimer_Tick(sender As Object, e As EventArgs) If stopWatch.IsRunning Then Dim ts As TimeSpan = stopWatch.Elapsed currentTime = String.Format("{0:00}:{1:00}:{2:00}", ts.Hours, ts.Minutes, ts.Seconds) lblTimer.Content = currentTime Dim rotations() As String rotations = Split(com3.ReadExisting, Environment.NewLine) Dim lastRotations = rotations(rotations.Length - 2) lblRotations.Content = lastRotations Dim thisDistance As Integer = CInt(lastRotations * 700 * 3.14 / 1000) Dim totalDistance = thisDistance + myDistSoFar lblDistance.Content = thisDistance.ToString Dim myCurrentSqlLoc As SqlGeography myCurrentSqlLoc = SQLSpatialTools.Functions.LocateAlongGeog(myGeog, totalDis-tance) Dim myCurrentBingLoc As Location = New Location(myCurrentSqlLoc.Lat, myCur-rentSqlLoc.Long) myPin.Location = myCurrentBingLoc myMap.SetView(myCurrentBingLoc, 17) myGauge.CurrentValue = thisDistance / ts.TotalSeconds * 3.6 End If End Sub
The gauge measures my average speed and makes me feel bad when I get below 35 km/h. For the gauge I re-used a piece of code written by EvelynT and published on Code Project.
I was contemplating creating a multi-user environment where you can race your friends and maybe that is something I’ll do later, but for now it’s been quite fun to figure out how to make this work and it continues to be fun to cycle virtually through beautiful landscapes. On the downside: I may have to resign as the mayor of the Hippo Pool soon.
- Bing Maps Team