The ANT ‘Flexibile & Interoperatble Data Transfer’ (FIT) protocol
ANT devices and Garmin GPS units, like the Edge 810 and 830 I use, store all device information in .FIT files. The FIT protocol is open and is defined in the FIT SDK. It is a binary protocol, not human readable, designed for embedded devices (having limited resources).
Since I wanted to decode the activity files, containing the GPS track, I decided to write a FIT file reader.
The .FIT file
The FIT file is extensively described on the FIT Developer Site. A short summary is presented here for overview and for definition of the terms used in the software.
A FIT file consists of
The header defines some overall stuff like versions, data size, “.FIT” and a header CRC. 12 bytes
- Data records
Records containing the data, including a definition of the data
Used to validate the file, 2 bytes
Data Records come in two flavours:
- Definition message
A Definition Message defines a local message type (0-15), the message that it represents and the subset of message fields that are used in this local message type.
After the Definition Message occurred, subsequent Data Messages may be added for this local message type, containing the data for this message.
Note that a local message type may be redefined and reused for another message by a subsequent Definition Message for that local message type. This is necessary, since the only 16 local message types are available and more messages may be used in one file.
- Data message
A Data Message contains the data for the message (fields) defined by the local message type.
A Data Message refers to the latest Definition Message defining its local message type as is shown in the image below.
Note that the same message may occur in multiple Definition Messages. In the FitReader software each Definition Message results in a FitMessage instance. This means that there may be multiple FitMessages that refer to the same message. The software however contains functionality to request a List of all FitDataRecords based on message name
Available messages with all their possible message fields are defined in the Global Fit Profile. This is the master list and is an Excel file Profile.xslx that is part of the FIT SDK. The Excel contains two tabs: one with field type definitions (‘Types’), one with message including message field definition (‘Messages’).
Note that not all manufacturers limit the messages to the messages defined in the Global Fit Profile. Devices like the Garmin devices use custom messages of which the definition is not public. Therefore, in this FitReader software the excel /src/main/resources/Profile.xslx is extended with two additional tabs ‘Additional Types’ and ‘Additional Messages’, to make it extendable for example by reverse engineering. Both tabs have the same format as the original tabs
Note that the Definition Message allows for additional fields to be defined, the Developer Defined Message Fields. These fields do not occur in the Global Fit Profile but are defined (non-descriptive) in the Definition Message itself.
If you compare the .FIT file to a database, the Definition Message defines the table. The message is the table name. The message fields define the column names and format. The Data Records are the rows in the table. There may be multiple tables with the same name (message), the protocol even allows for a different subset of columns (message fields) per tables.
Refer to the FIT Developer Site for a detailed explanation of the file format.
The .FIT reader is delivered as Github repository. The project results in a JAR file with all dependencies, together with Javadocs. The FitReader class is the entry point. The readFile() method does the job. It returns a FitMessageRepository instance, containing the FitMessages read. Each FitMessage contains a number of fields. Fit Messages Fields come in two types:
- FitMessageFields predefined in Global Profile (see FIT SDK; FitMessageFields).
- FitDeveloperFields. These are defined in the FIT file itself.
Field values of each message are provided in FitDataRecords, whose values can be obtained by the FitMessage class.
Refer to the javadocs, Github repository the FitReader source code and tests, for some example code.
Next diagram shows the usage:
- Obtain the instance of the FitReader
- Read a fit file
- This is delegated to readInputStream()
- A FitMessageRepository is created and populated based on the file content
- The FitMessageRepository is returned
- Get a list of Message names (as explained a message may occur multiple times)
- All instances of the Message can be requested based on name. For each instance:
- Request the field names of the fields within the Message
- For a particular field you can request the details
- Check the number of data records associated with the field
- Request a value of the record depending on the field type: getFloatValue(), getIntValue(), getTimeValue(), …
If you want to know what your .FIT files look like, a handy function of is dumpMessageDefinitions() of FitMessageRepository. It dumps the message definitions in the file to the log4J debug output (Info level).
MESSAGE 0-file_id: 1 records, local message type 0 file_id (00000) serial_number (003) type: uint32z/ uint32z, units: , scale 1.000000, offset 0.000000 file_id (00000) time_created (004) type: date_time/ uint32, units: , scale 1.000000, offset 0.000000 file_id (00000) manufacturer (001) type: manufacturer/ uint16, units: , scale 1.000000, offset 0.000000 file_id (00000) product (002) type: uint16/ uint16, units: , scale 1.000000, offset 0.000000 file_id (00000) number (005) type: uint16/ uint16, units: , scale 1.000000, offset 0.000000 file_id (00000) type (000) type: file/ enum, units: , scale 1.000000, offset 0.000000 MESSAGE 49-file_creator: 1 records, local message type 1 file_creator (00049) software_version (000) type: uint16/ uint16, units: , scale 1.000000, offset 0.000000 file_creator (00049) hardware_version (001) type: uint8/ uint8, units: , scale 1.000000, offset 0.000000 MESSAGE 29-location: 2 records, local message type 2 location (00029) timestamp (253) type: date_time/ uint32, units: , scale 1.000000, offset 0.000000 location (00029) name (000) type: string/ string, units: , scale 1.000000, offset 0.000000 location (00029) position_lat (001) type: sint32/ sint32, units: semicircles, scale 1.000000, offset 0.000000 location (00029) position_long (002) type: sint32/ sint32, units: semicircles, scale 1.000000, offset 0.000000 location (00029) message_index (254) type: message_index/ uint16, units: , scale 1.000000, offset 0.000000 location (00029) symbol (003) type: uint16/ uint16, units: , scale 1.000000, offset 0.000000 location (00029) altitude (004) type: uint16/ uint16, units: m, scale 5.000000, offset 500.000000 location (00029) unknown (005) type: uint16/ uint16, units: , scale 1.000000, offset 0.000000 location (00029) description (006) type: string/ string, units: , scale 1.000000, offset 0.000000
The datatype unsigned long is not supported
Garmin Track Converter
The ‘proof of the pudding’ for the FitReader is the Garmin Track Converter which I wrote to convert tracks logged with my Garmin Edge 880 bike computer. The converter takes in the .FIT files and converts the tracks to .GPX format, to be used in other programs.
Waypoints that can be indicated by the user are not stored in the track .FIT. Up to 100 waypoints can be stored in a separate file, Locations.fit. The converter takes this file into account and selects the waypoints that were logged during the track. These selected waypoints <wpt> are combined with the track data <trk> in the GPX file.
The name and serial number of the device is stored in the description of the track.
Available on GitHub: