Skip to content

Instantly share code, notes, and snippets.

@marcov
Last active September 1, 2023 16:05
Show Gist options
  • Save marcov/1717b6bcc5902f3a38af384ae894c0b3 to your computer and use it in GitHub Desktop.
Save marcov/1717b6bcc5902f3a38af384ae894c0b3 to your computer and use it in GitHub Desktop.
Recover a accidentally paused ride on the Wahoo Fitness App

Recover a paused ride on the Wahoo Fitness App

If you like me use the Wahoo Fitness App to track your bike rides, from time to time it may happen to inadvertitely pause the recording in the middle of a ride.

Sometimes this really sucks cause it will of course not include the full mileage and duration from your ride and also skip valuable Strava segments recorded.

What I found out is that the app is actually recording all the GPS points of the track, but when exporting it will split it up is as many additional tracks as the times your have paused/resumed.

So with some reverse engineering I found a way to avoid this and to have a single and complete track when exporting to online services like Strava. The only limitation I found is that the app will continue to show the rides with the reduced mileage & durations (not a big deal if you export your tracks anyway).

So here's what you need to do, assuming you are using Wahoo Fitness on iOS:

  • Connect the phone to your PC with the USB cable, open up iTunes and copy the file WorkoutData.sqlite locally. Make a backup of this file and run the following instructions on a copy of it.

  • Open the file with DB Browser for SQLITE

  • Identify the value of ZWORKOUTDATA ID (Workout ID) for your ride, e.g. by inspecting the table ZWORKOUTSEGMENT (the higher the ID the most recent the activity).

  • Identify your ZSESSION ID for your ride by inspectin the ZWFWORKOUT table (the higher the ID the most recent the activity).

Update (April 27th, 2020)

Users reported not being able to use the original method on newer versions of the Wahoo Fitness App.

If the original method described below does not work for you, you can try this:

@MarsFlyer has come up with a way to dump ride data from the SQLITE db using SQL queries, and generate a GPX file from this data. See here.

Original method

This method involves altering the SQLITE db in order to merge the paused segment into a single ride, and upload it back to the iPhone.

  • Run the following queries:
UPDATE ZWFBIKECADENCESAMPLE SET ZISWORKOUTACTIVE = 1 WHERE ZWORKOUTDATA = [ZWOID];

UPDATE ZWFBIKESPEEDSAMPLE SET ZISWORKOUTACTIVE = 1 WHERE ZWORKOUTDATA = [ZWOID];

UPDATE ZWFLOCATIONSAMPLE SET ZISWORKOUTACTIVE = 1 WHERE ZWORKOUTDATA = [ZWOID];

UPDATE ZWFTIMEINTERVAL SET ZISWORKOUTACTIVE = 1 WHERE ZWORKOUTSEGMENT = [ZWOID];

UPDATE ZWFWORKOUT SET ZPAUSEDDURATION = 0 WHERE ZSESSION = [ZWOSSID];

UPDATE ZWFWORKOUTSEGMENT SET ZPAUSEDDURATION = 0 WHERE ZWORKOUTDATA = [ZWOID];

Update (May 1st, 2020)

On newer version of the app, you also need to update the ZWFWORKOUT table as suggested by @slingbike:

Seems there are three fields in ZWFWORKOUT that perhaps were not there previously: ZCURRENTSTATE, ZDELETESTATUS, AND ZHIDDEN [...] set to 0, 0, and "null," respectively ...

  • Save the changes and upload the file back on your phone using iTunes.
@TheOtherDude
Copy link

Thanks, that method worked for me as well.

@allisonsc27
Copy link

Hi all! If I sent someone my file and $15 (price negotiable :) ) would you be able to help fix this in my file and send back? I unfortunately have no experience in sql/coding/etc but am OCD about getting all my miles in my ride

@marcov
Copy link
Author

marcov commented Dec 28, 2022

@allisonsc27, lol, I can empathize with the OCD, especially if those miles matter for the 2022 final balance :-)
Can you upload your file somewhere (e.g. dropbox) we can download it, if there's no personal data in it?

@allisonsc27
Copy link

@allisonsc27, lol, I can empathize with the OCD, especially if those miles matter for the 2022 final balance :-) Can you upload your file somewhere (e.g. dropbox) we can download it, if there's no personal data in it?

Thanks, Marco! Here's the ride. https://drive.google.com/file/d/1FIyf-zR_Hpnahyp0fwFk8xEClwlmGCXv/view?usp=sharing. It'll only add about 10 slow mountain bike miles onto my ride but decided to try the Rapha festive 500 this year and every mile counts :)

