Overview

I posted Access and visualize Garmin FIT data: nocturnal oxygen saturation, hike activity and GPS recently and noted that there was also a Python interface to FIT data. This notebook shows how to access the data via the Mathematica Python interface.
​
I decided to make this a separate post, to document the incredible help I had from two artificial intelligence programs. I have been playing with computers for a long time, but since 1995, I have not learned any new languages; I began using Mathematica in 1988. I have never written a line of Python by myself. I can sort of get the gist of what is going on from reading the code. My approach was to discover the names of the functions and the input they required, then I would get the output into Mathematica where I could competently deal with it.

Loading the Python package

The hard part was getting Python configured in Mathematica 14.3. Once I solved that by accident, it turned out to be incredibly easy to install the Python package. I had the whole Garmin FITSDK on my computer but I couldn’t figure out how to get it into Mathematica. I did finally accomplish that, but there is an incredibly easier method, using a ResourceFunction, as the package is also on the internet.
In[]:=
[◼]
PythonPackageInstall
["garmin_fit_sdk"]
Out[]=
Success
✓
Message: The installation of "garmin_fit_sdk" is complete.
InstalledDependencies: garmin-fit-sdk21.178.0


Notebook Assistant

I asked Notebook Assistant for help in learning about the package contents stepping through each level until I finally was showed help files, which documented how the functions worked.
In[]:=
ExternalEvaluate["Python","import garmin_fit_sdk.decoder as decoderhelp(decoder.Decoder.read_file_header)"]
Help on function read_file_header in module garmin_fit_sdk.decoder:
read_file_header(self, reset, decode_mode=<DecodeMode.NORMAL: 1>)
Reads the file's header and returns its parameters.
In[]:=
ExternalEvaluate["Python","import garmin_fit_sdk.decoder as decoderhelp(decoder.Decoder.read)"]
Help on function read in module garmin_fit_sdk.decoder:
read(self, apply_scale_and_offset=True, convert_datetimes_to_dates=True, convert_types_to_strings=True, enable_crc_check=True, expand_sub_fields=True, expand_components=True, merge_heart_rates=True, mesg_listener=None, decode_mode=<DecodeMode.NORMAL: 1>)
Reads the entire contents of the fit file and returns the decoded messages
That was about as far as Notebook Assistant could get. It tried a variety of methods and finally could read a FIT file but ended up outputting the same FIT file that it read in. I gave up on that.

Grok 4 Fast beta

I have used the free version of Grok that comes with an X account for most of this year. It is usually a little better than Notebook Assistant with Mathematica code, but mostly doesn’t know ResourceFunctions or Wolfram Alpha content. It has very wide information, however, about Python. So I gave it the very explicit prompt below after pasting the Mathematica output above along with the filepath.
The above code shows that I have loaded the garmin_fit_sdk Python package into Mathematica. What I would like now is to have an ExternalEvaluate Python string for a function which will work in Mathematica to first read and decode the file header then read and decode a .fit file. To do this you cannot just wing it. You will need to do a deep search of the garmin_fit_sdk for example code that works. The filepath below gives the file location. I am not going to run this code right away so you may think on it as long as you want and I will look at everything later today.

Grok result

After 38 seconds Grok gave me a text string which Mathematica could reformat as it was pasted to pythonStr
In[]:=
pythonStr="from garmin_fit_sdk import Decoder, StreamfilePath = r\"C:\\Mathematica 14\\Garmin\\Activity\\2025-10-17-08-22-16.fit\"stream = Stream.from_file(filePath)decoder = Decoder(stream)header = decoder.read_file_header(reset=True)messages, errors = decoder.read()(header, messages, errors)";
I executed the result expecting failure but was amazed to see output of an ExternalObject containing everything that I knew was in the file from using the java tool.
In[]:=
result=ExternalEvaluate["Python",pythonStr]
Out[]=
ExternalObject
System: Python
IsModule: False
​
Module: garmin_fit_sdk.decoder
IsClass: False
,file_id_mesgs{serial_number3616299671,time_createdDateObject[{2025,10,17,15,22,16.},Instant,Gregorian,UTC],manufacturergarmin,product4586,typeactivity,garmin_productinstinct3_amoled_45mm},
⋯27⋯
,1042531129648987,40,04275,288,323,
⋯6⋯
,2531129651687,40,04250,286,322,{}
Full expression not available
(
original memory size:
4.5 MB)

