youmad / endurance-fit
Garmin FIT protocol implementation
Requires
- php: ^8.5
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.95.25
- phpstan/phpstan: ^2.2.13
- phpstan/phpstan-phpunit: ^2.0.18
- phpunit/phpunit: ^12.3
Suggests
None
Provides
None
Conflicts
None
Replaces
None
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.