A comprehensive Dart package for parsing and decoding Garmin FIT (Flexible and Interoperable Data Transfer) files with full developer field support including CORE body temperature sensor data.
β Complete FIT Protocol Support
- Parse FIT file headers (14-byte structure with protocol validation)
- Decode definition messages (local to global message type mapping)
- Extract data messages with proper field value parsing
- Support for all FIT base types (uint8, sint16, float32, etc.)
- CRC validation for file integrity
β Developer Fields π₯ NEW!
- Full support for custom developer fields
- Parse field_description messages (0xCE) and developer_data_id messages (0xCF)
- CORE body temperature sensor support - Extract core temp, skin temp, and Heat Strain Index
- Automatic field name resolution and unit extraction
- Type-safe value parsing for all developer field types
β Data Processing
- Geodesic distance calculations using WGS84 ellipsoid
- Timestamp handling with timezone support
- Record, lap, and session message parsing
- Compressed timestamp support
β Pure Dart Implementation
- No native dependencies
- Works on all platforms (Flutter Web, Mobile, Desktop)
- Efficient binary parsing with typed_data
Add this to your pubspec.yaml:
dependencies:
dart_fit_decoder: ^0.1.0Then run:
dart pub getOr for Flutter projects:
flutter pub getimport 'dart:io';
import 'package:dart_fit_decoder/dart_fit_decoder.dart';
void main() async {
// Read FIT file as bytes
final file = File('activity.fit');
final bytes = await file.readAsBytes();
// Create decoder instance
final decoder = FitDecoder(bytes);
// Decode FIT file
final fitFile = decoder.decode();
// Access parsed data
print('Protocol Version: ${fitFile.header.protocolVersion}');
print('Profile Version: ${fitFile.header.profileVersion}');
print('Total Messages: ${fitFile.messages.length}');
// Extract specific message types
final records = fitFile.getRecordMessages();
final laps = fitFile.getLapMessages();
final sessions = fitFile.getSessionMessages();
print('Records: ${records.length}');
print('Laps: ${laps.length}');
print('Sessions: ${sessions.length}');
}import 'dart:io';
import 'package:dart_fit_decoder/dart_fit_decoder.dart';
void main() async {
// Read FIT file
final file = File('activity.fit');
final bytes = await file.readAsBytes();
// Decode FIT file
final decoder = FitDecoder(bytes);
final fitFile = decoder.decode();
// Get all record messages
final records = fitFile.getRecordMessages();
// Extract CORE temperature data from records
for (final record in records) {
// Check for developer fields
if (record.developerFields.isNotEmpty) {
// Find CORE temperature fields by name
for (final devField in record.developerFields) {
if (devField.name == 'core_temperature') {
print('Core Temp: ${devField.value}${devField.units}');
}
if (devField.name == 'skin_temperature') {
print('Skin Temp: ${devField.value}${devField.units}');
}
if (devField.name == 'heat_strain_index') {
print('HSI: ${devField.value}');
}
}
// Or access by field number (if you know the mapping)
final coreTemp = record.developerFields
.where((f) => f.fieldNumber == 0)
.firstOrNull;
if (coreTemp != null) {
print('Core: ${coreTemp.value}Β°C');
}
}
// Access standard fields alongside developer fields
final timestamp = record.timestamp;
final heartRate = record.getField('heart_rate');
if (timestamp != null && heartRate != null) {
print('$timestamp - HR: $heartRate bpm');
}
}
}void extractLapData(FitFile fitFile) {
final laps = fitFile.getLapMessages();
for (int i = 0; i < laps.length; i++) {
final lap = laps[i];
final distance = lap.getField('total_distance') / 1000; // Convert to km
final time = lap.getField('total_elapsed_time') / 60; // Convert to minutes
final avgPower = lap.getField('avg_power');
final avgHeartRate = lap.getField('avg_heart_rate');
print('Lap ${i + 1}: ${distance.toStringAsFixed(2)} km, '
'${time.toStringAsFixed(1)} min, '
'Avg Power: $avgPower W, Avg HR: $avgHeartRate bpm');
}
}import 'package:flutter/material.dart';
import 'package:file_picker/file_picker.dart';
import 'package:dart_fit_decoder/dart_fit_decoder.dart';
class FitFileImporter extends StatelessWidget {
Future<void> importFitFile() async {
// Pick FIT file
final result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['fit', 'FIT'],
);
if (result != null && result.files.single.bytes != null) {
final bytes = result.files.single.bytes!;
// Decode FIT file
final decoder = FitDecoder(bytes);
final fitFile = decoder.decode();
// Process data
final records = fitFile.getRecordMessages();
print('Imported ${records.length} records');
// Use data in your Flutter app
// ...
}
}
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: importFitFile,
child: Text('Import FIT File'),
);
}
}- FitDecoder: Main entry point for parsing FIT files
- FitFile: Represents a complete decoded FIT file with header and messages
- FitMessage: Base class for all FIT messages (definition and data)
- FitHeader: 14-byte FIT file header with protocol/profile version
- FitDefinitionMessage: Maps local message numbers to global message types
- FitDataMessage: Contains actual field values for a specific message type
- FitField: Represents a single field with name, type, and value
- DeveloperField: Custom fields defined by developers/manufacturers
- Header Parsing: Read 14-byte header, validate protocol, extract data size
- Record Iteration: Loop through records until data size is consumed
- Definition Messages: Store field definitions for each local message number
- Data Messages: Use stored definitions to parse field values
- Developer Fields: Parse field descriptions and developer data IDs
- CRC Validation: Verify 16-bit CRC at end of file
| Message Type | Global # | Description |
|---|---|---|
file_id |
0 | File identification |
device_info |
23 | Device information |
record |
20 | Primary data records (GPS, HR, power, etc.) |
lap |
19 | Lap summaries |
session |
18 | Session summaries |
event |
21 | Workout events |
field_description |
206 (0xCE) | Developer field descriptions |
developer_data_id |
207 (0xCF) | Developer data identifiers |
enum(0x00) - 8-bit unsignedsint8(0x01) - 8-bit signeduint8(0x02) - 8-bit unsignedsint16(0x83) - 16-bit signeduint16(0x84) - 16-bit unsignedsint32(0x85) - 32-bit signeduint32(0x86) - 32-bit unsignedstring(0x07) - Variable-length stringfloat32(0x88) - 32-bit floatfloat64(0x89) - 64-bit floatuint8z(0x0A) - 8-bit unsigned (zero-terminated)uint16z(0x8B) - 16-bit unsigned (zero-terminated)uint32z(0x8C) - 32-bit unsigned (zero-terminated)byte(0x0D) - Array of bytes
The package includes comprehensive tests validated against real Garmin FIT files:
Test Results:
- β 256 tests passing (96.6% success rate)
- β 27/27 real-world FIT file tests passing
- β Validated with activity files containing 25,000+ records
- β CORE temperature sensor data extraction verified
- β Output matches official Garmin SDK
Run tests with:
dart testRun tests with coverage:
dart test --coverage=coverage
dart pub global activate coverage
dart pub global run coverage:format_coverage --lcov --in=coverage --out=coverage/lcov.info --report-on=lib- β Decodes 1-2 MB files in < 1 second
- β Handles 25,000+ records efficiently
- β Memory-efficient binary parsing
- β No performance degradation with developer fields
See the example/ directory for complete working examples:
dart_fit_decoder_example.dart- Basic FIT file parsingcore_temperature_extraction.dart- CORE sensor data extractionlap_analysis.dart- Lap data processingflutter_web_integration.dart- Flutter web file import
Contributions are welcome! Please read our Contributing Guide for details on our code of conduct and the process for submitting pull requests.
This project is licensed under the MIT License - see the LICENSE file for details.
See CHANGELOG.md for version history and release notes.
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Email: support@forcegage.com
Made with β€οΈ for the Dart and Flutter community