The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.

NAME

Parser::FIT - A parser for garmin FIT (Flexible and Interoperable Data Transfer) files

SYNOPSIS

  use Parser::FIT;

  my $recordCount = 0;
  my $parser = Parser::FIT->new(on => {
    record => sub { $recordMsg = shift; $recordCount++; }
  });

  $parser->parse("some/file.fit");

  print "The file contained $recordCount records.";

ALPHA STATUS

The module is in an early alpha status. APIs may change. Parse results may be wrong.

Additionally i will probably not implement the full set of FIT messages. I started the module for my personal needs to be able to parse FIT files from my garmin bike computer. So results for e.g. a triathlon multisport watch may varry greatly!

But this module is free and open source: Feel free to contribute code, example data, etc!

METHODS

new

Create a new Parser::FIT object.

  Parser::FIT->new(
          debug => 1|0 # enable/disable debug output. Disabled by default
          on => { # Provide a hashref of message handlers
                sessiont => sub { },
                lap => sub { },
          }
  )

on

Register and deregister handlers for a parser.

  $parser->on(record => sub { my $message = shift; });

Concrete message handlers receive on paramter which represents the parsed message. See "MESSAGES" for more details.

Registering an already existing handler overwrites the old one.

  $parser->on(session => sub { say "foo" });
  $parser->on(session => sub { say "bar" }); # Overwrites the previous handler

Registering a falsy value for a message type will deregister the handler:

  $parser->on(session => undef);

There is currently no check, if the provided message name actually represents an existing one from the FIT specs.

Additionally there is a special message name: _any. Which can be used to receive just every message encountered by the parser. The _any handler receives two parameters. The first one is the messageType which is just a string with the name of the message. The second one is a message hash-ref.

  $parser->on(_any => sub {
          my $messageType = shift;
          my $message = shift;

          print "Saw a message of type $msgType";
  });

The on method can also be called from inside a handler callback in order to de-/register handlers based on the stream of events

  # Count the number of records per lap
  my $lapCount = 0;
  my $lapResults = [];
  $parser->on("lap" => sub {
          my $lapMsg = shift;
          my $lapCount++;
          $parser->on("record" => {
                  $lapResults[$lapCount]++;
          });
  });

parse

Parse a file and call registered message handlers.

  $parser->parse('/some/file.fit');

parse_data

Parse FIT data contained in a scalar and call registered message handlers.

  $parser->parse_data($inMemoryFitData);

DATA STRUCTURES

This section explains the used data structures you may or may not encounter when using this module.

MESSAGES

A message is a hash-ref where the keys map to fieldnames defined by the FIT Profile (aka Profile.xls) for the given message.

The FIT protocol defines so called local messages which allow to only store a subset of the so called global message. For example the session global message defines 134 fields, but an actually recorded session message in a FIT file may only contain 20 of these.

This way it is possible to create FIT files which only contain the data the device is currently "seeing". But this also means, that this data may change "in-flight". For example if a session is started without an heartrate sensor, the include FIT data will not have heartrate related data. When later in the session the user straps on a heartrate sensor and pairs it with his device, all upcoming data inside the FIT file will have heartrate data. The same is true for sensors/data that goes away while recording.

Therefore you always have to check if the desired data is actually in the message.

For a list of field names you may expect to see, you can check the Garmin FIT SDK. It includes a Profile.xls file which defines all the valid fields for every global message.

The fields of a message are represented as message fields.

An example record message:

  {
    'speed' => {
      'fieldDescriptor' => {
                              'name' => 'speed',
                              'id' => '6',
                              'scale' => 1000,
                              'unit' => 'm/s',
                              'type' => 'uint16',
                              'offset' => undef
                          },
      'rawValue' => 2100
      'value' => '2.1',
    },
    'position_lat' => {
        'fieldDescriptor' => { }, # skipped for readability
        'rawValue' => 574866379,
        'value' => '48.184743'
    },
    'distance' => {
        'fieldDescriptor' => { }, # skipped for readability
        'rawValue' => 238,
        'value' => '2.38',
    },
    'heart_rate' => {
      'fieldDescriptor' => { }, # skipped for readability
      'value' => 70,
      'rawValue' => 70
      },
    'timestamp' => {
      'rawValue' => 983200317,
      'fieldDescriptor' => { }, # skipped for readability
      'value' => 1614265917
      },
    'altitude' => {
        'value' => '790.8',
        'fieldDescriptor' => { }, # skipped for readability
        'rawValue' => 6454
    },
    'position_long' => {
        'fieldDescriptor' => { }, # skipped for readability
        'value' => '9.102652',
        'rawValue' => 108598869
      }
  }
=head2 MESSAGE FIELDS

A message field represents actuall data inside a message. It consists of a hash-ref containg:

value

The value after post processing.

rawValue

The original value as it is stored in the FIT file.

fieldDescriptor

A hash-ref containing a field descriptor which describes this field.

FIELD DESCRIPTOR

A field descriptor is just a hash-ref with some key-value pairs describing the underlying field.

The keys are:

id

The id of the field in relation to the message type.

name

The name of the field this descriptor represents.

unit

The unit of measurement (e.G. kcal, m, bpm).

scale

The scale by which the rawValue needs to be scaled.

type

The original FIT data type (e.G. uint8, date_time).

The values for these keys are directly taken from the FIT Profile.xls.

POST PROCESSING

SCALE

The FIT protocol defines for various data fields a scale (e.G. distances define a scale of 100) in order to optimize the low-level storage type.

Parser::FIT divides the rawValue by the scale and stores the result in value. The rawValue stays untouched.

OFFSET

The FIT protocol defines for various data fields an offset (e.G. altitude values are offset by 500m) in order to optimize the low-level storage type.

Parser::FIT subtracts the offsets from the rawValue and stores the result in value. The rawValue stays untouched.

CONVERSIONS

The FIT protocol defines various special data types. Parser::FIT converts the following types to "more usefull" ones:

SEMICRICLES

Fields with the data type semicricles get converted to degrees via this formula: degrees = semicircles * (180/2^31).

So the value of a field with data type semicricles is in degrees. The rawValue stays in semicircles.

DATE_TIME

Fields with the data type date_time get converted to unix epoche timestamps via this formula: unixTimestamp = fitTimestamp + 631065600.

Internally FIT is using it's own epoche starting at December 31, 1989 UTC.

AUTHOR

This module was created by Sven Eppler <ghandi@cpan.org>

COPYRIGHT AND LICENSE

Copyright (C) 2018-2022 by Sven Eppler

This program is free software, you can redistribute it and/or modify it under the terms of the Artistic License version 2.0.

SEE ALSO

Parser::FIT::Simple, Garmin FIT SDK