Garmin Forerunner to GPX with heartrate as elevation (altitude)

Cycling Back Home

GPX track in Google Earth with heart rate as elevation/altitude (track height)


It seems like I’ve been working on this for ages but I finally managed to get some kind of output from the Garmin Forerunner 301 that Miles Chalcraft kindly leant me.

  1. Grab GarminTools from here.
  2. Plug in the Forerunner and invoke garmin_save_runs
  3. Convert the binary *.gmn file and redirect it to a funky xml file that needs fixing with garmin_dump [input file.gmn] > [output_file.xml]
  4. #!/usr/bin/env python
    #-*- coding:utf-8 -*-
    #
    # A script to mend the broken xml that garmintools generates and
    # forces the sax parser to choke on a fatal error
    #
    # Problem 1: the whole file needs wrapping in a tag, so I wrap it with
    # 
    #
    # Problem 2: There seems to be some dodgy encoding going on, probably
    # with reading straight from the garmin device, so I just loose the
    # offending line which starts 
    #
    # Daniel Belasco Rogers 2011
    
    
    from sys import exit
    import os
    from shutil import copy
    from optparse import OptionParser
    
    # frivolous variables for stout formatting
    rockdots = "+"
    dotnum = 50
    
    
    def fileBackup(filename):
        """
        save a copy of the file as a backup appending ~ to the filename
        """
        copy(filename, filename + "~")
        print
        print rockdots * dotnum
        print "Wrote backup '%s'" % (filename + "~")
        print rockdots * dotnum
        print
        return
    
    
    def insertTopBottom(filename):
        """
        open the file, read its contents, check for the xml tag and if not
        there, call the backup routine and then insert the opening and
        closing xml tags at the beginning and end of the file
        """
        with open(filename, "r+") as f:
            # read everything in the file into a list of lines
            old = f.readlines()
            # check if this file has already been altered
            if old[0].startswith("< ?xml"):
                print
                print rockdots * dotnum
                print """The file you specified has already seems
    to have been altered
    
    This is not an error
    Script ends here"""
                print rockdots * dotnum
                print
                return
            fileBackup(filename)
            # rewind the file to the beginning
            f.seek(0)
            fileheader = """
    
    """
            f.write(fileheader)
            for line in old:
                # don't copy the dodgy encoding line in the output file
                if not line.startswith(" ")
            print rockdots * dotnum
            print """Re-written the xml file inserting proper headers and
    deleting bad encoding
    Script ends sucessfully here"""
            print rockdots * dotnum
            print
    
    
    def main():
        """
        parse the arguments passed, check for file and call the functions
        that create backup and inserting xml tags
        """
        usage = "usage: %prog /path/to/xml/file.xml"
        parser = OptionParser(usage, version="%prog 1.0")
        (options, args) = parser.parse_args()
        if len(args) != 1:
            parser.error("specify a file path")
        filename = args[0]
        insertTopBottom(filename)
    
    
    if __name__ == '__main__':
        exit(main())
    
  5. Invoke it like python repairForerunnerXML.py path/to/broken/xml.xml This will save the original file as a backup with a tilde extension, should you ever need it again and wrap the file in an xml tag and remove bad encoding lines
  6. #!/usr/bin/env python
    #-*- coding:utf-8 -*-
    #
    # ForerunnerXML2GPX
    #
    #
    # Converts output of garmintools xml from Garmin Forerunner to GPX
    #
    # Replaces elevation (ele) with heartrate (hr)
    #
    # At the moment, the namespace writing at the top of the file is a
    # hack. It seems this is a problem with lxml but could do with further
    # investigation
    #
    # Script now conflates track and track segment tags: each segment is
    # now inside a new track - this is to make Google Earth more compliant
    #
    # Copyright 2011 Daniel Belasco Rogers
    # 
    
    import sys
    import os.path
    from optparse import OptionParser
    import datetime
    try:
        from lxml import etree
    except ImportError:
        print '*' * 48
        print 'This script needs the python module lxml to work'
        print 'lxml is not currently installed'
        print 'You can get it by typing:'
        print 'sudo apt-get install python-lxml'
        print '*' * 48
        sys.exit(2)
    
    
    def convertTimeString(string):
        '''
        take the time string and return a datetime object for
        time delta comparison
        '''
        string = string.replace('T', ' ')
        string = string.replace('-', ' ')
        string = string.replace(':', ' ')
        # Knock off the Z if it's there
        if string[-1] == 'Z':
            string = string[:-1]
        findplus = string.find('+')
        if findplus <> -1:
            # just ignore the tzinfo for now - it doesn't matter for the
            # delta calculation
            string = string[:findplus]
        strlist = string.split()
        return datetime.datetime(int(strlist[0]),
                                 int(strlist[1]),
                                 int(strlist[2]),
                                 int(strlist[3]),
                                 int(strlist[4]),
                                 int(strlist[5]))
    
    
    def main():
        usage = "usage: %prog /path/to/xml/file.gpx"
        optparse = OptionParser(usage, version="%prog 0.1")
        (options, args) = optparse.parse_args()
        if len(args) != 1:
            optparse.error("""
    
    Please define input Forerunner XML file
    """)
        filename = args[0]
        if not(os.path.isfile(filename)):
            print '*' * 48
            print "input file %s does not exist" % filename
            print '*' * 48
            exit(1)
    
        parser = etree.XMLParser(remove_blank_text=True)
        doc = etree.parse(filename, parser)
        inroot = doc.getroot()
        # the namespace hack
        outroot = etree.Element('gpx',
                                attrib={"creator": "ForerunnerXML2GPX.py",
                                        "version": "1.0",
                                        "xmlns": "https://www.topografix.com/GPX/1/0"})
        # this is the bit that doesn't work about namespace at the moment
        #etree.register_namespace("", "https://www.topografix.com/GPX/1/0")
        #outroot = etree.Element('{https://www.topografix.com/GPX/1/0}gpx',
        #                    attrib={'creator': 'gpx-split', "version": "1.0"})
        timed1 = None
        newTrkseg = True
        #trk = etree.SubElement(outroot, 'trk')
        for element in inroot.iter('point'):
            time = element.get('time')
            lat = element.get('lat')
            lon = element.get('lon')
            hr = element.get('hr')
            # start writing new nodes
            # work out if you need to start a new trk element by finding
            # out time delta between previous point and current
            timed2 = convertTimeString(time)
            if timed1 is None: # the first time
                timed1 = timed2
            timegap = (timed2 - timed1)
            if timegap > datetime.timedelta(hours=1):
                newTrkseg = True
            if lat is not None:
                if newTrkseg:
                    trk = etree.SubElement(outroot, 'trk')
                    name = etree.SubElement(trk, 'name')
                    name.text = time
                    trkseg = etree.SubElement(trk, 'trkseg')
                    newTrkseg = False
                trkpt = etree.SubElement(trkseg, 'trkpt')
                trkpt.set('lat', lat)
                trkpt.set('lon', lon)
                ele = etree.SubElement(trkpt, 'ele')
                if hr is None:
                    hr = '0'
                ele.text = hr
                tagtime = etree.SubElement(trkpt, 'time')
                tagtime.text = time
            else:
                newTrkseg = True
            timed1 = convertTimeString(time)
    
        newfilename = os.path.splitext(filename)[0] + '.gpx'
        # why doesn't this always work?
    #     if (os.path.isfile(filename)):
    #         print """
    # file
    # %s
    # already exists, please check and try again
    # """ % newfilename
    #         sys.exit(2)
        print 'writing new file %s' % newfilename
        with open(newfilename, 'w') as f:
            f.write(etree.tostring(outroot,
                           encoding="UTF-8",
                           xml_declaration=True,
                           pretty_print=True))
    
    
    if __name__ == '__main__':
        sys.exit(main())
    
  7. Invoke thusly: python ForerunnerXML2GPX.py path/to/fixed/xml.xml This produces a GPX file with the same name as the xml file. It writes heart rate to the tag, effectively mapping heart rate onto altitude for an interesting plot. When the Forerunner hasn’t recorded heart rate, only position, the elevation is set to 0. If the Forerunner has only captured heart rate and no position data, it is skipped (obviously)
  8. Open GPX file in Google Earth and play with the settings (won’t double up their copious help here)
This entry was posted in Code, GPS, Python, Software, Walking and tagged , , , , , , , . Bookmark the permalink.