diff --git a/requirements.txt b/requirements.txt index 12a230e6797..0e406e926ca 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,7 +22,7 @@ stravaweblib tenacity numpy tzlocal -fit-tool +garmin-fit-sdk haversine==2.8.0 garth pycryptodome diff --git a/run_page/gpxtrackposter/track.py b/run_page/gpxtrackposter/track.py index 3172db26a1c..ed94da8cfc3 100644 --- a/run_page/gpxtrackposter/track.py +++ b/run_page/gpxtrackposter/track.py @@ -12,14 +12,8 @@ import lxml import polyline import s2sphere as s2 -from fit_tool.fit_file import FitFile -from fit_tool.profile.messages.activity_message import ActivityMessage -from fit_tool.profile.messages.device_info_message import DeviceInfoMessage -from fit_tool.profile.messages.file_id_message import FileIdMessage -from fit_tool.profile.messages.record_message import RecordMessage -from fit_tool.profile.messages.session_message import SessionMessage -from fit_tool.profile.messages.software_message import SoftwareMessage -from fit_tool.profile.profile_type import Sport +from garmin_fit_sdk import Decoder, Stream +from garmin_fit_sdk.util import FIT_EPOCH_S from polyline_processor import filter_out from rich import print from tcxreader.tcxreader import TCXReader @@ -31,6 +25,7 @@ run_map = namedtuple("polyline", "summary_polyline") IGNORE_BEFORE_SAVING = os.getenv("IGNORE_BEFORE_SAVING", False) +SEMICIRCLE = 11930465 class Track: @@ -91,9 +86,12 @@ def load_fit(self, file_name): # (for example, treadmill runs pulled via garmin-connect-export) if os.path.getsize(file_name) == 0: raise TrackLoadError("Empty FIT file") - - fit = FitFile.from_file(file_name) - self._load_fit_data(fit) + stream = Stream.from_file(file_name) + decoder = Decoder(stream) + messages, errors = decoder.read(convert_datetimes_to_dates=False) + if len(errors) > 0: + print(f"FIT file read fail: {errors}") + self._load_fit_data(messages) except Exception as e: print( f"Something went wrong when loading FIT. for file {self.file_names[0]}, we just ignore this file and continue" @@ -223,59 +221,55 @@ def _load_gpx_data(self, gpx): ) self.moving_dict = self._get_moving_data(gpx) - def _load_fit_data(self, fit: FitFile): + def _load_fit_data(self, fit: dict): _polylines = [] self.polyline_container = [] + message = fit["session_mesgs"][0] + self.start_time = datetime.datetime.utcfromtimestamp( + (message["start_time"] + FIT_EPOCH_S) + ) + self.run_id = self.__make_run_id(self.start_time) + self.end_time = datetime.datetime.utcfromtimestamp( + (message["start_time"] + FIT_EPOCH_S + message["total_elapsed_time"]) + ) + self.length = message["total_distance"] + self.average_heartrate = ( + message["avg_heart_rate"] if "avg_heart_rate" in message else None + ) + self.type = message["sport"].lower() - for record in fit.records: - message = record.message - - if isinstance(message, RecordMessage): - if message.position_lat and message.position_long: - _polylines.append( - s2.LatLng.from_degrees( - message.position_lat, message.position_long - ) - ) - self.polyline_container.append( - [message.position_lat, message.position_long] - ) - elif isinstance(message, SessionMessage): - self.start_time = datetime.datetime.utcfromtimestamp( - message.start_time / 1000 - ) - self.run_id = message.start_time - self.end_time = datetime.datetime.utcfromtimestamp( - (message.start_time + message.total_elapsed_time * 1000) / 1000 - ) - self.length = message.total_distance - self.average_heartrate = ( - message.avg_heart_rate if message.avg_heart_rate != 0 else None - ) - self.type = Sport(message.sport).name.lower() - - # moving_dict - self.moving_dict["distance"] = message.total_distance - self.moving_dict["moving_time"] = datetime.timedelta( - seconds=message.total_moving_time - if message.total_moving_time - else message.total_timer_time - ) - self.moving_dict["elapsed_time"] = datetime.timedelta( - seconds=message.total_elapsed_time - ) - self.moving_dict["average_speed"] = ( - message.enhanced_avg_speed - if message.enhanced_avg_speed - else message.avg_speed - ) - - self.start_time_local, self.end_time_local = parse_datetime_to_local( - self.start_time, self.end_time, self.polyline_container[0] + # moving_dict + self.moving_dict["distance"] = message["total_distance"] + self.moving_dict["moving_time"] = datetime.timedelta( + seconds=message["total_moving_time"] + if "total_moving_time" in message + else message["total_timer_time"] + ) + self.moving_dict["elapsed_time"] = datetime.timedelta( + seconds=message["total_elapsed_time"] ) - self.start_latlng = start_point(*self.polyline_container[0]) - self.polylines.append(_polylines) - self.polyline_str = polyline.encode(self.polyline_container) + self.moving_dict["average_speed"] = ( + message["enhanced_avg_speed"] + if message["enhanced_avg_speed"] + else message["avg_speed"] + ) + for record in fit["record_mesgs"]: + if "position_lat" in record and "position_long" in record: + lat = record["position_lat"] / SEMICIRCLE + lng = record["position_long"] / SEMICIRCLE + _polylines.append(s2.LatLng.from_degrees(lat, lng)) + self.polyline_container.append([lat, lng]) + if len(self.polyline_container) > 0: + self.start_time_local, self.end_time_local = parse_datetime_to_local( + self.start_time, self.end_time, self.polyline_container[0] + ) + self.start_latlng = start_point(*self.polyline_container[0]) + self.polylines.append(_polylines) + self.polyline_str = polyline.encode(self.polyline_container) + else: + self.start_time_local, self.end_time_local = parse_datetime_to_local( + self.start_time, self.end_time, None + ) def append(self, other): """Append other track to self.""" diff --git a/run_page/gpxtrackposter/utils.py b/run_page/gpxtrackposter/utils.py index b2234c97484..6b371ec1507 100644 --- a/run_page/gpxtrackposter/utils.py +++ b/run_page/gpxtrackposter/utils.py @@ -129,16 +129,19 @@ def format_float(f): def parse_datetime_to_local(start_time, end_time, point): - # just parse the start time, because start/end maybe different - offset = start_time.utcoffset() - if offset: - return start_time + offset, end_time + offset - lat, lng = point - try: - timezone = get_tz(lng=lng, lat=lat) - except: - # just a little trick when tzfpy support windows will delete this + if not point: + timezone = "Asia/Shanghai" + else: + # just parse the start time, because start/end maybe different + offset = start_time.utcoffset() + if offset: + return start_time + offset, end_time + offset lat, lng = point - timezone = tf.timezone_at(lng=lng, lat=lat) + try: + timezone = get_tz(lng=lng, lat=lat) + except: + # just a little trick when tzfpy support windows will delete this + lat, lng = point + timezone = tf.timezone_at(lng=lng, lat=lat) tc_offset = datetime.now(pytz.timezone(timezone)).utcoffset() return start_time + tc_offset, end_time + tc_offset