Working With Developer Data Fields

Version 2.0 of the FIT SDK introduced Developer Data Fields as a way to add custom data fields to existing messages. Developer Data Fields can be added to any message at runtime by providing a self-describing field definition. Developer Data Fields are also used by the Connect IQ FIT Contributor library, allowing Connect IQ apps and data fields to include custom data in FIT Activity files during the recording of activities.

This recipe covers:

Example Project

The example code used in this recipe is a C# console app written with .NET Core. All example projects in the FIT Cookbook use Visual Studio Code and can be compiled and executed on Windows, Mac, and Linux systems. A guide for using the example projects in this cookbook can be found here. The source code for this and other recipes is included with the FIT SDK and is located at /path/to/fit/sdk/cs/cookbook.

Developer Data Overview

Prior to the introduction of Developer Data Fields, the only way to include user defined data in a FIT file was to use the FitGen tool to add a custom message to the FIT SDK to hold the data. This is a good solution until there is a need to expose the custom data. To do this, you would need to share your custom FIT SDK, or at least the custom message definitions, which at best is tedious and at worst is error prone.

Developer Data Fields were created as a way to solve this problem, allowing custom fields to be added to existing messages without the need to customize the FIT SDK. Developer Data Fields can be added to any message at runtime through the use of self-describing field definitions. These field definitions are included in the message definitions of the messages that they are used with, allowing for the custom fields to be decoded without the need for any prior knowledge of the Developer Data Fields.

FIT Protocol v1.0 versus v2.0

Developer Data Fields were introduced as a breaking change to the FIT protocol. In order to add fields to a message at runtime, a change was needed to the data message header. Because of this, FIT files created with the v2.0 protocol are not backwards compatible with earlier versions of the FIT SDK. To enable backwards compatibility, developers can choose which protocol to use when encoding FIT files. This enables compatibility with platforms that process FIT files but have not updated to a FIT SDK supporting the v2.0 protocol.

By default, the v1.0 protocol is used when encoding FIT files. The v2.0 protocol can be requested when creating an instance of an Encode object. If your application uses Developer Data Fields, then the v2.0 protocol should be used.

Encode encoder = new Encode(ProtocolVersion.V20);

There are no performance or file size differences between the v1.0 and v2.0 protocols when encoding FIT files. However, when using the v1.0 protocol, any messages containing Developer Data Fields will not be written to the file.

Encoding Developer Data

Developer Data can be added to any existing FIT message. Developer Data is typically used in Record, Lap, and Session messages to include additional time series data in Activity files.

The steps to include Developer Data Fields in existing messages are:

  1. Create a Developer Data Id message that identifies the source of the data.
  2. Create a Field Description message that describes the data type, name, and units of the data.
  3. Create a Developer Field, set its value, and attach it to an existing message.

The example code for the Encoding FIT Activity Files recipe demonstrates how to encode Developer Data into FIT files. In the following sections we will take a look at how each of these steps are implemented in that project.

Developer Data Id Message

When developer data is added to a FIT file from a Connect IQ data field, there may be more than one data field contributing to the FIT file so there needs to be a way to identify the source of the data. This is done with a Developer Data Id message. A FIT file can contain up to 255 unique Developer Data Id messages. These messages must occur before any related field description messages are written to the file.

The Developer Data Id message contains an Application Id field, which is a byte array with a length of 16. Connect IQ Apps use the 128-bit App Id GUID that is generated by the Connect IQ project wizard for the Application Id value. When using developer data directly through the FIT SDK, it is up to the developer to create an App Id GUID. It is recommended that the same GUID is used for all FIT files created by your platform. The application version can also be provided. The application version should be multiplied by 100 and converted to a uint.

var developerIdMesg = new DeveloperDataIdMesg();
byte [] appId = new Guid("00010203-0405-0607-0809-0A0B0C0D0E0F").ToByteArray();
for (int i = 0; i < appId.Length; i++)
{
    developerIdMesg.SetApplicationId(i, appId[i]);
}
developerIdMesg.SetDeveloperDataIndex(0);
developerIdMesg.SetApplicationVersion(110); // Version 1.1
encoder.Write(developerIdMesg);

Field Description Message

Each Developer Data Field needs to be described in terms of its data type, name, and units. This is done using a Field Description message. The name and units should be self-descriptive, concise, and display-friendly. The Field Description is written to the FIT file once and then referenced within the message definition of the messages it is used with.

The example project creates two Field Descriptions. The first Field Description is to track the number of doughnuts earned during the activity. This is added to the Session message. The second Field Description is used to add a secondary heart rate field to the Record messages. The heart rate field uses the Native Field Number property to provide a hint when decoding the file that this data can be treated equivalently to the native heart rate field.

var doughnutsFieldDescMesg = new FieldDescriptionMesg();
doughnutsFieldDescMesg.SetDeveloperDataIndex(0);
doughnutsFieldDescMesg.SetFieldDefinitionNumber(0);
doughnutsFieldDescMesg.SetFitBaseTypeId(FitBaseType.Float32);
doughnutsFieldDescMesg.SetFieldName(0, "Doughnuts Earned");
doughnutsFieldDescMesg.SetUnits(0, "doughnuts");
encoder.Write(doughnutsFieldDescMesg);

