Search by

youmad / endurance-fit

youmad

Garmin FIT protocol implementation

Package info

github.com/youmad/endurance-fit

pkg:composer/youmad/endurance-fit

Statistics

Installs: 8

Dependents: 1

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.3 2026-09-29 20:13 UTC

This package is auto-updated.

Last update: 2026-09-29 20:16:03 UTC


README

Streaming FIT decoding with explicitly supplied message and type profiles.

The package reads binary FIT streams, decodes field values, and resolves profile metadata into unified messages. Activity validation, persistence, and GPX export belong to other packages.

Features

  • streaming reads of standalone and chained FIT files, with each header and file CRC verified;
  • little- and big-endian fields, compressed timestamps, and invalid values;
  • symbolic values, scale/offset transforms, subfields, and components;
  • accumulated components and developer-field metadata;
  • raw and profile-aware decoding entry points;
  • local generation of PHP registries from a separately supplied workbook.

Installation

composer require youmad/endurance-fit

A 64-bit PHP build is required. Generating registries from an XLSX workbook also requires Phar support.

Prepare a Profile

Profile data is supplied by the application. This package does not bundle Profile.xlsx, messages.php, or types.php, and does not download them.

For the tested configuration, obtain Profile.xlsx from Garmin FIT SDK 21.214, subject to Garmin's terms, using the official FIT SDK site. The tested workbook has this SHA-256:

609b09e3d35028054ff78ecbef8d6f5e267206d8df6842c09511167fc5ce0ad0

Keep the workbook and generated registries in your application, outside vendor/. The examples below use var/fit-profile: create that directory and place your separately obtained workbook at var/fit-profile/Profile.xlsx. Run all commands from the application root.

Check the workbook checksum before generation:

sha256sum var/fit-profile/Profile.xlsx

Composer exposes the generators in vendor/bin (or your configured bin-dir):

vendor/bin/fit-generate-types \
    --input=var/fit-profile/Profile.xlsx \
    --output=var/fit-profile/types.php
vendor/bin/fit-generate-messages \
    --input=var/fit-profile/Profile.xlsx \
    --output=var/fit-profile/messages.php

Here var/fit-profile belongs to the consuming application, outside the installed package. Obtain the workbook separately and keep it and generated registries out of distributed source. Both commands support --help and --check.

Compare the checksum before generation. Both commands require explicit input and output paths. Add --check to compare the existing output with freshly generated bytes without changing it. Other workbook versions are not implied to be compatible merely because their FIT files use protocol 2.0.

Decode a file

With dependencies installed and registries generated, save this example as a PHP script in the application root. Replace /path/to/activity.fit with your input:

<?php

declare(strict_types=1);

require __DIR__ . '/vendor/autoload.php';

use Youmad\Endurance\Fit\Decoder\FitDecoder;
use Youmad\Endurance\Fit\IO\ResourceFitInput;
use Youmad\Endurance\Fit\Profile\Generated\GeneratedFitProfileSet;

$profile = GeneratedFitProfileSet::load(
    messagesFile: __DIR__ . '/var/fit-profile/messages.php',
    typesFile: __DIR__ . '/var/fit-profile/types.php',
);

$handle = fopen('/path/to/activity.fit', 'rb');

if (false === $handle) {
    throw new RuntimeException('Cannot open the FIT file.');
}

try {
    $stream = FitDecoder::standard($profile)->open(
        new ResourceFitInput($handle),
    );

    $messageCount = 0;

    foreach ($stream->messages() as $message) {
        // Process each UnifiedDataMessage here.
        ++$messageCount;
    }

    $trailer = $stream->trailer();
    printf("Decoded %d messages; CRC 0x%04X verified.\n", $messageCount, $trailer->declaredCrc);
} finally {
    fclose($handle);
}

The stream is consumed once and reads all FIT members up to input EOF. Each member has its own header, data section and CRC; failure in a later member fails the stream. Stopping iteration early does not verify the whole input. The caller owns and closes the input resource.

A chain shares local message definitions, component/developer state and message sequence numbers. At member boundaries, timestamp state follows Garmin SDK 21.214's nextFile() semantics: the timestamp is retained and its compressed offset is reset. Independent open() calls have fresh state.

header describes the first member. trailer() is available only after the entire chain succeeds and describes the final member; it is not an aggregate CRC. Consumers which embed a FIT stream inside another format must provide a bounded FitInput for the FIT portion. Custom FitInput implementations must implement isAtEnd() without consuming a logical byte or confusing a read error with EOF. ResourceFitInput uses one-byte lookahead, so it also works without seeking.

Both registries must record the same source workbook hash. This detects mixed generation outputs, not authenticity: the registries are executable PHP and must come from a trusted source.

For raw messages without Profile resolution, use Decoder\FitFileReader. For custom profiles, construct FitDecoder with implementations of Profile\FitProfileRegistry and Profile\FitTypeRegistry. Loading generated registries is one configuration option, not a dependency of the raw parser.

Development

composer install
composer check

Composer does not create vendor/bin proxies for the root package's own commands. To run the generators from this checkout, use their source paths and keep Profile data outside the checkout, for example:

php tools/profile-generator/bin/fit-generate-types \
    --input=../fit-profile/Profile.xlsx \
    --output=../fit-profile/types.php
php tools/profile-generator/bin/fit-generate-messages \
    --input=../fit-profile/Profile.xlsx \
    --output=../fit-profile/messages.php

test runs all package-local tests without external Profile data. check validates Composer metadata, runs the same tests, analyses source code, tests, and both CLI generators with PHPStan (level 6), and checks their code style with PHP CS Fixer (@Symfony). The explicit test:without-profile command remains available.

Run individual checks or apply code-style fixes with:

composer test
composer analyse
composer cs:check
composer cs:fix

These commands work without Tracker's private directories. They check parser, CRC, value, profile-model, and generator mechanics; they do not establish compatibility with a particular Garmin Profile. External-Profile conformance tests and generated-output checks run separately in the Tracker monorepository. Those tests and data are not distributed with this package.

License and external materials

The project-authored source code in src/ and tools/, package-local tests, configuration, and documentation are licensed under the Mozilla Public License 2.0 (MPL-2.0). The following notice applies to those files:

This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.

See LICENSE. Dependencies retain their own licenses.

This grant covers the generator's source code; it does not grant rights to Garmin SDK materials, Profile.xlsx, or Garmin-derived contents of generated registries. Running the generator does not make those contents MPL-licensed. Obtain and use the input and outputs under the applicable rights and terms; keep them outside the package's distributed source tree.

FIT is a Garmin protocol. Garmin names and marks belong to their respective owners. This is an independent project, not an official Garmin SDK or a Garmin-endorsed product.

Decoder session ownership

FitDecodingSession assembles the unified processor and the lower-level components used by specialized projections. Its messages processor shares componentValues and developerFields with those projections. It also owns the raw decoder and its per-definition cache, profile normalizer, and component extractor. Create a fresh session for every independent stream; never share one between concurrent files. Profile and type registries may be shared.

FitDecoder creates a fresh session for each decode(), decodeStream(), open(), and newMessageProcessor() invocation. The existing processor reset() clears accumulation and developer state, including the state visible to a specialized projection. Definition caches remain instance-local and keyed by definition identity. The FIT package has no Activity dependency.