Garmin Forerunner to GPX with heartrate as elevation (altitude)

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.
-
Grab GarminTools from here.
-
Plug in the Forerunner and invoke
garmin_save_runs
-
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]
#!/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())
- 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
#!/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())
-
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<ele>
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)</ele>
-
Open GPX file in Google Earth and play with the settings (won’t double up their copious help here)