Wednesday 20 January 2021

Raspberry Pi Exercise Bike

 The PiBike

A Project to upgrade a simple exercise bike into something a bit more fun...


Do you have an exercise bike like this?

I have used this exercise bike quite a bit but it is a little......boring. You get a read out of your effort as numbers on a little LCD display that shows distance, speed and calories etc. It also reads your pulse.

The bike itself has a large knob that allows you to set the resistance. 

Then one day the LCD unit failed. I think the circuit board got some sweat on it and corroded. I tried to fix it but it seems to be dead.

So I thought I'd have a go at replacing it with a raspberry pi.



The original head unit is connected to the bike via three connections. Two of them are to the hand sensors that read your pulse. I ignored these.

The third is connected to a switch that closes every time the pedal rotates. Using this I can connect the  bike via a small interface circuit connected to the GPIO pins on the Pi.






I used the 7" LCD colour screen for the raspberry pi and mounted it where the old head unit used to sit. You don't have to use this screen, any screen will do. I've even had it running on the pi and used VNC to connect to the pi so that I could see the race on my phone!
The screen I used is the 'Official' raspberry pi 7" touchscreen from here:


It currently costs £60 plus £12 for a case. No doubt there are cheaper options out there.




The Raspberry Pi then displays an avatar for 'you' along with some computer oponents and you then enters you into a 'race' against the other riders. You can choose the length of the race. The course goes up and down hills and your speed will be affected by how fast you pedal, the gradient you are riding on and the aero-gains you can get by riding behind the other riders.

Obviously on this cheap exercise bike the pi cannot control the resistance as this is controlled by a large mechanical knob on the bike, so it is up to you how hard you want to work by setting this at the start.

For the software I simply took the latest raspberry pi image from the Raspberry Pi org here:

Included in the latest O/S are all the libraries need for GPIO and 'PyGame' which is a simple framework for writing games. The code for the game is written in Python.

There are three python files - race.py, bike.py and route.py. The first two handle the race itself and the bike. The third creates the route you'll be cycling.

The images I used are as follows along with their pixel sizes:

bikebluesmall.gif  113x80:



bikeredsmall.gif   113x80:


bikegreysmall.gif  113x80:



tree.gif           84x95:
 


giftshop.gif       168x132


cafe.gif           138x118:


bookshop.gif       156x130:


finish.gif         156x130:


flamerouge.gif     158x197:


crowd.gif          355x103:



podium.gif         800x450

The images don't have to be exactly these sizes but try to get close to them or things may look a little odd. You can source your own pictures.

The player bike is the 'red' rider and the blue rider is used for the computer bikes. The 'grey' bike is used for when the player crosses the finish line.

As you ride you'll see a green/black bar at the top which shows your 'aero' bonus when you are benefitting from riding in another bike's wind shadow. Keeping this in the black should mean you can go faster for less effort. Similarly the other bikes will benefit from being in the draft of a bike in front of them and will recover energy as they follow you. Each computer rider also has a random 'attack' period that they will use at some point in the race - so watch out for a break-away forming. Try not to let them get away!

Here are the python files. Create a folder in /home/pi called 'bike' and place all the python files and image files in there. Then to run just enter:

> python race.py

If you want this to run at start up then edit the file /etc/rc.local on the pi and insert a line to run the python program before the line that says 'exit 0'. It should look something like this:

#!/bin/sh -e
#
# rc.local
#
# This script is executed at the end of each multiuser runlevel.
# Make sure that the script will "exit 0" on success or any other
# value on error.
#
# In order to enable or disable this script just change the execution
# bits.
#
# By default this script does nothing.
# Print the IP address
_IP=$(hostname -I) || true
if [ "$_IP" ]; then
  printf "My IP address is %s\n" "$_IP"
fi
python /home/pi/bike/race.py &
exit 0


Bike.py:

import time
import math
class Bike:
   def __init__(self, id, name, power, sprint, rotation, attackdist, attackpower, attacklength):
      self.Mass=100
      self.Energy=0
      self.Distance=0
      self.id = id
      self.name = name
      self.power = power
      self.sprint = sprint
      self.rotation = rotation
      self.aerofactor = 0.0
      self.AirResistForce=0.08
      self.AttackDistance = attackdist
      self.AttackLength = attacklength
      self.AttackPower = attackpower
      self.FinishTime = 0
      self.Finished = False
      self.RecoveryPower = 0
   def GetName(self):
      return self.name
   def GetId(self):
      return self.id
   def GetRotation(self):
      return self.rotation;
   def GetAeroFactor(self):
      return self.aerofactor
   def ReduceRecoveryPower(self, change):
      self.RecoveryPower = self.RecoveryPower - change
      if self.RecoveryPower < 0:
         self.RecoveryPower = 0
   def IncreaseRecoveryPower(self, change):
      self.RecoveryPower = self.RecoveryPower + change
   def GetRecoveryPower(self):
      return self.RecoveryPower
   def SetRotation(self, r):
      if r>self.rotation:
         self.rotation = self.rotation + 1
      if r<self.rotation:
         self.rotation = self.rotation - 1
   def SetFinishTime(self, timefin):
      # record the finish time - but only if it hasn't already been set. This allows us to call this repeatedly nce the rider passes the finish
      # without constantly updating the time.
      if self.FinishTime == 0:
         self.FinishTime = timefin
         self.Finished = True
   def GetFinishTime(self):
      return self.FinishTime
   def GetFinished(self):
      return self.Finished
   def AddEnergy(self, e):
      self.Energy = self.Energy + e
      if self.Energy < 0:
         self.Energy = 0
   def SubtractEnergy(self, e):
      if self.Energy < e:
         self.Energy=0
      else:
         self.Energy = self.Energy - e
   def GetEnergy(self):
      return self.Energy
   def GetMass(self):
      return self.Mass
   def GetSpeed(self):
      speed = math.sqrt(2*self.Energy/self.Mass) *0.7
      return speed
   def GetDistance(self):
      return self.Distance
   def GetPower(self):
      return self.power
   def GetSprint(self):
      return self.sprint
   def Update(self, Bikes, timediff):
      bikespeed = self.GetSpeed()
      air_resistance = bikespeed * bikespeed * self.AirResistForce
      factor = 1.0
      # if the bike is in front of this one then it might give some shelter:
      for b in Bikes:
         if b.GetId()!=self.id:
           
            # the bike 'position' is actually at the back of the image
            # the image is actually 100 pixels wide (50m long!)
            # so we add a bit to the position of this bike to get a rough idea of where the front wheel is
            frontofbike = self.GetDistance()*1000 + 35
            backofotherbike = b.GetDistance() * 1000
            otherbikegap = backofotherbike - frontofbike
            if (otherbikegap<50 and otherbikegap>0):
               # multiply the aero factor from this bike with what we have so far - this will get us the cumulative effect of all the bikes in front
               # of this one...
               factor = factor * otherbikegap/50
        
      # finally multiply the air resistance by the calculated factor. If we have no bikes in front then this will still be 1.0 
      air_resistance = air_resistance * (factor*0.75+0.25)
      self.aerofactor = factor
      air_resist_energy = air_resistance/timediff
      self.SubtractEnergy(air_resist_energy)
      # ...and friction, which will be proportional to speed:
      friction = bikespeed * 0.1
      friction_energy = friction / timediff
      self.SubtractEnergy(friction_energy)
      bikespeed = self.GetSpeed()
      self.Distance = self.Distance + (bikespeed/3600)*timediff
      if ((self.Distance*1000)>self.AttackDistance and (self.Distance*1000)<(self.AttackDistance+self.AttackLength)):
         self.AddEnergy(self.AttackPower*timediff)

Race.py:

#!/usr/bin/python

import RPi.GPIO as GPIO
import time
import pygame
import random
import math
import sys
from bike import Bike
from route import Route


pulse_count = 0
currtm = time.time()
lastinterval=0
race_started=False

white = 255, 255, 255
green = (0, 255, 0)
dgreen = (19, 121, 19)
red = (255, 0, 0)
blue = (0, 0, 255)
black = (0, 0, 0)
grey = (240, 240, 240)
# light shade of the button 
color_light = (170,170,170) 
# dark shade of the button 
color_dark = (100,100,100)

def InitDisplay():

  # initialisation
   pygame.init()

   screen = pygame.display.set_mode((800,450)) # Set screen size of pygame window
   background = pygame.Surface(screen.get_size())  # Create empty pygame surface
   background.fill((255,255,255))     # Fill the background white color (red,green,blue)
   background = background.convert()  # Convert Surface to make blitting faster

   screen.blit(background, (0, 0))

   # Print in titlebar
   text = "Cycling"
   pygame.display.set_caption(text)

   return screen

def write(msg="pygame is cool", size=24, color=(0,0,0)):
    myfont = pygame.font.SysFont("None", size)
    mytext = myfont.render(msg, True, color)
    mytext = mytext.convert_alpha()
    return mytext


def bike_pulse(channel):
    global currtm
    global lastinterval
    global race_started

    newtm = time.time()
    interval = newtm - currtm

    #print ("pulse " + str(interval))

    race_started = True

    currtm = newtm
    if (interval>0.2 and interval<3.0):
       lastinterval=interval
    else:
       lastinterval=0



def GetFinalLeaderboard(playerBike, computerBikes):   
   # unlike the race leaderboard, the final finishing positions are determined by the finish times
    racePosition = []
    for b in computerBikes:
       racePosition.append(b)

    racePosition.append(playerBike)
    racePosition.sort(key=sortFuncTime, reverse=False)

    return racePosition

def sortFuncTime(b):
   if b.GetFinishTime()==0:
      return 999999999999
   else:
      return b.GetFinishTime()


def GetLeaderboard(playerBike, computerBikes):   
    racePosition = []
    for b in computerBikes:
       racePosition.append(b)

    racePosition.append(playerBike)
    racePosition.sort(key=sortFuncDist, reverse=True)

    return racePosition

def sortFuncDist(b):
   return b.GetDistance()

def GetRaceLength(screen):
   #lengthchoice = pygame.image.load("/home/pi/bike/racelength.gif").convert()

   screen.fill(white)
   #screen.blit(racelength, (0,0)) 

   screen.blit(write("Choose Distance:", size=48), (160,100))

   pygame.display.flip()

   nochoice = True

   while nochoice==True: 
      # stores the (x,y) coordinates into 
      # the variable as a tuple 
      mouse = pygame.mouse.get_pos()  


      for ev in pygame.event.get(): 
         #checks if a mouse is clicked 
         if ev.type == pygame.MOUSEBUTTONDOWN: 
            #print (mouse[0], mouse [1])
            if 360 <= mouse[0] <= 520 and 200 <= mouse[1] <= 230: 
               choice = 2000
               nochoice = False 

            if 360 <= mouse[0] <= 520 and 250 <= mouse[1] <= 280: 
               choice = 10000
               nochoice = False 

            if 360 <= mouse[0] <= 520 and 300 <= mouse[1] <= 330: 
               choice = 15000
               nochoice = False 

            if 360 <= mouse[0] <= 520 and 350 <= mouse[1] <= 380: 
               choice = 20000
               nochoice = False 

            if 360 <= mouse[0] <= 520 and 400 <= mouse[1] <= 430: 
               choice = 25000
               nochoice = False 

         if 360 <= mouse[0] <= 520 and 200 <= mouse[1] <= 230:
            pygame.draw.rect(screen,color_light,[360,200,160,30])
         else:
            pygame.draw.rect(screen,color_dark,[360,200,160,30])

         if 360 <= mouse[0] <= 520 and 250 <= mouse[1] <= 280:
            pygame.draw.rect(screen,color_light,[360,250,160,30])
         else:
            pygame.draw.rect(screen,color_dark,[360,250,160,30])

         if 360 <= mouse[0] <= 520 and 300 <= mouse[1] <= 330:
            pygame.draw.rect(screen,color_light,[360,300,160,30])
         else:
            pygame.draw.rect(screen,color_dark,[360,300,160,30])

         if 360 <= mouse[0] <= 520 and 350 <= mouse[1] <= 380:
            pygame.draw.rect(screen,color_light,[360,350,160,30])
         else:
            pygame.draw.rect(screen,color_dark,[360,350,160,30])

         if 360 <= mouse[0] <= 520 and 400 <= mouse[1] <= 430:
            pygame.draw.rect(screen,color_light,[360,400,160,30])
         else:
            pygame.draw.rect(screen,color_dark,[360,400,160,30])

      screen.blit(write("2 km", size=48), (400,200))
      screen.blit(write("10 km", size=48), (400,250))
      screen.blit(write("15 km", size=48), (400,300))
      screen.blit(write("20 km", size=48), (400,350))
      screen.blit(write("25 km", size=48), (400,400))
     
      pygame.display.update() 
   
   
   screen.fill(white)
   screen.blit(write( str(choice/1000) + "km", size=48), (300,200))
   pygame.display.update() 
   time.sleep(5)


   return choice

