TL;DR

Install gols and upload your runs, from your watch to Garmin Connect, automatically.

You can use the python cli on a mounted device, or bear with me a few more lines on how to automount it and run the script as soon as the USB is plugged.


The script

Yes you can

I run quite often, and over the years I tested almost all ways, well quite a number of ways, to upload my sessions to Garmin.

Recently I got great hopes with the bluetooth, but it appeared to be unreliable at best with my phone at least, the debug logs are nothing but cryptic and uninteresting and the Garmin customer support on this clueless.

I tried the VM way, which works, I just hate booting windows, but Garmin Express does the job fine.

I can’t remember how far I went the wine way, but that displeased me quite rapidly.

So I ended up using the manual upload, thanks god the watch has USB mass storage !

garmin connect

That works well, it’s just manual, would there be a way to upload that automatically ?

Show me the way

The first step has been to create a script that mimics the upload.

You can find it on the repo above, install instructions are clear I hope, nothing fancy, just some use of requests and click so that it can be run as a cli.

The fun part was to figure out how to login to Garmin in python.

url_login = 'https://sso.garmin.com/sso/login'
req_login = s.get(url_login, params=params_login)
req_login2 = s.post(url_login, data=data_login)  # we need that to authenticate further, kind like a weird way to login but...
t = req_login2.cookies.get('CASTGC')
t = 'ST-0' + t[4:] # now the auth with the cookies we got
url_post_auth = 'https://connect.garmin.com/post-auth/login'
params_post_auth = {'ticket': t}
req_post_auth = s.get(url_post_auth, params=params_post_auth) # login should be done we upload now
url_upload = 'https://connect.garmin.com/proxy/upload-service-1.1/json/upload/.fit'

It boils down to :

  1. get a cookie from the login page
  2. post the username / password with some other hidden inputs
  3. (no, no you’re not finished), change a little the cookie you get from the preceding post
  4. fire a last GET on an mysterious endpoint, more on that in a second

What’s the logic behind that scheme, some clever guys will tell me.

It took me hours to get it working.

I’m glad I found the logic in that repo: https://github.com/kjkjava/garmin-connect-export/blob/master/gcexport.py, but I still can’t figure how they guessed that endpoint.

As you can see in the image below, https://connect.garmin.com/post-auth/login never appears in the redirects, that’s a mystery for me, I checked in Burp, Chrome dev tools, Phantom, I inspected the various js files, all I know is that without it my session end up in a 403 when it attempts to upload stuff, with it it works….

using dev tools to look for the right POST url

The automount

udev or systemd ?

Ok now, to automate all that we need to run the script when the watch is USB plugged.

As the script needs the watch to be mounted, that something we should do.

At first I tried with udev and its RUN+= then reading that blog post,

Until recently, udev didn’t mind doing that via just RUN+=”/path/to/binary …”, but in recent merged systemd-udevd versions this behavior was deprecated:

and because I wanted to understand a little bit more systemd I ended up doing it differently.

On top of that the udev rule I came up with was running the script too soon, before the filesystem was mounted.

KERNEL==”sd*”, ATTRS{idVendor}==”091e”, ATTRS{idProduct}==”27af”, RUN+=”/home/user/PycharmProjects/gols/gols.sh”

I’m no systemd guru, but reading the doc, it seems the fstab entries are converted automatically.

So add in your /etc/fstab the following entry:

  • the endpoint (/media/fenix2 below) has been created by the user who will run the script
  • get your UUID running sudo blkid
  • uid and gid are of course the ids of the user and group
# garmin fenix 2UUID=489A-9E97
/media/fenix2 vfat auto,nofail,rw,user,uid=1000,gid=1000 0 2

Now the watch automatically mounts with correct permissions. Oh, don’t forget to sudo systemd daemon-reload as the fstab entries won’t be updated, this one almost made me post there

Mounts listed in /etc/fstab will be converted into native units dynamically at boot and when the configuration of the system manager is reloaded

Finally we’ll create a systemd user unit, that will run the script after the mount has been done.

systemctl --user edit gols.service --force

the unit is rather simple

[Unit]
Description=gols a little less now
Requires=media-fenix2.mount
After=media-fenix2.mount
[Service]
ExecStart=/home/user/PycharmProjects/gols/gols.sh
[Install]
WantedBy=media-fenix2.mount

Don’t forget to create the gols.sh file wherever you want but change the path in the systemd unit accordingly of course !

I put an example in the repo, it should contain the arguments you want to pass on the python script, you can check those with the help.

(gols) ➜ gols git:(master) ✗ gols — help 

Usage: gols [OPTIONS]
Options: 
-d, --directory_fit DIRECTORY Path of your .fit files on your watch mount path [required] 
-n, --notification / --no_notification Get notified
-m, --move / --no_move Move files upon upload --debug / --no_debug Set to true to see debug logs on top of info
--help Show this message and exit.

Personally I left it in my repo, and I run it with a virtual env to let my system “clean” (small rant about not being able to pip install pygobject in a virtual env, I ended up symlinking stuff which is gross).

So my gols.sh is the following, yours should be like the one below:

#!/bin/bash

/home/user/venv/gols/bin/python /home/user/PycharmProjects/gols/gols.py -d /media/fenix2/Garmin/Activity -n -m

path_to_python3 path_to_gols.py [options as seen above]

Now enable the service and reload the daemon

systemctl — user enable gols.service
systemctl — user daemon-reload

And voila, you should see the stuff running in your syslog if all went well, that’s all.


Conclusion

My next watch will be a Suntoo