Parsing the output with Mathematica

It did not take too much time to find where the relevant information was and the output was conveniently in the form of a list of Associations, which shows the excellence of the Mathematica interface.
In[]:=
Dimensions@result
Out[]=
{3}
headers and errors form Python were empty; the middle column, messages, contained all the data.
In[]:=
keys=Keys@result[[2]]
Out[]=
{file_id_mesgs,file_creator_mesgs,activity_mesgs,140,session_mesgs,time_in_zone_mesgs,lap_mesgs,288,327,326,timestamp_correlation_mesgs,event_mesgs,device_info_mesgs,22,141,394,device_settings_mesgs,user_profile_mesgs,79,sport_mesgs,training_settings_mesgs,zones_target_mesgs,record_mesgs,gps_metadata_mesgs,233,325,499,517,104}
The desired data was in record_mesgs.
In[]:=
records=result[[2]]["record_mesgs"];
In[]:=
records[[1]]​​records[[-1]]
Out[]=
timestampDateObject[{2025,10,17,15,22,16.},Instant,Gregorian,UTC],position_lat408599644,position_long-1327516416,distance2.54,enhanced_speed0.,enhanced_altitude1462.,cycle_length160.12,heart_rate66,cadence0,fractional_cadence0.,1071,13511,13666,14380
Out[]=
timestampDateObject[{2025,10,17,16,12,25.},Instant,Gregorian,UTC],position_lat408599494,position_long-1327516985,distance3287.57,enhanced_speed1.092,enhanced_altitude1459.6,cycle_length160.,heart_rate100,cadence0,fractional_cadence0.,1071,13598,136100,14370
Dataset could interpret the DateObjects, but Tabular needed a list of lists rather than a list of Associations.
In[]:=
recordT=Normal[Take[#,10]]&/@records//Tabular
Out[]=
The above could then be put into data formatted for Mathematica. There was a bogus Key, “107” which had to be removed manually. Since all the data keys were the same as the java output, only a slight modification of the code for the java tool was needed.
In[]:=
keys=Keys[Normal[recordT[[1]]]][[1;;10]]​​sccorr=180./2^31;
Out[]=
{timestamp,position_lat,position_long,distance,enhanced_speed,enhanced_altitude,cycle_length16,heart_rate,cadence,fractional_cadence}
In[]:=
recordT=TransformColumns[recordT,{"Timestamp"->Function[TimeZoneConvert[#timestamp,$SystemTimeZone]],​​"GeoPosition"->Function[GeoPosition[{#"position_lat"sccorr,#"position_long"sccorr}]],​​"Distance"->Function[UnitConvert[Quantity[#distance,"m"],"Miles"]],​​"Altitude"->Function[UnitConvert[Quantity[#"enhanced_altitude","m"],"feet"]],​​"Speed"->Function[UnitConvert[Quantity[#"enhanced_speed","m/s"],"Miles"/"Hour"]],​​"HeartRate"->Function[#"heart_rate"],​​"Cadence"->Function[#cadence],​​"FractionalCadence"->Function[#"fractional_cadence"],​​"CycleLength"->Function[#"cycle_length16"]​​}];​​DeleteColumns[DeleteColumns[recordT,keys],{1}]
Out[]=
Tabular
Row count: 585
Column count: 9


Visualizing the data

In[]:=
GeoGraphics[{Red,Thick,Line[recordT[All,"GeoPosition"]]},GeoBackground->"Satellite"]
Out[]=

Conclusion

It has taken me longer to write this notebook than to get the data from the garmin_fit_sdk, once I discovered how easy it was to load a Python package. Grok probably has knowledge about every Python package, so with AI assistance it should not be too difficult to use Python code in Mathematica.

CITE THIS NOTEBOOK

AI assisted access of Garmin FIT data via Python interface​
by Robert Rimmer​
Wolfram Community, STAFF PICKS, October 18, 2025
​https://community.wolfram.com/groups/-/m/t/3562391