def DisplayPodium(screen, playerBike, computerBikes, raceStartTime):
   podium = pygame.image.load("/home/pi/bike/podium.gif").convert()

   screen.fill(white)
   screen.blit(podium, (0,0)) 

   screen.blit(write("Winner", size=48), (160,400))

   racePosition = GetFinalLeaderboard(playerBike, computerBikes)

   txt = racePosition[0].GetName()
   screen.blit(write(txt, size=48), (500,400))

   pygame.display.flip()

   time.sleep(7)

   screen.fill(white)

   winningTime = racePosition[0].GetFinishTime()
   raceElapsedTime = winningTime - raceStartTime
   m, s = divmod(raceElapsedTime, 60)
   h, m = divmod(m, 60)

   strRaceTime = '{:.0f}h:{:.0f}m:{:.3f}s'.format(h, m, s)

   screen.blit(write("Position:            Rider:           Time:"), (145, 65))
   position = 0  
   for b in racePosition:
      if (position%2)==0:
         pygame.draw.rect(screen, grey, (0, 100+position*30, 800, 30)) 
         
      screen.blit(write(str(position+1), size=24), (180, 100 + position*30))
      screen.blit(write(b.GetName(), size=24), (200, 100 + position*30)) 

      finishTime = b.GetFinishTime()
      if finishTime==0:
         displayTime = "DNF"
      else:
         gap = round(finishTime - winningTime,3)
         displayTime = str(gap) 

      if position>0:
         screen.blit(write("+" + displayTime, size=24), (400, 100 + position*30)) 
      else:
         screen.blit(write(strRaceTime, size=24), (400, 100 + position*30)) 

      position = position + 1

   pygame.display.flip()

   time.sleep(10)
   


def bikeRotation (height_diff, dist_diff):
   if height_diff==0 or dist_diff==0:
      return 0

   rot =  math.atan(height_diff/(dist_diff*1000))*180/3.14
   rot = rot * -0.3

   return rot

def DrawLeaderboard(screen, leaderboard, x, y):
   race_pos=1
   leaderDist = leaderboard[0].GetDistance()

   for b in leaderboard:
      distdiff = int( round( ((leaderDist - b.GetDistance()) * 1000), 0) )
      nametxt = str(race_pos) + " " + b.GetName()
      postxt = "+" + str(distdiff) + "m"
      recoverytxt = str(b.GetRecoveryPower())
      screen.blit(write(nametxt), (x, y + race_pos*15))
      screen.blit(write(postxt), (x+150, y + race_pos*15))
      #screen.blit(write(recoverytxt), (x+250, y + race_pos*15))
      race_pos = race_pos + 1

def plotX(groundpos):
   return groundpos*2
 
