FIT SDK
When a person uses their wearable device or cycling computer to record their activities, chances are that data is stored in a FIT Activity file. FIT Activity files are also a common format used by web APIs to transfer activity data between fitness platforms. This makes FIT Activity files the most common of all FIT file types. In this recipe, we will go over how to decode an Activity file and provide a few tips and tricks for maximizing the data found in Activity files.
This recipe covers:
The example project 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 the 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.
Included with the FIT SDK are sample FIT Activity files that demonstrate the concepts covered in this recipe. There are single-sport and multi-sport files; files that contain developer data; files that have been truncated/corrupted; pool swim files; and files containing messages for electronic shifting, Cycling Dynamics, and battery status events. These files are located in the examples folder located at /path/to/fit/sdk/examples. The input file to be decoded should be provided as a command line argument to the program. When using Visual Studio Code to debug the project, the input file can be specified in the /.vscode/launch.json file. The .csv files created by the example project can be viewed in Visual Studio Code, in a text editor, or spreadsheet application.
Activity files are used to store sensor data and events recorded by devices and apps during an active session. This includes GPS location data and sensor data from heart rate monitors, stride sensors, power meters, etc. Activity files may also include information about the sport type, course followed, training targets & zones, and user profile data. Activity files may be recorded in real time by devices or they may be used as a way to share data between platforms.
Decoding FIT files begins with knowing what messages types are found within each file type. The flexibility of the FIT file format allows for any combination of valid messages to be included in a FIT file, however each file type lends itself to a common grouping of messages that can be expected but not necessarily required. See the File Types guide for a list of file types and the message types associated with them.
There is a common set of message types that can be expected in all Activity files. These include File Id, Activity, Session, Lap, Length, Event, and Record messages. See the Activity File description for the minimum requirements of a FIT Activity file. In many cases, Activity files will contain more than the minimum required message types. Messages found in a FIT Activity file may be manufacturer, device, or sport-specific. Messages included in a FIT Activity file may also depend on if a specific feature of the device was used, if the user was performing a workout or following a course, or what sensors were in use during the recording of the activity.
A robust Activity file parser should account for all of the required message types as well as any additional messages types that are commonly used. One way to discover the message types in a given FIT file is to use FitCSVTool. FitCSVTool is a Java command line app that can be used to convert binary FIT files to readable text files. Using the “–data none” option will output a file containing only message definitions. This list of message definitions can be used as a guide for developing a robust Activity file parser.
java -jar /path/to/fit/sdk/java/FitCSVTool.jar /path/to/input_file.fit --data none
The following is the output from FitCSVTool showing the message definitions found in an example Activity file. This list of message definitions provides a starting point for decoding Activity files.
Type,Local Number,Message,Field 1,Value 1...
Definition,0,file_id,serial_number,1...
Definition,1,file_creator,unknown,20...
Definition,2,device_info,timestamp,1...
Definition,3,device_settings,utc_offset,1...
Definition,4,training_file,timestamp,1...
Definition,5,user_profile,friendly_name,16...
Definition,6,zones_target,max_heart_rate,1...
Definition,7,workout,wkt_name,16...
Definition,8,workout_step,notes,32...
Definition,9,event,timestamp,1...
Definition,10,record,timestamp,1...
Definition,11,hrv,time,5...
Definition,12,climb_pro,timestamp,1...
Definition,13,segment_lap,timestamp,1...
Definition,14,lap,timestamp,1...
Definition,15,session,timestamp,1...
Definition,0,activity,timestamp,1...
Based on the list of message definitions, it can be inferred that this activity includes messages related to performing a structured workout (Workout and Workout Step messages), following a hilly course (Climb Pro messages), wearing a heart rate monitor with advanced features (HRV messages), and using live segments (Segment Lap messages).
It is recommended to survey multiple files across multiple makes and models of devices for the complete list of potential messages types that may be encountered.
When decoding Activity files, keep the following in mind.
With the two different message encoding patterns, the entire file needs to be read before any further decisions on how to process the messages can be made. A strategy to accomplish this is to read all of the messages from the file, grouping them by message type. Once all of the messages have been read from the file, the summary messages can be processed based on the message start time rather than the order that they appear in the file.
The first step in decoding FIT files of any type is to create instances of the FIT SDK Decode and MesgBroadcaster classes. The Decode class is responsible for reading data from a FIT file and creating instances of the base Mesg or MesgDefinition classes for each data record found in the file. The Decoder passes the base messages to the Message Broadcaster, which acts as an adaptor that creates specialized message objects based on the message type. The Message Broadcaster then passes these objects to the registered delegate methods. If you are interested in how this works, the source code for the Decode and Message Broadcaster classes is available in the FIT SDK.
The following diagram illustrates how messages flow through the Decoder, Message Broadcaster, and delegate methods.
The code below shows the relationship between the Decoder, Message Broadcaster and delegate methods.
// Create the Decode and Message Broadcaster Objects
Decode decoder = new Decode();
MesgBroadcaster mesgBroadcaster = new MesgBroadcaster();
// Connect the the Decode and Message Broadcaster Objects
decoder.MesgEvent += mesgBroadcaster.OnMesg;
decoder.MesgDefinitionEvent += mesgBroadcaster.OnMesgDefinition;
The next steps are to create the application delegate methods, connect the delegate methods to the Message Broadcaster, and store the incoming messages locally. In the example project, the custom FitDecoder and FitMessages classes work together to accomplish these tasks. The FitDecoder class implements a delegate method for each message type that is commonly found in an Activity file, and the FitMessages class acts as the data model where the messages are stored.
// Connect the Message Broadcaster Events to the Message Listener Delegates
mesgBroadcaster.ActivityMesgEvent += OnActivityMesg;
mesgBroadcaster.ClimbProMesgEvent += OnClimbProMesg;
mesgBroadcaster.CourseMesgEvent += OnCourseMesg;
mesgBroadcaster.CoursePointMesgEvent += OnCoursePointMesg;
mesgBroadcaster.DeviceInfoMesgEvent += OnDeviceInfoMesg;
mesgBroadcaster.EventMesgEvent += OnEventMesg;
mesgBroadcaster.FileIdMesgEvent += OnFileIdMesg;
mesgBroadcaster.HrMesgEvent += OnHrMesg;
mesgBroadcaster.HrvMesgEvent += OnHrvMesg;
mesgBroadcaster.LapMesgEvent += OnLapMesg;
mesgBroadcaster.LengthMesgEvent += OnLengthMesg;
mesgBroadcaster.RecordMesgEvent += OnRecordMesg;
mesgBroadcaster.SegmentLapMesgEvent += OnSegmentLapMesg;
mesgBroadcaster.SessionMesgEvent += OnSessionMesg;
mesgBroadcaster.UserProfileMesgEvent += OnUserProfileMesg;
mesgBroadcaster.WorkoutMesgEvent += OnWorkoutMesg;
mesgBroadcaster.WorkoutStepMesgEvent += OnWorkoutStepMesg;
mesgBroadcaster.ZonesTargetMesgEvent += OnZonesTargetMesg;
Most of the delegate methods look and behave similarly. The incoming message is stored in the data model so it can be processed after all the messages in the file have been decoded. For message types where only one message is expected, the message is stored in a single property.
public void OnFileIdMesg(object sender, MesgEventArgs e)
{
Messages.FileId = (FileIdMesg)e.mesg;
}
For message types where more than one message is expected, the messages are stored in a list.
public void OnRecordMesg(object sender, MesgEventArgs e)
{
Messages.Records.Add(e.mesg as RecordMesg);
}
With the decoder, message broadcaster, message listener, and data model objects created and connected, the next step is to call the Read() method, passing the input stream to decode.
try
{
decoder.Read(stream);
}
catch (FitException ex)
{
// Handle FIT Decode Exception
}
catch (System.Exception ex)
{
// Handle Exception
}
When the Decode.Read() method returns, all of the data from the file will have been read and the data model will hold all of the decoded messages.
After the decoder is finished reading the file and all of the messages are stored locally in the data model, the messages can be parsed. How the messages are parsed depends on how the data will be used. The message data may be stored in proprietary data structures or in a database, the messages may be serialized into a new file format, the messages may be converted to JSON and returned from a web service, etc. The processing of the messages is specific to the application they are being used in.
If there is only one Session message in the file, then all messages in the file belong to that single session. If there are multiple Session messages in the file, then messages are considered to be part of a session if their timestamp occurs within the time span of that session.
The example project groups messages with the session they belong to based on timestamps. Once the messages are grouped by session, the example project creates a CSV file for each session containing data from the Session, Record, Event, and Lap messages.
In the example project, the parsing of the messages takes place in the custom ActivityParser class and the grouped messages are stored in the custom SessionMessages class. The ParseSessions() method returns a list of SessionMessages objects, one for each Session message, that contains all of the Lap, Length, Record, Event, etc. messages belonging to the Session.
// Create the Activity Parser and group the messages into individual sessions.
ActivityParser activityParser = new ActivityParser(messageListener.Messages);
var sessions = activityParser.ParseSessions();
For both brevity and code reuse, ActivityParser uses the LINQ query syntax combined with custom extension methods to slice the lists of messages based on the timestamp of the message falling within the time span of a given Session message. The same result can be achieved using Java’s Stream interface and Predicates or by converting the LINQ statements to for() loops and the extension methods to helper methods.
Using the LINQ Where() method and the custom extension method Within(), each list of messages is filtered to find the messages that belong to a specific session. The results are stored in a SessionMessages object.
session.Records = _messages.Records.Where(record => record.Within(session)).ToList();
Laps are unique in that the Session message provides the First Lap Index and Lap Count properties that can be used to find all the Lap messages associated with a Session. These properties can be used with the LINQ Skip() and Take() methods to find all Laps belonging to a Session. Java’s Stream methods provide similar functionality.
session.Laps = _messages.Laps.Skip(session.GetFirstLapIndex() ?? 0).Take(session.GetNumLaps() ?? 0).ToList();
Not all manufacturers provide the First Lap Index and Lap Count properties. If these values are not provided, then the Where() and Within() technique can be used to filter the list of Lap messages instead.
When the ParseSessions() methods returns, all of the messages associated with each session are stored in a corresponding SessionMessages object.
Converting FIT files to CSV is such a common use case that the FIT SDK includes FitCSVTool for doing just that. But there may be times when the CSV output needs to be customized for a specific application.
The example project implements a CSV exporter that can be used as a starting point for further customization. The output CSV contains a single header row that is dynamically created based on the data available in the file. To create the header row, we need a list of field names for all Record messages in the file. To create this list, the RecordMesgEvent delegate has been updated to collect the field names found in the Record messages in a Set collection. This creates a list of unique field names across all Record messages in the file, which will be used to create the header row of the CSV file. The names of each Developer Field found in the Record messages are stored in a separate Set collection. Doing this in the delegate method prevents the need to iterate over the list of Record messages later on.
public void OnRecordMesg(object sender, MesgEventArgs e)
{
Messages.Records.Add(new ExtendedRecordMesg(e.mesg as RecordMesg));
foreach (Field field in e.mesg.Fields)
{
if (field.Name.ToLower() != "unknown")
{
Messages.RecordFieldNames.Add(field.Name);
}
}
foreach (DeveloperField field in e.mesg.DeveloperFields)
{
Messages.RecordDeveloperFieldNames.Add(field.Name);
}
}
Checking all Record messages in the file is required since the fields in the Records message may change throughout the recording of the activity. This can occur if the activity recording was started before all of the sensors were paired or the GPS signals were acquired, or during multi-sport events when bike-specific sensors will not be connected during the swim or run portions of the event.
Now that the set of available fields for all Records messages in the file is known, the CSV export can be prototyped in pseudo code as:
foreach (record in records)
{
foreach (fieldName in fieldNames)
{
AppendValueToRow(record.GetFieldValue(fieldName))
}
AppendNewLine()
}
See the RecordsToCSV() method in the example project for the complete code listing.
The CSV export includes the Developer Data found in the Record messages. Developer Data provides a way to add custom data fields to FIT messages without the need to update the FIT Profile. Developer Data is commonly used by Connect IQ apps to contribute custom data to Activity files.
See the Working With Developer Data Fields recipe for an explanation of how to encode and decode developer fields from Activity files.
Knowing the type of FIT file being decoded can prevent the need to fully decode file types that are not supported, or determine how to process the decoded messages. Activity files and Workout files can both contain Workout and Workout Step messages, Activity files and Course files can contain Record messages, and a truncated Activity file might be missing its Activity and Session messages. This means that inspecting the message types found in a file is not a deterministic way to determine the type of file being decoded. A more reliable way to determine the file type is to check the Type field found in the File Id message.
Checking the file type can help prevent decoding and processing messages for an unsupported file type. The example project checks the file type in the OnFileIdMesg() delegate method. If the Type field is not File.Activity, then a custom FileTypeException is thrown. The exception is caught in the Main() method of the application. This prevents the file from being read if it is not the expected type.
if((e.mesg as FileIdMesg).GetType() != File.Activity)
{
throw new FileTypeException($"Expected File Type: Activity, recieved File Type: {(e.mesg as FileIdMesg).GetType().ToString()}");
}
Timestamps in FIT messages are almost always given as the number of seconds since the Garmin Epoch 1989–12–31T00:00:00Z. The Z indicates that the reference timezone is UTC, but most of the time we will want to display values in the user’s local timezone.
The Activity message includes the Local Timestamp property which is the number of seconds since 1989–12–31T00:00:00 in local time. When provided, this property can be used to calculate the timezone offset for the activity. The example project provides an extension method for calculating the timezone offset from the Activity message Timestamp and Local Timestamp fields. Not all manufacturers include the Local Timestamp property, so it should always be checked for a valid value before using.
public static TimeSpan? TimezoneOffset(this ActivityMesg activity)
{
if (activity == null)
{
return null;
}
if (!activity.GetLocalTimestamp().HasValue)
{
return null;
}
return TimeSpan.FromSeconds((int)activity.GetLocalTimestamp() - (int)activity.GetTimestamp().GetTimeStamp());
}
When the Local Timestamp property is not included it is common to use a timezone lookup based on the GPS location of the first Record message in the file.
Most devices encode Activity files in real time using the summary last message sequence, resulting in the Session and Activity messages being written to the file at the end of the activity recording. This means that if the device crashes, runs out of battery power, or if the file is truncated during transfer the file may not include a Session or Activity message. This may be an issue if the parsing of the messages is dependent on having at least one Session message and that Session message having valid Start Time and Total Elapsed Time properties. One fix for a missing Session message is to create a Session message based on other messages in the file. If there are no Session messages but there are Record messages, a Session message can be created based on the timestamps of the first and last Record messages. Adding this new session to the data model allows the message parsing to continue as usual. The Activity Parser class shows an example of how this can be done.
if (_messages.Sessions.Count == 0 && _messages.Records.Count > 0)
{
Dynastream.Fit.DateTime startTime = _messages.Records[0].GetTimestamp();
Dynastream.Fit.DateTime timestamp = _messages.Records[_messages.Records.Count - 1].GetTimestamp();
var session = new SessionMesg();
session.SetStartTime(startTime);
session.SetTimestamp(timestamp);
session.SetTotalElapsedTime(timestamp.GetTimeStamp() - startTime.GetTimeStamp());
session.SetTotalTimerTime(timestamp.GetTimeStamp() - startTime.GetTimeStamp());
_messages.Sessions.Add(session);
}
All messages in the FIT SDK inherit from the base Mesg class. The Mesg class provides methods for encoding and decoding messages and accessing data fields within a message by id. But the Mesg class does not contain any data fields of its own. All data fields are defined in the subclasses of the Mesg class.
Most message types found in an Activity File have a timestamp property, but this property is implemented in each individual subclass and not the base Mesg class, which means there is no means of retrieving the timestamp of a message through a generic interface.
A way around the lack of an interface to common properties is to access the data fields by Field Name or Field Id. Care was taken when defining the messages in the FIT Profile to reuse Field Names and Field Ids within similar messages. The timestamp in a message will always have the same name and the same ID. This allows the timestamp data fields to be accessed through the Mesg class by ID or name using the GetField(fieldNum) and GetField(fieldName) methods.
An example of this can be found in the extension method GetTimestamp(), which adds a timestamp property to the Mesg object. All messages use the Field Id 253 for the timestamp field, so the ID can be used to get the timestamp of an object even it it has been upcast to a Mesg.
public static Dynastream.Fit.DateTime GetTimestamp(this Mesg mesg)
{
Object val = mesg.GetFieldValue(253);
if (val == null)
{
return null;
}
return mesg.TimestampToDateTime(Convert.ToUInt32(val));
}
For other data fields where the Field Id may not be shared between messages, the field can be accessed by name. An example of this is the start_time field.
public static Dynastream.Fit.DateTime GetStartTime(this Mesg mesg)
{
Object val = mesg.GetFieldValue("start_time");
if (val == null)
{
return null;
}
return mesg.TimestampToDateTime(Convert.ToUInt32(val));
}
When working in a different programming language, a similar result can be achieved by converting the extension methods to helper methods that accept a Mesg object as an argument.
Many devices offer the choice of Smart Recording or Every Second Recording. Smart Recording aims to reduce file size by only recording messages when a significant change in a value is detected. Every Second Recording records the messages to the file every second whether or not the device detects a change in the data. This setting typically applies to Record Messages.
Smart Recording is great at reducing file size, which allows for more files to be stored on a device and for faster file transfers from the device to the cloud; however, it can make performing otherwise simple calculations on the data more complex. Smart Recording may also make it difficult to know the difference between short pauses in the recording of the data and long durations in between samples.
Independent of the recording rate, devices will use Timer events to indicate when the recording of data has been paused and then restarted. Incorporating Timer events into calculations can help reduce code complexity and may even yield better results. Timer events can also be used when graphing data to show a break in the line, indicating that the timer was not running and data was not being recorded.
The example project includes the custom ExtendedRecordMesg class that subclasses the Record message, inheriting all of the properties found in the Record message. ExtendedRecordMesg adds an Event Type property, allowing timer events to be interleaved with data from Record messages. The Event Type property can be used within algorithms, graphing routines, etc. to know when the recording of the data was paused.
public class ExtendedRecordMesg : RecordMesg
{
public EventType EventType {get; private set;}
public ExtendedRecordMesg(RecordMesg mesg) : base(mesg)
{
EventType = EventType.Invalid;
}
public ExtendedRecordMesg(EventMesg mesg)
{
SetTimestamp(mesg.GetTimestamp());
EventType = mesg.GetEventType() ?? EventType.Invalid;
}
}
The OnRecordMesg() delegate creates instances of ExtendedRecordMesg objects from the Record Message and store it in the data model.
public void OnRecordMesg(object sender, MesgEventArgs e)
{
Messages.Records.Add(new ExtendedRecordMesg(e.mesg as RecordMesg));
}
Similarly, the OnEventMesg() delegate checks if the event type is Event.Timer. If so, an ExtendedRecordMesg object is created from the Event message and stored in the Records list of the data model.
public void OnEventMesg(object sender, MesgEventArgs e)
{
var eventMesg = e.mesg as EventMesg;
Messages.Events.Add(eventMesg);
if(eventMesg?.GetEvent() == Event.Timer && eventMesg?.GetTimestamp() != null)
{
Messages.Records.Add(new ExtendedRecordMesg(eventMesg));
}
}
When performing operations on the ExtendedRecordMesg list, the EventType property can be checked to determine the appropriate action.
foreach (ExtendedRecordMesg record in records)
{
if(record.EventType == EventType.Start)
{
// Initialize operation, and continue.
continue;
}
else if(record.EventType == EventType.Stop)
{
// Finalize operation, and continue.
continue;
}
else
{
// Use current record in the operation
}
}
Event messages are used to indicate that something notable occurred at a point in time during the recording of an activity. Events can indicate when the timer was started or stopped, when a workout step was completed, if the users deviated from a course they were following, gear changes for electronic shifting systems, vehicles detected by radar, or Cycling Dynamics events like the rider standing or sitting. The complete list of Event Types is defined in the Event enum.
It may be useful to separate the list of Event messages by type. The example program does this using the Where() and Within() technique, and filtering based on the Event property.
messages.RiderPositionChangeEvents = _messages.Events.Where(evt => evt.GetEvent() == Event.RiderPositionChange && evt.Within(session)).ToList();
Device Info messages are used in Activity files to store information about hardware accessories or sensors that were used during the recording of the activity. Device Info messages may include the manufacturer ID, name, description, serial number, software version, and battery status of the accessory.
A use case for decoding Device Info messages is alerting the user that the battery is low in one of their sensors. Most bike computers and wearables will display a low battery alert during the recording of the activity, but the user may not always see the alert. When stored in the Activity file, this information may be used to provide the user with a follow-up alert after the activity is completed.
The ActivityParser class includes a method to filter Device Info messages where the battery status is low or critically low. This information can then be conveyed to the user after the activity has been completed.
public List<DeviceInfoMesg> WhereDeviceBatteryStatusIsLow()
{
var batteryStatus = new List<byte>() { BatteryStatus.Critical, BatteryStatus.Low };
var deviceInfos = new List<DeviceInfoMesg>();
deviceInfos = _messages.DeviceInfos.Where(info => batteryStatus.Contains(info.GetBatteryStatus() ?? BatteryStatus.Unknown)).ToList();
return deviceInfos;
}
The example project uses this method to output a message if there is a sensor with a low battery.
var deviceInfos = activityParser.WhereDeviceBatteryStatusIsLow();
foreach(DeviceInfoMesg info in deviceInfos)
{
Console.WriteLine($"Device Type {info.GetAntplusDeviceType()} has a low battery.");
}
Many wearable devices use built-in accelerometers to detect flip and open turns while lap swimming. This allows devices to track the number of lengths of the pool that have been covered, track the total distance of the swim activity, and calculate the average pace for each length and the overall activity. More advanced wearable devices will use the accelerometers to detect stroke type, count the number of strokes, and provide a drill mode for kick sets and other swim drills.
Activity files for pool swim activities use the Length message to record information about each length of the pool the user swims. Length messages are used to store the start time, elapsed time, stroke count, and stroke type for each length of the pool that was covered. Length messages are also used to track idle periods between active sets. Lap messages are used to group sets of active and idle Length messages. The length of the pool is a constant and is stored in the Session message. The distance represented by each Length message is equal to the value of the pool_length field in the Session message. The pool length is specified in meters, and the pool_length_unit field provides the corresponding display units which are either metric for meters or statute for yards. With pool swim activities, the length of the pool is a constant and the recording rate of the messages is variable.
To decode and parse pool swim Activity files, we start by using FitCSVTool to find all of the message types used in a sample pool swim Activity file.
Type,Local Number,Message,Field 1,Value 1...
Definition,0,file_id,serial_number,1...
Definition,1,file_creator,unknown,20...
Definition,2,device_info,timestamp,1...
Definition,3,device_settings,utc_offset,1...
Definition,4,user_profile,friendly_name,16...
Definition,5,zones_target,max_heart_rate,1...
Definition,6,event,timestamp,1...
Definition,7,record,timestamp,1...
Definition,8,length,timestamp,1...
Definition,9,lap,timestamp,1...
Definition,10,session,timestamp,1...
Definition,11,activity,timestamp,1...
The list of message types used with pool swim activities is similar to the list of message types commonly found in other Activity files. The only new message type used in pool swim files is the Length message. Since the example project already accounts for the other message types, very few changes are needed to start decoding pool swim Activity files.
First, the message listener and data models are updated to handle Length messages.
public void OnLengthMesg(object sender, MesgEventArgs e)
{
Messages.Lengths.Add(e.mesg as LengthMesg);
}
Next, the ActivityParser is updated to group Length messages with the session they belong to based on timestamps.
session.Lengths = _messages.Lengths.Where(length => length.Overlaps(sessionMesg)).ToList();
With those changes in place, the example project can now decode and parse pool swim Activity files.
The last step is to do something with the data. Since Length messages can be used to represent any fixed-length course, it is important to check the sport type before performing any pool swim specific processing.
if (session.Session.GetSport() == Sport.Swimming && session.Session.GetSubSport() == SubSport.LapSwimming && session.Lengths.Count > 0)
{
// Process Length Messages for Pool Swim Workouts
var lengthsCSV = Export.LengthsToCSV(session);
.
.
.
}
The example project implements a CSV exporter that creates a CSV file in a format that is commonly used to represent pool swim activities. The output CSV file contains a row for each Length message in the session. The SWOLF and Distance per Stroke (DPS) metrics are calculated from the data in each Length message.
#Lengths,Swimming,LapSwimming,2020-06-13 23:10:05,500,25,yards,0,840
LENGTH TYPE,DURATION (seconds),DISTANCE (yards),PACE,STOKE COUNT,SWOLF,DPS,STROKE RATE,STROKE TYPE
Active,20,25,1.25,30,50,0.83,90,Freestyle
Active,25,25,1,20,45,1.25,48,Freestyle
Active,30,25,0.83,10,40,2.5,20,Freestyle
Active,35,25,0.71,20,55,1.25,34,Freestyle
Idle,60,,,,,,,
Active,20,25,1.25,30,50,0.83,90,Backstroke
Active,25,25,1,20,45,1.25,48,Backstroke
Active,30,25,0.83,10,40,2.5,20,Backstroke
Active,35,25,0.71,20,55,1.25,34,Backstroke
Idle,60,,,,,,,
Active,20,25,1.25,30,50,0.83,90,Breaststroke
Active,25,25,1,20,45,1.25,48,Breaststroke
Active,30,25,0.83,10,40,2.5,20,Breaststroke
Active,35,25,0.71,20,55,1.25,34,Breaststroke
Idle,60,,,,,,,
Active,20,25,1.25,30,50,0.83,90,Butterfly
Active,25,25,1,20,45,1.25,48,Butterfly
Active,30,25,0.83,10,40,2.5,20,Butterfly
Active,35,25,0.71,20,55,1.25,34,Butterfly
Idle,60,,,,,,,
Active,40,25,0.63,,40,,,Drill
Active,40,25,0.63,,40,,,Drill
Active,40,25,0.63,,40,,,Drill
Active,40,25,0.63,,40,,,Drill
See the LengthsToCSV() method in the example project for the complete code listing.
If the file contains Record messages, a CSV file containing Record messages will also be created.
#Records,Swimming,LapSwimming,2020-06-13 23:10:05,457.2,0,840
Seconds,Timestamp,Distance,Speed,Cadence,EnhancedSpeed,TimerEvent,Lap
0,961024205,,,,,Start,1
20,961024225,22.86,1.143,90,1.143,,1
45,961024250,45.72,0.914,48,0.914,,1
75,961024280,68.58,0.762,20,0.762,,1
110,961024315,91.44,0.653,34,0.653,,1
170,961024375,114.3,,,,,2
190,961024395,114.3,1.143,90,1.143,,3
215,961024420,137.16,0.914,48,0.914,,3
245,961024450,160.02,0.762,20,0.762,,3
280,961024485,182.88,0.653,34,0.653,,3
340,961024545,205.74,,,,,4
360,961024565,205.74,1.143,90,1.143,,5
385,961024590,228.6,0.914,48,0.914,,5
415,961024620,251.46,0.762,20,0.762,,5
450,961024655,274.32,0.653,34,0.653,,5
510,961024715,297.18,,,,,6
530,961024735,297.18,1.143,90,1.143,,7
555,961024760,320.04,0.914,48,0.914,,7
585,961024790,342.9,0.762,20,0.762,,7
620,961024825,365.76,0.653,34,0.653,,7
680,961024885,388.62,,,,,8
720,961024925,388.62,0.572,,0.572,,9
760,961024965,411.48,0.572,,0.572,,9
800,961025005,434.34,0.572,,0.572,,9
840,961025045,457.2,0.572,,0.572,,9
840,961025045,,,,,StopAll,9
Plugins sit between the message broadcaster and the message listeners, and allow for the preprocessing of messages before the messages are sent to the message listeners. See the FIT Protocol for a description of the FIT Plugin Framework.
The HrToRecordMesgBroadcast plugin included with the FIT SDK can be used to preprocess HR messages recorded with a Garmin HRM-Tri or HRM-Swim heart rate monitor during pool swim or open water swim activities. During swimming activities the HRM uses a “store and forward” data recording technique; where the HRM stores the heart rate data recorded during the activity in a FIT file and then forwards the FIT file to the watch at the end of the activity using ANT-FS. The watch appends the FIT file from the HRM to the FIT Activity file recorded by the watch, creating a single file. This is known as a Chained FIT files. See the FIT Protocol for a description of Chained FIT files.
The FIT files created by the HRM contains compressed heart rate data, which needs to be expanded before it can be used. The goal of the HrToRecordMesgBroadcast plugin is to make the task of expanding the data in the HR messages transparent to the client application. The client application only see the resulting heart rate data in the Record messages, as if the heart rate data was recorded by the watch itself.
To use the HrToRecordMesgBroadcast, or any other, plugin the following updates to the client application need to be made.
The required changes are shown in the code below.
// Create the Decode Object
Decode decoder = new Decode();
// 1. Create a Buffered Message Broadcaster Object
BufferedMesgBroadcaster mesgBroadcaster = new BufferedMesgBroadcaster();
// Connect the the Decode and Message Broadcaster Objects
decoder.MesgEvent += mesgBroadcaster.OnMesg;
decoder.MesgDefinitionEvent += mesgBroadcaster.OnMesgDefinition;
// Subscribe to message events of interest by connecting to the Message Broadcaster
mesgBroadcaster.FileIdMesgEvent += OnFileIdMesg;
.
.
.
// 2. Create the plugin and regisiter it with the Buffered Message Broadcaster
IMesgBroadcastPlugin plugin = new HrToRecordMesgBroadcastPlugin();
mesgBroadcaster.RegisterMesgBroadcastPlugin(plugin);
// Decode the file
decoder.Read(stream);
// 3. Allow the plugins to process the messages,
// and broadcast the messages to the listeners
mesgBroadcaster.Broadcast();
During the decoding of the file, the Buffered Message Broadcaster buffers all of the decoded messages in an array. After all of the messages have been decoded from the file, the Broadcast() method passes the array of messages to each plugin. After each plugin has processed the messages, the the Buffered Message Broadcaster loops over the array of messages and passes the messages to the message listeners. The application then processes the messages as usual.
The purpose of the HrToRecordMesgBroadcast plugin is to reduce complexity when decoding Activity files that contain HR messages, and the plugin accomplishes this with minimal changes to the client application. But the plugin adds overhead to the processing of files. Since the decision to use the plugin is made at compile time the additional overhead is incurred by all files, not just those containing HR messages. Knowing that only a small percentage of files will contain HR messages a better solution would be to make the decision to apply the logic contained in the plugin at run time based on the messages found in the file. To accomplish this, the plugin code can be used as a reference design and adapted to work in the example project.
Looking at the source code for the HrToRecordMesgBroadcast plugin reveals two loops. An outer loop that searches an array of messages looking for Record messages, and an inner loop that looks within the same array for HR messages. These loops can can be modified to act upon the separate lists of Record and HR messages found in the example project’s FitMessages class, allowing the decision to execute the plugin code to be made at runtime. This eliminates the overhead added by the Buffered Message Listener and the HrToRecordMesgBroadcast plugin for files that do not contain HR messages.
// Decode the file
decoder.Read(stream);
// If there are HR messages, merge the heart-rate data with the Record messages.
if (Messages.HeartRates.Count > 0)
{
HrToRecordMesgWithoutPlugin.MergeHeartRates(Messages);
}
See the HrToRecordMesgWithoutPlugin class in the example project for the complete code listing. The changes are dependent on the data structures used in the example project, but can be adapted to work with any data model containing lists of Record and HR messages.