@tinrobot2000
Copy link

tinrobot2000 commented Dec 31, 2022

Hi @marcov, @MarsFlyer & all,
Thanks for this info - I have been digging into the WF DB as I have missed some lap points on some recent rides and would like to add them back in before uploading to Strava. (I was also thinking of creating an auto lap script as well). There's very little info available on hacking the WF db - so this discussion is appreciated

I can only assume that Wahoo are using Jan 1, 2001 (1st day of millennium) as their epoch hence +31 years from the unix epoch (so will it break in 2069?)

Python script I am using to dump a sample (default n=100) of the DB to Excel sheets and tables for anyone stumbling across this gist. If no workout is specified will extract from the start. (caveat emptor) You can use fetch=-1 to get everything

import sqlite3
from pathlib import Path
import xlsxwriter


def dumptoexcel(cursor, excelpath, fetch=100, workout=None):
    """Dump Wahoo sqlite database to excel sheets and tables"""
    # Helper to remove bytes-like objects
    def clean(data):
        data = [list(i) for i in data]  # convert to lists
        for row, i in enumerate(data):
            for col, j in enumerate(i):
                if isinstance(j, (bytes, bytearray)):
                    data[row][col] = ""
        return data

    workbook = xlsxwriter.Workbook(excelpath)

    # Iterate tables
    for table in gettables(cursor):
        # Collect data - if this is none we skip creating worksheet
        headers = [
            item[0]
            for item in cursor.execute(
                f"SELECT p.name FROM pragma_table_info('{table}') p"
            ).fetchall()
        ]

        if workout and "ZWORKOUTDATA" in headers:
            data = cursor.execute(
                f"SELECT * FROM '{table}' WHERE ZWORKOUTDATA={workout} LIMIT {fetch}"
            ).fetchall()
        else:
            data = cursor.execute(f"SELECT * FROM '{table}' LIMIT {fetch}").fetchall()

        if data:
            data = clean(data)
            # Headers as dicts for Excel tables
            headers = [{"header": item} for item in headers]

            worksheet = workbook.add_worksheet(table)            
            worksheet.add_table(
                0, 0, len(data), len(headers) - 1, {"columns": headers, "data": data}
            )

    workbook.close()

    return True


def gettables(cursor):
    return [table[1] for table in cursor.execute("PRAGMA table_list;").fetchall()]


if __name__ == "__main__":
    dbpath = Path.cwd() / "WorkoutData.sqlite"
    cursor = sqlite3.Connection(dbpath).cursor()
    excelpath = Path.cwd() / "wahoo_sqlite_excel_dump.xlsx"
    dumptoexcel(cursor, excelpath, workout=20)

@tinrobot2000
Copy link

tinrobot2000 commented Jan 7, 2023

I have written an sqlite query that can extract a workout (active segments) as a complete GPX here -> link

The query uses the variable :workout to select the workout from the database - refer to ZWFWORKOUT, Z_PK column to get the number
And :limit to restrict the number of trackpoints (I use this for testing)

Includes heart-rate, cadence and temperature based on the nearest prior timestamp to the location timestamp. I have done quite a bit of testing to optimise the selections as some of the tables have a lot of data but no index on timestamp (ZWFATMOSPRESSURESAMPLE I'm looking at you!). I also managed to choose data on absolute closest timestamp, however this was very slow and comparing to Wahoo .fit data it appears the app uses the timestamp prior method as well
EDIT I have updated so that if extensions (hr, cadence & temp) are not available it will still return a GPX valid row

I have included the Garmin Trackpoint Extension V1 for cadence even though Strava says it won't parse yet it turns up in their own GPX output - yet to be tested
Also note that I have taken the cadence sample over ~3 seconds to smooth out spikes

@briceap
Copy link

briceap commented Jul 12, 2023

Hello everyone
Any idea why my WorkoutData.sqlite file is made of empty tables ? There's no data in it
Thanks

@fz6
Copy link

fz6 commented Sep 1, 2023

Anybody had the problem of not being able to find the sqlite file? I connected my Elemnt Bolt v1 to a PC running Ubuntu 20.04, it's mounted by MTP, I can see a bunch of directories ("maps", "exports"), but browsed through each (and I've ran find | grep -i sql), can't seem to see a sqlite file - I can see a bunch of map files, and a bunch of .fit files, but that's about it.

Edit - have also tried connecting with adb and running $ adb shell ls -lR | grep -i sql, all I can find is /data/data/com.wahoofitness.bolt/databases/BoltApp.sqlite (which has a very different table structure) and a few .so libs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment