tl;dr - I’ve got a GitHub Repo that contains scripts for getting the cadence sensor on my exercise bike to control video games.

So, I’ve posted before about setting up my exercise bike in front of a PlayStation. It does two things:

  • Makes my exercise a bit more fun.
  • stops me gaming until 4am like a teenager and thus wrecking my job, relationships, and entire life.

I’ve got an alarm on my watch that beeps if my heartbeat drops below a target and I’ve been working my way through the best games of, like, 2007.

However, something I’ve always wanted was a system that mapped the speed of the pedals to something in the game. This weekend I got around to putting an old cadence sensor on the spin bike and writing a quick script that took the cadence sensor information and used it to control the keyboard, and thus control a simple online game.

Here’s the proof of concept:

You can see the basic code here(note: that’s a link to the commit rather than the up-to-date state) and the particular code shown in the video is this one:

from bleak import BleakClient
import asyncio
import viciouscycle
import bleak
import keyboard

def go_forward(sender, data): 
    print("Here")
    cadence=viciouscycle.decode_and_handle_measurement(sender,data) 
    if cadence is None:
        print("None") 
        return
    if cadence > 60: 
        if cadence <150: 
            print("Doing great!")         
            keyboard.hold_key('up',2) 

    else:
        print("No, do better") 



async def play():
    async with BleakClient(viciouscycle.sensor_address) as client:
        print("Start to play") 
        await client.start_notify(viciouscycle.CSC_MEASUREMENT_UUID, go_forward)
        await asyncio.sleep(60)
        await client.stop_notify(viciouscycle.CSC_MEASUREMENT_UUID)
        print("Stopped notifications")

if __name__ == "__main__":
    print("Entering Warm up") 
    print("Seeking Sensor")  
    try: 
        asyncio.run(play())
    except bleak.exc.BleakDeviceNotFoundError: 
        print("Device was NOT found")  

It’s my plan that I’ll come back to this shortly with some more complex code and, much more importantly, a setup where I’ve been able to play some games using the pedals for a few hours. I don’t expect to get as far as Mario Kat 8, but it would be nice to have something with that is low-overhead and fun.

JUNE UPDATE

Since making the proof of concept I rebuilt the overall setup so that it will be a lot easier to do things like ‘fit the laptop on the desk’ and so on. I also bought a Retrode on the basis that would be a nice way in to doing some bike controlled gaming with simple games.

JULY UPDATE

I successfully used my cadence sensor to play Road Rash 2 via the Retrode and a cheap USB gamepad.

Playing Road Rash 2

It was successful in the sense that I am much more tired by the workout that I would have been with my other gaming setup. I used some extremely simple logic (If cadence is above 80, then hold down the accelerate button) and relived my youth a little.

While doing it, I rebuild the code in general, and investigated what might be possible with the device on other games (It won’t be needed for a while, Road Rash is wearing me out at the moment). Presumably the setup of ‘hold down the accelerate button’ will work for similar retro games like Mario Kart.

Other ideas include:

  • My peddling speed changing the overall speed of the game: either peddling faster slows down a fast game like Tetris or speeds up a slow 90’s RPG
  • My peddling being the only control for very simple games like Flappy Bird or some of the button mashing sports/rhythm games (every rotation would be a press)
  • Switching off the monitor if cadence goes below a certain rate (like this)
  • Enabling the very 90’s ‘turbo fire’ if I was pedalling particularly fast.

But I should definitely work out how much control I might have first:

I have this cadence sensor. It sends one data pulse every 0.76 seconds or so (and quite a lot get lost, so the average time between received packets about 0.9 seconds) I get the impression that I get more regular updates the higher the cadence, but I don’t have particularly hard data on that and it certainly doesn’t get below 0.7 seconds or so.

That eliminates the ‘direct control’ option immediately. 0.7 seconds plus the sensor lag (it will take a while before it can tell I’ve sped up) is far to long for any sort of gaming response.

Side note - while thinking about this it occurred to me that I could get something much more fast and accurate by using something magnet based and putting a *bunch* of magnets around the crank path. That starts looking like something I might only try if I start building a full cycle computer.

While I’m here I’ll have a look at the rest of the data (my sensor is using the ‘Cycling Speed and Cadence (CSC) Profile’). An example of the raw data I get (in hex) is: “022d00c1b7” and the first byte (“02”) is a flags field.

The flags field is 1 byte (8 bits) long, and each bit represents a different piece of data.

Flag Bit Value Description
0 0x01 Cumulative Wheel Revolutions Present
1 0x02 Cumulative Crank Revolutions Present
2 0x04 Sensor Contact Status Supported
3 0x08 Multiple Sensor Locations Supported
4 0x10 Crank Length Supported
5 0x20 Bike Weight Supported
6 0x40 Power Measurement Supported
7 0x80 Energy Expenditure Supported

For me, the hex value is 02, so only the ‘Cumulative Crank Revolutions Present’ is relevant. Presumably if I spent quite a lot of money on a smart turbo trainer or power meter the other values would be present. I might have a look if I happen to have my laptop near a cycling friend’s system.

The sensor sends data (both Crank Revolutions and ‘last crank event time’) every 0.76 seconds (although it seems to skip one about one in four - so an average of one a second). Cadence can be worked out by looking at the change in rotations and time since the last pulse.

However, Cumulative Crank Revolutions is an integer so it has almost always increased by exactly one more than last time and thus almost always returns a cadence of 79 to 85 because of the sensor sampling rate.

So my code uses a n=10 buffer so that the cadence calculation is taken on the total revolutions in the last 8 to 9.5 seconds.

That gives me vastly more accurate timings (but even less responsiveness) with two small problems.

  • If I stop peddling entirely (or drastically slow down) the sensor stops sending new data - it sends the old data again - so my code thinks that the time and crank revolutions haven’t changed. So if I stop peddling it takes the code a really long time to notice and update the cadence.
  • If I stop for a moment and then speed up, the sensor sends a big update like 9 revolutions in 7 seconds.

The bottom line is that these cadence sensors are good for their central use case (measuring cadence for cyclists over reasonably long periods) but far from great for my use-case (super-fast reaction times in video gaming).

I created a test system using my Retrode and Road Rash 2 (1992) (I played this game as a child, still have a copy I can put into the Retrode, and know that it’s played by having the accelerator down for the whole race, which is fairly easy to do with my script). I’m using RetroArch to actually play via the Retrode⁰

The results are quite acceptable - and achieved the aim of making my exercise both a bit more interesting (which the other version did well) and also effective)

Sidenote: in the 15 years or so since I last properly looked at retro games emulators they have been very much enhanced. Here’s a before and after of me finding the controls for graphic enhancement:

Comparing graphics

Next Steps

  • Next time I update this post I intend to be able to show a basic GUI, better controls, effective testing, and another, more complex, game.

Daydream Goals

  • More ‘aware’ controls that recognise the game state and adapt automatically.
  • The python code directly controlling the PS4 via emulating the controller.
  • Porting the code to a Raspberry Pi (or whatever is current these days) so I don’t have to lug around my laptop
  • More advanced crank tracking using a bunch of magnets(like this but with more magnets and on a stationary bike) (Update: I was thinking about magnets and a hall effect sensor, but I actually think it would be a lot easier to have a dynamo and a voltmeter…)

⁰ OpenEmu is more popular, but as detailed here really doesn’t work well for accepting inputs from python.