Skip to content

Commit

Permalink
Handle files which only have local/relative timestamps
Browse files Browse the repository at this point in the history
In some files, perhaps created for non-GPS activities or by non-GPS devices,
all timestamps in laps and trackpoints are expressed as integer offsets.
(Probably an offset in seconds relative to when the device was turned on.)

The only "real" timestamp available is in the 'activity' message field
called 'local_timestamp', which represents the device's time in its *local*
timezone.

To process such files, we must use 'activity.local_timestamp' along with
'activity.timestamp' to calibrate the offset between the integer timestamps
and Unix epoch seconds.  However, even after doing this we *do not know* the
correct timezone for the resulting timestamps.  We emphasize this in the
ISO8601-format timestamps in the TCX output, by not appending 'Z'.

'tests/files/antfs-dump.63.fit' from dtcooper/python-fitparse is a good
example of this issue.  It contains data from a heart-rate monitor device
without GPS.
  • Loading branch information
dlenski committed Oct 1, 2021
1 parent 1e40044 commit 4a8e0e8
Showing 1 changed file with 36 additions and 13 deletions.
49 changes: 36 additions & 13 deletions fittotcx/program.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import argparse
import sys
import lxml.etree
from datetime import datetime
from fitparse import FitFile, FitParseError

try:
Expand Down Expand Up @@ -82,6 +83,15 @@ def ff(number):
return "{:.10f}".format(number).rstrip("0").rstrip(".")


def tsz(number_or_datetime, epoch_offset):
if number_or_datetime is None:
return None, ""
elif isinstance(number_or_datetime, int):
return datetime.fromtimestamp(number_or_datetime + epoch_offset), ""
else:
return number_or_datetime, "Z"


def create_element(tag, text=None, namespace=None):
namespace = NSMAP[namespace]
tag = "{%s}%s" % (namespace, tag)
Expand Down Expand Up @@ -158,8 +168,8 @@ def add_author(document):
create_sub_element(b, "BuildMinor", "0")


def add_trackpoint(element, trackpoint):
timestamp = trackpoint.get_value("timestamp")
def add_trackpoint(element, trackpoint, epoch_offset=None):
timestamp, zone = tsz(trackpoint.get_value("timestamp"), epoch_offset)
pos_lat = trackpoint.get_value("position_lat")
pos_long = trackpoint.get_value("position_long")
distance = trackpoint.get_value("distance")
Expand All @@ -168,7 +178,7 @@ def add_trackpoint(element, trackpoint):
heart_rate = trackpoint.get_value("heart_rate")
cadence = trackpoint.get_value("cadence")

create_sub_element(element, "Time", timestamp.isoformat() + "Z")
create_sub_element(element, "Time", timestamp.isoformat() + zone)

if pos_lat is not None and pos_long is not None:
pos = create_sub_element(element, "Position")
Expand Down Expand Up @@ -200,9 +210,9 @@ def add_trackpoint(element, trackpoint):
create_sub_element(tpx, "Speed", ff(speed))


def add_lap(element, activity, lap):
start_time = lap.get_value("start_time")
end_time = lap.get_value("timestamp") # opt
def add_lap(element, activity, lap, epoch_offset=None):
start_time, zone = tsz(lap.get_value("start_time"), epoch_offset)
end_time, _ = tsz(lap.get_value("timestamp"), epoch_offset) # opt

totaltime = lap.get_value("total_elapsed_time") # opt
if totaltime is None:
Expand All @@ -227,7 +237,7 @@ def add_lap(element, activity, lap):
# extensions

lapelem = create_sub_element(element, "Lap")
lapelem.set("StartTime", start_time.isoformat() + "Z")
lapelem.set("StartTime", start_time.isoformat() + zone)

if totaltime is not None:
create_sub_element(lapelem, "TotalTimeSeconds", ff(totaltime))
Expand All @@ -249,11 +259,11 @@ def add_lap(element, activity, lap):
# Add track points to lap
trackelem = create_sub_element(lapelem, "Track")
for trackpoint in activity.get_messages(name="record"):
tts = trackpoint.get_value("timestamp")
tts = tsz(trackpoint.get_value("timestamp"), epoch_offset)
if start_time <= tts:
if end_time is None or tts <= end_time:
trackpointelem = create_sub_element(trackelem, "Trackpoint")
add_trackpoint(trackpointelem, trackpoint)
add_trackpoint(trackpointelem, trackpoint, epoch_offset)


def add_activity(element, activity):
Expand All @@ -262,17 +272,30 @@ def add_activity(element, activity):
# Sport type
sport = SPORT_MAP.get(session.get_value("sport"), "Other")

# Identity (in UTC)
# Identity (along with all other FIT timestamps, normally in UTC)
epoch_offset, zone = None, "Z"
identity = session.get_value("start_time")
if isinstance(identity, int):
# Try to use activity.local_timestamp as a backup (e.g. for non-GPS activities)
act_message = next(activity.get_messages(name="activity"), None)
if act_message is None:
raise FitParseError("FIT file contains insufficient time information")

lt = act_message.get_value("local_timestamp")
ts = act_message.get_value("timestamp")
if not isinstance(lt, datetime) or not isinstance(ts, int):
raise FitParseError("FIT file contains insufficient time information")
epoch_offset, zone = int(lt.timestamp()) - ts, ""
identity = lt

actelem = create_sub_element(element, "Activity")
actelem.set("Sport", sport)
create_sub_element(actelem, "Id", identity.isoformat() + "Z")
create_sub_element(actelem, "Id", identity.isoformat() + zone)

for lap in activity.get_messages("lap"):
add_lap(actelem, activity, lap)
add_lap(actelem, activity, lap, epoch_offset)

device_info = next(activity.get_messages(name="device_info"))
device_info = next(activity.get_messages(name="device_info"), None)
if device_info is not None:
add_creator(actelem, device_info)

Expand Down

0 comments on commit 4a8e0e8

Please sign in to comment.