FieldDescriptionMesg hrFieldDescMesg = new FieldDescriptionMesg();
hrFieldDescMesg.SetDeveloperDataIndex(0);
hrFieldDescMesg.SetFieldDefinitionNumber(1);
hrFieldDescMesg.SetFitBaseTypeId(FitBaseType.Uint8);
hrFieldDescMesg.SetFieldName(0, "Heart Rate");
hrFieldDescMesg.SetUnits(0, "bpm");
hrFieldDescMesg.SetNativeFieldNum(RecordMesg.FieldDefNum.HeartRate);
encoder.Write(hrFieldDescMesg);

The combination of the Developer Data Index and Field Definition Number create a unique id for each Field Description.

Both messages use the same Developer Data Index value of 0. This is the index into the array of Developer Data Id messages used in the file. Since the example application only uses a single Developer Data Id message, the index will be 0.

For a given Developer Data Index, each associated Field Description should have a sequential Field Definition number. This value is also an index and should start at 0 and increase sequentially by 1. The two Field Description messages use the values 0 and 1 respectively. A FIT file can contain up to 255 unique Field Descriptions per developer. These messages must occur in the file before any related data is written to the file.

Developer Field Message

The final step is to create a Developer Field, set its value, and attach it to a message. A Developer Field needs to be associated with both a Developer Id and Field Description. This is done through the constructor. The value can be set before or after attaching the developer field to a message, but both steps need to occur before writing the message to the file. In the example program we are we are rewarding ourselves with three doughnuts per hour of activity! 🍩

var doughnutsEarnedDevField = new DeveloperField(doughnutsFieldDescMesg, developerIdMesg);
doughnutsEarnedDevField.SetValue(sessionMesg.GetTotalElapsedTime() / 1200.0f);
sessionMesg.SetDeveloperField(doughnutsEarnedDevField);
encoder.Write(sessionMesg);

Decoding Developer Data

When decoding FIT files, there is no option to choose the protocol used. Versions 2.0 and later of the FIT SDK support files created with both the v1.0 and v2.0 protocol. Earlier versions of the FIT SDK are unaware of the breaking changes introduced with the v2.0 protocol and will experience runtime errors when trying to decode files containing Developer Data Fields. Because of this, it is recommend that platforms that process FIT files use a version of the FIT SDK that supports the v2.0 protocol.

Developer Fields are automatically decoded as part of the message they are associated with and added to the list of DeveloperFields for that message. The list of DeveloperFields can be iterated over and the data extracted.

foreach (DeveloperField devField in recordMesg.DeveloperFields)
{
    var name = devField.Name;
    var value = devField.GetValue();
    var units = devField.GetUnits();
    var isHeartRate = devField.NativeOverride == RecordMesg.FieldDefNum.HeartRate;
    System.Console.WriteLine($"{name} {value} {units} {isHeartRate}");
}

When this code is used with the FIT file created by the Encoding Activity Files recipe described above, the following debug output is written to the console. The output matches what we know was written to the file.

Heart Rate 126 bpm True
Heart Rate 134 bpm True
Heart Rate 142 bpm True
Heart Rate 150 bpm True
Heart Rate 158 bpm True
Heart Rate 166 bpm True
Heart Rate 173 bpm True
Heart Rate 181 bpm True
Heart Rate 188 bpm True
Heart Rate 195 bpm True
Heart Rate 201 bpm True
Heart Rate 207 bpm True
Heart Rate 213 bpm True
Heart Rate 219 bpm True
Heart Rate 224 bpm True
Heart Rate 229 bpm True
Heart Rate 234 bpm True
Heart Rate 238 bpm True
Heart Rate 241 bpm True
.
.
.

The NativeOverride field provides additional context to the data; knowing that this data is heart rate data means that it can be treated in the same way as the native heart rate field found in the Record message. A common use case for native overrides is when processing FIT files that contain Developer Data created by Connect IQ apps. Any data contributed to the FIT file by a Connect IQ app or data field will be stored in the FIT file as Developer Data Fields. The Native Field Number provides a way to convey which native field the data should be considered as. The Native Field Number is a more deterministic way of conveying this information when compared to the field name and unit properties, which are defined using strings and may vary subtly from vendor to vendor making comparisons difficult. Since native overrides are an integer type, they are more computationally efficient when used in conditional operators versus string comparison functions.

A common native override used with CIQ apps is running power. There are multiple CIQ data fields that calculate running power-based accelerometer data and other factors. These CIQ data fields use developer fields to write the running power data to the file, specifying the native override for power. The Garmin Labs Running Power data field is one example of this.

When the native override field is not present or additional identification of the data is required, the App Id, App Version, and Field Number values can be inspected and compared to known values.

var appId = new Guid(devField.AppId());
var appVersion = devField.AppVersion();
var fieldDefinitionNumber = devField.Num();

See the Decoding Activity Files and Encoding Activity Files recipes for examples of using Developer Data.