def run(screen, playerBike, computerBikes, Race_Length):

   global lastinterval
   global race_started
    
   route = Route(Race_Length)

   bluebike = pygame.image.load("/home/pi/bike/bikebluesmall.gif").convert()
   redbike = pygame.image.load("/home/pi/bike/bikeredsmall.gif").convert()
   greybike = pygame.image.load("/home/pi/bike/bikegreysmall.gif").convert()
   tree = pygame.image.load("/home/pi/bike/tree.gif").convert()
   giftshop = pygame.image.load("/home/pi/bike/giftshop.gif").convert()
   cafe = pygame.image.load("/home/pi/bike/cafe.gif").convert()
   bookshop = pygame.image.load("/home/pi/bike/bookshop.gif").convert()
   finish = pygame.image.load("/home/pi/bike/finish.gif").convert()
   flamerouge = pygame.image.load("/home/pi/bike/flamerouge.gif").convert()
   crowd = pygame.image.load("/home/pi/bike/crowd.gif").convert()
   WHITE = (255, 255, 255)
   bluebike.set_colorkey(WHITE)  # White colors will not be blit.
   redbike.set_colorkey(WHITE)  # White colors will not be blit.
   greybike.set_colorkey(WHITE)  # White colors will not be blit.
   tree.set_colorkey(WHITE)  # White colors will not be blit.
   bookshop.set_colorkey(WHITE)  # White colors will not be blit.
   cafe.set_colorkey(WHITE)  # White colors will not be blit.
   giftshop.set_colorkey(WHITE)  # White colors will not be blit.
   finish.set_colorkey(WHITE)  # White colors will not be blit.
   flamerouge.set_colorkey(WHITE)  # White colors will not be blit.
   crowd.set_colorkey(WHITE)  # White colors will not be blit.

   running = True
   currtime = time.time()
   lastinterval = 0

   race_started = False

   while running:
      newtm = time.time()
      timediff = newtm - currtime
      currtime = newtm

      #print (timediff, computerBikes[0].GetDistance(), computerBikes[0].GetSpeed(), computerBikes[0].GetEnergy())

      leaderboard = GetLeaderboard(playerBike, computerBikes)

      #lines for auto-play:
      #race_started=True
      #lastinterval=5

      # add in energy from pedalling
      if lastinterval>0:
         cyclist_energy = playerBike.GetPower()/lastinterval
         lastinterval = 0
      else:
         cyclist_energy = 0

      playerBike.AddEnergy(cyclist_energy)

      if race_started==True:
         for b in computerBikes:
            if b.GetFinished()==False:
               b.AddEnergy((b.GetPower()+b.GetRecoveryPower())*timediff)
  
               # computer bikes will start to sprint for the line in the final 10% of the race: 
               if b.GetDistance()*1000 > Race_Length * 0.9:
                  b.AddEnergy(b.GetSprint()*timediff)

            if b.GetAeroFactor()>0.8:
               b.ReduceRecoveryPower(10)
            if b.GetAeroFactor()<0.5:
               b.IncreaseRecoveryPower(10)

            comp_dist = b.GetDistance()
            comp_height = route.GetHeight(int(comp_dist*1000))

            b.Update(leaderboard, timediff)

            comp_new_dist = b.GetDistance()
            comp_new_height = route.GetHeight(int(comp_new_dist*1000))
            comp_height_diff = comp_height - comp_new_height
            comp_dist_diff= comp_new_dist - comp_dist
            b.SetRotation(bikeRotation(comp_height_diff, comp_dist_diff))

            comp_pot_energy = b.GetMass() * comp_height_diff * 3
            b.AddEnergy(comp_pot_energy)

      player_dist = playerBike.GetDistance()
      player_height = route.GetHeight(int(player_dist*1000))

      playerBike.Update(leaderboard, timediff)
      
      player_new_dist = playerBike.GetDistance()
      player_new_height = route.GetHeight(int(player_new_dist*1000))
      player_height_diff = player_height - player_new_height
      player_dist_diff = player_new_dist - player_dist
      playerBike.SetRotation(bikeRotation(player_height_diff,player_dist_diff))

      player_pot_energy = playerBike.GetMass() * player_height_diff * 3
      playerBike.AddEnergy(player_pot_energy)
 
      lastinterval=0
      
      for event in pygame.event.get():

         if event.type == pygame.QUIT:
            #
            running = False

      screen.fill(white)

      txt = "Speed {:.2f} km/h"
      displayStr = txt.format(playerBike.GetSpeed())
      screen.blit(write(displayStr), (100,390))

      txt = "Height {:.2f}"
      displayStr = txt.format(round(player_new_height,1))+"m"
      screen.blit(write(displayStr), (100,410))

      txt = "Distance : {:.2f}"
      displayStr = txt.format(player_new_dist) + "km"
      screen.blit(write(displayStr), (100,430))

      DrawLeaderboard(screen, leaderboard, 500, 350)

      leftedge = player_dist

      for b in computerBikes:
         if leftedge>b.GetDistance():
            leftedge = b.GetDistance()

      # if the computer falls more than 300m behind the player then he will fall off the screen:
      if leftedge < player_dist - 0.3:
         leftedge = player_dist - 0.3 

      left = int(leftedge * 1000)
      groundpos =left-100
      routelen = route.GetLength()

      while groundpos < left + 550:

         groundheight = route.GetHeight(groundpos)

         # check for scenery
         item = route.GetScenery(groundpos)

         if item == "tree":
            screen.blit(tree, (plotX(groundpos-left-90),300-(groundheight/4)-50)) 

         if item == "cafe":
            screen.blit(cafe, (plotX(groundpos-left-100),300-(groundheight/4)-70)) 

         if item == "bookshop":
            screen.blit(bookshop, (plotX(groundpos-left-100),300-(groundheight/4)-70)) 

         if item == "giftshop":
            screen.blit(giftshop, (plotX(groundpos-left-100),300-(groundheight/4)-70)) 
         
         if item == "finish":
            screen.blit(finish, (plotX(groundpos-left-100),300-(groundheight/4)-120)) 
         
         if item == "flamerouge":
            screen.blit(flamerouge, (plotX(groundpos-left-100),300-(groundheight/4)-120)) 
         
         if item == "crowd":
            screen.blit(crowd, (plotX(groundpos-left-90),300-(groundheight/4)-60)) 
         
         groundpos = groundpos + 1

      groundpos =left
      while groundpos < left + 500:
         
         groundheight = route.GetHeight(groundpos)

         # Draw the ground green with a red line at the finish
         if groundpos == route.GetLength()+52:
            pygame.draw.rect(screen, red, (plotX(groundpos-left), 300-(groundheight/4)+50, 10, 20)) 
         else:
            pygame.draw.rect(screen, green, (plotX(groundpos-left), 300-(groundheight/4)+50, 10, 20)) 
        
         groundpos = groundpos + 1

      for b in computerBikes:
         compDist = b.GetDistance()
         compHeight = route.GetHeight(int(compDist*1000))
         computerbikeX = plotX(int((compDist-leftedge) * 1000))
         computerbikeY = 300 - compHeight/4  + b.GetId()*3
         #print (compDist, compHeight, computerbikeY)

         rotated_comp = pygame.transform.rotate(bluebike, b.GetRotation() )
         screen.blit(rotated_comp, (computerbikeX,computerbikeY)) 
         #pygame.draw.rect(screen, red, (computerbikeX, computerbikeY, 5, 5))

      playerbikeX = plotX(int((player_dist - leftedge) * 1000))
      playerbikeY = 300-player_height/4+20
     
      if playerBike.GetFinished()==True: 
         rotated_player = pygame.transform.rotate(greybike, playerBike.GetRotation())
      else:
         rotated_player = pygame.transform.rotate(redbike, playerBike.GetRotation())

      screen.blit(rotated_player, (playerbikeX,playerbikeY)) 
      #pygame.draw.rect(screen, blue, (playerbikeX+100, playerbikeY, 5, 5))
     
      aero = playerBike.GetAeroFactor()
      aerobarlength = aero * 100
      
      pygame.draw.rect(screen, black, (197, 38, 106, 24)) 
      pygame.draw.rect(screen, green, (200, 40, aerobarlength, 20)) 
      
      pygame.display.flip()

      # record finish time for each rider:
      if (playerBike.GetDistance()*1000)>= route.GetLength():
         playerBike.SetFinishTime(currtime)
         print ("Player Finished " + str(currtime))
      for b in computerBikes:
         if (b.GetDistance()*1000)>= route.GetLength():
            b.SetFinishTime(currtime)

      # race finishes when all riders reach the end
      allFinished = True
      if (playerBike.GetDistance()*1000)<= route.GetLength():
         allFinished = False
      else:
         # player has finished, allow any bikes within a few meters to finish too
         for b in computerBikes:
            if b.GetDistance()*1000<= route.GetLength() and b.GetDistance()*1000> route.GetLength()-400:
               allFinished = False
         
        
      if allFinished:
         running = False



