Skip to content

Commit

Permalink
fix: yihong0618#584 change fit-tool to garmin-fit-sdk (yihong0618#590)
Browse files Browse the repository at this point in the history
* fix: 584 change fit-tool to garmin-fit-sdk

* feat: garmin - handle gpx(if exist) when sync with fit

* fix import error

* doc: SEMICIRCLE

* fix: wrap_device_info using fit-tool

---------

Co-authored-by: NaturezzZ <naturezzz@outlook.com>
  • Loading branch information
2 people authored and Kugin committed Sep 13, 2024
1 parent aa71336 commit 7648dca
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 106 deletions.
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ tenacity
numpy
tzlocal
fit-tool
garmin-fit-sdk
haversine==2.8.0
garth
pycryptodome
Expand Down
41 changes: 5 additions & 36 deletions run_page/garmin_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,7 @@ async def download_activity(self, activity_id, file_type="gpx"):
response.raise_for_status()
return response.read()

async def upload_activities_original_from_strava(
self, datas, use_fake_garmin_device=False
):
async def upload_activities_original(self, datas, use_fake_garmin_device=False):
print(
"start upload activities to garmin!, use_fake_garmin_device:",
use_fake_garmin_device,
Expand Down Expand Up @@ -154,38 +152,6 @@ async def upload_activities_original_from_strava(
print("garmin upload failed: ", e)
await self.req.aclose()

async def upload_activity_from_file(self, file):
print("Uploading " + str(file))
f = open(file, "rb")

file_body = BytesIO(f.read())
files = {"file": (file, file_body)}

try:
res = await self.req.post(
self.upload_url, files=files, headers=self.headers
)
f.close()
except Exception as e:
print(str(e))
# just pass for now
return
try:
resp = res.json()["detailedImportResult"]
print("garmin upload success: ", resp)
except Exception as e:
print("garmin upload failed: ", e)

async def upload_activities_files(self, files):
print("start upload activities to garmin!")

await gather_with_concurrency(
10,
[self.upload_activity_from_file(file=f) for f in files],
)

await self.req.aclose()


class GarminConnectHttpError(Exception):
def __init__(self, status):
Expand Down Expand Up @@ -243,7 +209,7 @@ async def download_garmin_data(client, activity_id, file_type="gpx"):
elif file_info.filename.endswith(".gpx"):
os.rename(
os.path.join(folder, f"{activity_id}_ACTIVITY.gpx"),
os.path.join(folder, f"{activity_id}.gpx"),
os.path.join(FOLDER_DICT["gpx"], f"{activity_id}.gpx"),
)
else:
os.remove(os.path.join(folder, file_info.filename))
Expand Down Expand Up @@ -362,4 +328,7 @@ async def download_new_activities(
)
)
loop.run_until_complete(future)
# fit may contain gpx(maybe upload by user)
if file_type == "fit":
make_activities_file(SQL_FILE, FOLDER_DICT["gpx"], JSON_FILE, file_suffix="gpx")
make_activities_file(SQL_FILE, folder, JSON_FILE, file_suffix=file_type)
120 changes: 60 additions & 60 deletions run_page/gpxtrackposter/track.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -32,6 +26,13 @@

IGNORE_BEFORE_SAVING = os.getenv("IGNORE_BEFORE_SAVING", False)

# Garmin stores all latitude and longitude values as 32-bit integer values.
# This unit is called semicircle.
# So that gives 2^32 possible values.
# And to represent values up to 360° (or -180° to 180°), each 'degree' represents 2^32 / 360 = 11930465.
# So dividing latitude and longitude (int32) value by 11930465 will give the decimal value.
SEMICIRCLE = 11930465


class Track:
def __init__(self):
Expand Down Expand Up @@ -91,9 +92,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 errors:
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"
Expand Down Expand Up @@ -223,59 +227,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 self.polyline_container:
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."""
Expand Down
23 changes: 13 additions & 10 deletions run_page/gpxtrackposter/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit 7648dca

Please sign in to comment.