Skip to main content

Converting HRM Files to TCX

· 5 min read
Polar WebSync logo

I recently posted an AWK script for combining GPX and HRM files into TCX format. This script is really handy when you have GPX files with or without matching HRM files, but what if you have HRM files without GPX files?

It did not immediately occur to me that there'd be any value in converting lone HRM files to TCX, since I think of TCX as being geographic location focused (which is not strictly true). However, prompted by Conrad, it became apparent that I could indeed convert HRM files (without GPX files) to TCX, and indeed, I already had 30 or so such HRM files that I'd been entering into Strava manually (stationary trainer sessions, and weights workouts).

So, here's the resulting AWK script:

# hrm2tcx.awk by Paul Colby (https://colby.id.au), no rights reserved ;)
# $Id: hrm2tcx.awk 296 2012-02-24 11:22:18Z paul $

BEGIN {
# Skip to the HR data in the HRM file.
DISTANCE=0 # Distance is *required* by the TCX format.
FS="="
while ((!FOUND_HRDATA) && (getline > 0)) {
if ($1 == "Version") {
HRM_VERSION=$2
} else if ((HRM_VERSION <= 105) && ($1 == "Mode")) {
FLAG=int(substr($2,1,1)) # First integer flag (0, 1 or 3).
HAVE_ALTITUDE=(FLAG == 1) ? 1 : 0
HAVE_CADENCE=(FLAG == 0) ? 1 : 0
IMPERIAL_UNITS=int(substr($2,3,1)); # Third bit flag (0 or 1).
} else if ((HRM_VERSION >= 106) && ($1 == "SMode")) {
HAVE_ALTITUDE=int(substr($2,3,1)) # Third bit flag (0 or 1).
HAVE_CADENCE=int(substr($2,2,1)) # Second bit flag (0 or 1).
HAVE_SPEED=int(substr($2,1,1)) # First bit flag (0 or 1).
IMPERIAL_UNITS=int(substr($2,8,1)); # Eighth bit flag (0 or 1).
} else if ($1 == "Date") {
START_TIME=substr($2,1,4)" "substr($2,5,2)" "substr($2,7,2)
} else if ($1 == "StartTime") {
START_TIME=START_TIME" "substr($2,1,2)" "substr($2,4,2)" "substr($2,7,2)
START_TIME=mktime(START_TIME)
} else if ($1 == "Length") {
DURATION=$2
} else if ($1 == "Interval") {
HRM_INTERVAL=int($2)
} else if ($1 == "[Trip]") {
getline DISTANCE # We'll use this one :)
if (IMPERIAL_UNITS > 0) DISTANCE=(DISTANCE*160.9344); # 1/10 miles to meters.
else DISTANCE=(DISTANCE*100); # 1/10 km to meters.
getline ASCENT # Not used.
getline TOTAL_TIME # Not used.
getline AVG_ALTITUDE # Not used.
getline MAX_ALTITUDE # Not used.
getline AVG_SPEED # Not used.
getline MAX_SPEED # We'll use this one :)
if (IMPERIAL_UNITS > 0) MAX_SPEED=(MAX_SPEED*160.9344/60.0/60.0); # 1/10 mph to m/s.
else MAX_SPEED=(MAX_SPEED*100.0 /60.0/60.0); # 1/10 km/h to m/s.
getline ODOMETER # Not used.
} else if (($1 == "[HRData]") || ($1 == "[HRData]\r")) {
FOUND_HRDATA=NR
}
}
FS=" "

printf "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\" ?>\n\
<TrainingCenterDatabase xmlns=\"http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2\"\
xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\
xsi:schemaLocation=\"http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2\
http://www.garmin.com/xmlschemas/TrainingCenterDatabasev2.xsd\">\n"

printf "\n <Activities>\n"
if (!SPORT) SPORT=(HAVE_CADENCE) ? "Biking" : "Other";
printf " <Activity Sport=\"%s\">\n", SPORT
printf " <Id>%s%s</Id>\n <Lap StartTime=\"%s%s\">\n",
strftime("%Y-%m-%dT%H:%M:%S", START_TIME), TIMEZONE,
strftime("%Y-%m-%dT%H:%M:%S", START_TIME), TIMEZONE
split(DURATION, DURATION_ARRAY, ":");
DURATION_NUMBER=DURATION_ARRAY[1]*60*60 + DURATION_ARRAY[2]*60 + DURATION_ARRAY[3];
printf " <TotalTimeSeconds>%s</TotalTimeSeconds>\n", DURATION_NUMBER
printf " <DistanceMeters>%f</DistanceMeters>\n", DISTANCE
if (MAX_SPEED) {
printf " <MaximumSpeed>%f</MaximumSpeed>\n", MAX_SPEED
}
printf " <Calories>0</Calories>\n"
printf " <Intensity>Active</Intensity>\n <TriggerMethod>Manual</TriggerMethod>\n"
printf " <Track>\n"
DISTANCE=0
}

{
if (NF) {
printf " <Trackpoint>\n"
TIMESTAMP=START_TIME + ((NR-FOUND_HRDATA-1) * HRM_INTERVAL);
printf " <Time>%s%s</Time>\n", strftime("%Y-%m-%dT%H:%M:%S", TIMESTAMP), TIMEZONE
if ((HAVE_ALTITUDE == 0) && (ALTITUDE > 0)) {
printf " <AltitudeMeters>%f</AltitudeMeters>\n", ALTITUDE
ALTITUDE=0
}
if (HAVE_ALTITUDE > 0) {
ALTITUDE=(HRM_VERSION <= 105) ? ALTITUDE=$3 : ALTITUDE=$(2+HAVE_SPEED+HAVE_CADENCE);
if (HRM_VERSION <= 102) ALTITUDE=(ALTITUDE*10);
if (IMPERIAL_UNITS > 0) ALTITUDE=(ALTITUDE/0.3048); # feet to meters.
printf " <AltitudeMeters>%f</AltitudeMeters>\n", ALTITUDE
}
if (HAVE_SPEED) {
if (IMPERIAL_UNITS > 0) SPEED=($2*160.9344/60.0/60.0); # 1/10 mph to m/s.
else SPEED=($2*100.0 /60.0/60.0); # 1/10 km/h to m/s.
DISTANCE=DISTANCE + (SPEED * HRM_INTERVAL)
printf " <DistanceMeters>%f</DistanceMeters>\n", DISTANCE
}
if ($1) {
printf " <HeartRateBpm xsi:type=\"HeartRateInBeatsPerMinute_t\">\n"
printf " <Value>%s</Value>\n", $1
printf " </HeartRateBpm>\n"
}
if (HAVE_CADENCE)
printf " <Cadence>%s</Cadence>\n", $(2+HAVE_SPEED)
printf " </Trackpoint>\n"
}
}

END {
printf " </Track>\n </Lap>\n"
printf " </Activity>\n </Activities>\n"

split("$Revision: 296 $", REVISION, " ")
split("$Date: 2012-02-24 22:22:18 +1100 (Fri, 24 Feb 2012) $", DATE, " ")
printf "\n <Author xsi:type=\"Application_t\"> \n\
<Name>Paul Colby's HRM to TCX Converter</Name> \n\
<Build> \n\
<Version> \n\
<VersionMajor>1</VersionMajor> \n\
<VersionMinor>1</VersionMinor> \n\
<BuildMajor>0</BuildMajor> \n\
<BuildMinor>%d</BuildMinor> \n\
</Version> \n\
<Type>Internal</Type> \n\
<Time>%sT%s%s</Time> \n\
<Builder>PaulColby</Builder> \n\
</Build> \n\
<LangID>EN</LangID> \n\
<PartNumber>636-F6C62-80</PartNumber> \n\
</Author>\n", REVISION[2], DATE[2], DATE[3], DATE[4]

printf "\n</TrainingCenterDatabase>\n"
}

(You can download it from this direct link, or from the files list at the end of this article).

Usage

Usage is as follows:

gawk -f hrm2tcx.awk [-v ALTITUDE=n.n] [-v TIMEZONE=Z|+nnnn] file.hrm > file.tcx

How it Works

Its pretty straight-forward. In many ways, its really just a sub-set of my gpx2tcx.awk script, so rather than repeat myself, check out my previous Combining GPX and HRM Files into TCX Format post instead.

Windows Command Shell

I've also written a simple Windows command script to automatically call the above AWK script on all HRM files in the current directory which do not already have matching TCX files, nor matching GPX files (if you have matching GPX files, I suggest you use my Windows gpx2tcx batch file for those instead).

:: hrm2tcx.cmd by Paul Colby (https://colby.id.au), no rights reserved ;)
:: $Id: hrm2tcx.cmd 298 2012-02-24 22:09:33Z paul $
@echo off

:: Optional: uncomment the following line to change UTC timezones.
set TIMEZONE=+11:00

:: Optional: uncomment the following line to set the altitude of the first point.
:: This will do nothing if your HRM files actually include altitude data, but helps
:: devices like the RCX5 with no altitude data, and sites like Strava that don't like that.
::set ALTITUDE=1.0

:: Update this path to include the location(s) UnxUtils is insstalled.
set PATH=%PATH%;C:\Program Files\UnxUtils\usr\local\wbin;C:\Program Files (x86)\UnxUtils\usr\local\wbin

:: Jump the to "main" block.
goto main

:convert
if not exist "%~1.hrm" goto :EOF
echo Processing %~1...
gawk.exe -f "hrm2tcx.awk" -v ALTITUDE=%ALTITUDE% -v TIMEZONE=%TIMEZONE% "%~1.hrm" > %~1.tcx
goto :EOF

:main
FOR /f %%A IN ( 'ls.exe -1 *.gpx *.hrm *.tcx 2^>^&1 ^| sed.exe -e "s/\.[^.]*$//" ^| uniq.exe -c ^| sed -ne "s/^ *1.//p"' ) DO call::convert %%A

pause

Enjoy!! :)

Attachments