def main():
   # Setup GPIO input
   try:
      GPIO.setmode(GPIO.BCM)
      GPIO.setup(4, GPIO.IN, pull_up_down = GPIO.PUD_UP) # pin 7
      GPIO.setup(14, GPIO.IN, pull_up_down = GPIO.PUD_UP) # pin 8
      GPIO.setup(15, GPIO.IN, pull_up_down = GPIO.PUD_UP) # pin 10
      GPIO.add_event_detect(4, GPIO.FALLING, callback=bike_pulse, bouncetime=200)
      GPIO.add_event_detect(14, GPIO.FALLING, callback=left_button, bouncetime=200)
      GPIO.add_event_detect(15, GPIO.FALLING, callback=right_button, bouncetime=200)
   except Exception as e:
      print('exception thrown by GPIO'.format(e))
      return

   screen = InitDisplay()
   raceLength = GetRaceLength(screen)

   while True:
      # id, name, power, sprint, rotation, attackdist, attackpower, attacklength
      playerBike = Bike(1, "Player", 7000, 0, 0, 0, 0, 0)
      computerBike1 = Bike(2, "Chris Frome", 7500, 2000, 0, random.randint(0, raceLength), 9000, 1000)
      computerBike2 = Bike(3, "Peter Sagan", 7000, 3000, 0, random.randint(0, raceLength), 9000, 1000)
      computerBike3 = Bike(4, "Geraint Thomas", 6700, 1000, 0, random.randint(0, raceLength), 9000, 1000)
      computerBike4 = Bike(5, "Vincenzo Nibali", 8000, 1000, 0, random.randint(0, raceLength), 9000, 1000)

      computerBikes = [ computerBike1, computerBike2, computerBike3, computerBike4 ]

      raceStartTime = time.time()
      run(screen, playerBike, computerBikes, raceLength)

      DisplayPodium(screen, playerBike, computerBikes, raceStartTime)

      raceLength = GetRaceLength(screen)
  
if __name__=="__main__":
   main()

 
Route.py:

 
#!/usr/bin/python
import random
import math


class Route:

   def GenerateRoute(self,length):
      currentgradient = 0.0
      currentheight = 0.0
      self.route= []
      position = 0

      while position < length:
         currentheight = currentheight + currentgradient
         if currentheight>1000:
            currentheight = 1000
            currentgradient = 0.0
         if currentheight<0:
            currentheight=0
            currentgradient = 0.0

         self.route.append(currentheight)

         #print (currentheight, currentgradient)

         if random.random()>0.8:
            # change the gradient
            # gradient can only be between -1 and 1
            currentgradient = currentgradient + random.random()*0.1-0.05
            if currentgradient>1.0:
               currentgradient = 1.0
            if currentgradient<-1.0:
               currentgradient = -1.0
      
         position = position + 1


   def GenerateScenery(self,length):
      self.scenery={}

      position = 0

      while position < self.length-1000:
         if random.random()<0.007:
            #put a tree here
            self.scenery[position]="tree"
         else:
            if random.random()<0.0007:
               #put a cafe here
               self.scenery[position]="cafe"
               position = position + 120
            else:
               if random.random()<0.0007:
                  #put a bookshop here
                  self.scenery[position]="bookshop"
                  position = position + 120
               else:
                  if random.random()<0.0007:
                     #put a giftshop here
                     self.scenery[position]="giftshop"
                     position = position + 120

         position = position + 1

      # put the finish line
      self.scenery[length+50]="finish"

      self.scenery[length-900] = "flamerouge"
      self.scenery[length-200] = "crowd"

   def GetHeight(self,position):

      if position>=self.length:
         return self.route[self.length-1]
      else:
         return self.route[position]

   def GetLength(self):
      return self.length

   def GetScenery(self,position):
      if position in self.scenery:
         return self.scenery[position]
      else:
         return ""

   def __init__(self, len):

      self.length = len
      self.GenerateRoute(len)
      self.GenerateScenery(len)