FIT Files Structureο
This document provides a comprehensive guide to the FIT (Flexible and Interoperable data Transfer) file structure generated by the ActivityWriter class in the UNA SDK. FIT is a binary file format developed by Garmin for storing fitness data, enabling interoperability between devices and applications. This guide is designed for new developers, covering everything from basic concepts to advanced implementation details, with a focus on practical code usage.
The UNA SDK leverages the FIT protocol to serialize activity data (running, cycling, hiking, heart rate monitoring) into .fit files. These files are compatible with tools like Garmin Connect, Strava, and third-party FIT parsers. The document emphasizes code examples from ActivityWriter.cpp implementations across different apps, helping developers understand how to create, modify, and extend FIT file generation.
How to Read This Documentο
For New Developers: Getting Started Guideο
If youβre new to FIT files and the UNA SDK, follow this reading path to build your understanding progressively:
Phase 1: Foundations (Essential - Read First)ο
Introduction to FIT - Understand what FIT is and why itβs used
FIT File Format Basics - Learn binary format, data types, and scaling
ActivityWriter Class Overview - Get familiar with the main class
Data Structures in ActivityWriter - Understand the data you work with
Phase 2: Implementation (Core Development)ο
FitHelper Component Deep Dive - Learn the helper class that does the work
Step-by-Step FIT File Creation - Follow the file creation process
Code Usage Examples and Walkthroughs - Study real code examples
Phase 3: Customization (When You Need to Modify)ο
Activity-Specific Variations - See how different activities are implemented
Developer Fields Implementation - Add custom data fields
Extending ActivityWriter for New Activities - Create new activity types
Phase 4: Advanced Topics (As Needed)ο
Advanced Topics and Best Practices - Performance and edge cases
Troubleshooting Common Issues - Debug problems
FIT File Parsing and Validation - Test and validate files
Key Sections for Different Tasksο
Task |
Primary Sections to Read |
Why |
|---|---|---|
Understanding FIT Basics |
Introduction, Format Basics |
Learn the protocol |
Using Existing ActivityWriter |
Class Overview, Data Structures, Code Examples |
Integrate into your app |
Adding Custom Fields |
Developer Fields, Extending ActivityWriter |
Customize data |
Creating New Activity Types |
Activity Variations, Extending ActivityWriter |
Build new sports |
Debugging FIT Issues |
Troubleshooting, Validation |
Fix problems |
Performance Optimization |
Advanced Topics, Best Practices |
Improve efficiency |
Essential Headers and Includesο
When working with FIT files in UNA SDK, include these headers in your source files:
Core FIT Functionalityο
// Main ActivityWriter class
#include "ActivityWriter.hpp"
// FitHelper for low-level FIT operations
#include "SDK/FitHelper/FitHelper.hpp"
// Kernel for file system access
#include "SDK/Kernel/Kernel.hpp"
// File system interface
#include "SDK/Interfaces/IFileSystem.hpp"
FIT SDK Headers (C-based)ο
// External C includes for FIT protocol
extern "C" {
#include "fit_product.h" // FIT product definitions
#include "fit_crc.h" // CRC calculation functions
#include "fit_example.h" // Example structures (may vary)
}
Standard Library Headersο
#include <cstdint> // Fixed-width integer types
#include <cstdbool> // Boolean type
#include <cstring> // String manipulation
#include <cassert> // Assertions
#include <memory> // Smart pointers
#include <string> // String class
UNA SDK Logger (Optional but Recommended)ο
#define LOG_MODULE_PRX "YourModule"
#define LOG_MODULE_LEVEL LOG_LEVEL_DEBUG
#include "SDK/UnaLogger/Logger.h"
Quick Start Checklistο
Before starting development:
[ ] Read βIntroduction to FITβ and βFIT File Format Basicsβ
[ ] Understand your activity type (see βActivity-Specific Variationsβ)
[ ] Review βData Structures in ActivityWriterβ for data formats
[ ] Look at code examples in existing apps (Running, Cycling, etc.)
When implementing:
[ ] Include the headers listed above
[ ] Follow the constructor pattern from examples
[ ] Use the method call sequence: start() β addRecord()* β addLap()* β stop()
[ ] Handle errors and validate data
For testing:
[ ] Use FIT SDK validator on generated files
[ ] Check with Garmin Connect or similar apps
[ ] Review logs for errors
π Getting Started: Where to Find ActivityWriterο
In just 1 minute, hereβs how to get the ActivityWriter code:
Location in UNA SDKο
The ActivityWriter class is located in the example apps:
Examples/Apps/[ActivityType]/Software/Libs/
βββ Header/ActivityWriter.hpp # Class declaration
βββ Sources/ActivityWriter.cpp # Implementation
Available Implementationsο
Running:
Examples/Apps/Running/Software/Libs/Cycling:
Examples/Apps/Cycling/Software/Libs/Hiking:
Examples/Apps/Hiking/Software/Libs/HRMonitor:
Examples/Apps/HRMonitor/Software/Libs/
Quick Copy Stepsο
Choose your activity type (Running, Cycling, Hiking, or HRMonitor)
Navigate to:
Examples/Apps/[YourChoice]/Software/Libs/Copy both files:
Header/ActivityWriter.hppSources/ActivityWriter.cpp
Paste into your appβs
Software/Libs/directoryInclude in your code:
#include "ActivityWriter.hpp"
Basic Usage (3 lines of code)ο
// 1. Create instance
ActivityWriter writer(kernel, "/path/to/fit/files");
// 2. Start activity
AppInfo info = {timestamp, version, devID, appID};
writer.start(info);
// 3. Add data and stop
writer.addRecord(recordData);
writer.stop(trackData);
Need Help?ο
All implementations are nearly identical - start with Running as the base
Check existing app implementations for integration examples
See βCode Usage Examplesβ section for detailed walkthroughs
Pro Tip: The ActivityWriter is ready-to-use - just copy the files and call the methods!
Table of Contentsο
Introduction to FITο
What is FIT?ο
FIT (Flexible and Interoperable data Transfer) is a binary file format and protocol developed by Garmin for storing fitness and activity data. Itβs designed to be compact, extensible, and platform-independent, allowing seamless data exchange between fitness devices, apps, and services like Garmin Connect, Strava, and TrainingPeaks.
Key characteristics:
Binary Format: Efficient storage with minimal overhead.
Self-Describing: Files include metadata about their structure.
Extensible: Supports standard fields and custom developer fields.
Versioned: Protocol and profile versions ensure compatibility.
In the UNA SDK, FIT files are used to export activity data from wearable devices, making it compatible with the broader fitness ecosystem.
Key Concepts for Beginnersο
Understanding FIT requires grasping a few core concepts:
Messages: The basic units of data in a FIT file. Each message represents a specific type of information, such as a GPS coordinate (Record message) or activity summary (Session message).
Definitions: Metadata that describes the structure of messages. Before writing data messages, you must write a definition message that specifies which fields are included and their data types.
Fields: Individual data elements within messages. For example, a Record message might include fields for timestamp, latitude, longitude, heart rate, etc.
Developer Fields: Custom fields not defined in the standard FIT profile. These allow apps to add proprietary data while maintaining FIT compatibility.
CRC (Cyclic Redundancy Check): A checksum appended to the file for data integrity verification.
Local Message Numbers: Identifiers (0-15) used within a FIT file to reference message types, allowing efficient compression.
FIT Protocol Versionsο
The UNA SDK uses specific FIT versions to ensure compatibility:
Protocol Version: 2.0 (
FIT_PROTOCOL_VERSION_20) - Defines the basic file structure and message encoding.Profile Version: As defined in
FIT_PROFILE_VERSION- Specifies the available message types and fields. This is typically the latest version supported by the FIT SDK.
These versions are set in the file header and ensure that FIT parsers can correctly interpret the file.
Why FIT in UNA SDK?ο
Interoperability: Export data to Garmin devices and third-party apps.
Standards Compliance: Follows established fitness data standards.
Extensibility: Add custom fields for UNA-specific features.
Efficiency: Binary format is ideal for resource-constrained embedded devices.
Prerequisites for Understandingο
Before diving deeper, ensure you understand:
Basic C++ programming
File I/O operations
Bit manipulation and binary data handling
The UNA SDK architecture (Kernel, Interfaces, etc.)
FIT File Format Basicsο
File Structure Overviewο
A FIT file consists of three main parts:
File Header (14 bytes): Contains metadata about the file.
Data Records: A sequence of definition and data messages.
CRC (2 bytes): Integrity check.
The header is written first, then data records are appended, and finally the CRC is calculated and written.
Byte Order and Endiannessο
FIT uses little-endian byte order for multi-byte values. When writing data, ensure proper byte ordering, especially on big-endian systems.
Message Encodingο
Messages are encoded as:
Definition Messages: Describe message structure (field numbers, types, sizes).
Data Messages: Contain the actual data values.
Each message starts with a header byte indicating the message type and local message number.
Data Types in FITο
FIT defines several base types:
FIT_BASE_TYPE_UINT8(1 byte)FIT_BASE_TYPE_SINT8(1 byte)FIT_BASE_TYPE_UINT16(2 bytes)FIT_BASE_TYPE_SINT16(2 bytes)FIT_BASE_TYPE_UINT32(4 bytes)FIT_BASE_TYPE_SINT32(4 bytes)FIT_BASE_TYPE_STRING(variable)FIT_BASE_TYPE_FLOAT32(4 bytes)FIT_BASE_TYPE_FLOAT64(8 bytes)FIT_BASE_TYPE_UINT8Z(1 byte, unsigned with invalid value)FIT_BASE_TYPE_UINT16Z(2 bytes)FIT_BASE_TYPE_UINT32Z(4 bytes)FIT_BASE_TYPE_BYTE(1 byte array)
Scaling and Unitsο
Many FIT fields use scaled integers to represent floating-point values efficiently:
Speed: Stored as uint16 in mm/s (divide by 1000 for m/s)
Distance: Stored as uint32 in cm (divide by 100 for meters)
Altitude: Stored as uint32 in mm with offset (formula:
(altitude + 500) * 5)Time: Stored as uint32 in ms
Always refer to the FIT profile documentation for exact scaling factors.
ActivityWriter Class Overviewο
Class Purpose and Architectureο
The ActivityWriter class is responsible for serializing activity data into FIT files. It encapsulates the complexity of FIT message creation, ensuring proper sequencing and data integrity.
Key responsibilities:
Initialize FIT message handlers
Create and manage FIT files
Write message definitions and data
Handle developer fields
Calculate and append CRC
Constructor and Initializationο
ActivityWriter::ActivityWriter(const SDK::Kernel& kernel, const char* pathToDir)
Takes a reference to the UNA Kernel for file system access
Stores the base path for FIT file storage
Initializes multiple
FitHelperobjects for different message types
The constructor sets up message handlers with specific local message numbers and field subsets:
mFHFileID(static_cast<uint8_t>(MsgNumber::FILE), (FIT_MESG_DEF*)fit_mesg_defs[FIT_MESG_FILE_ID])
mFHLap(static_cast<uint8_t>(MsgNumber::LAP), (FIT_MESG_DEF*)fit_mesg_defs[FIT_MESG_LAP])
// ... more initializations
Each FitHelper is initialized with a subset of fields relevant to the activity:
mFHLap.init({ FIT_LAP_FIELD_NUM_TIMESTAMP,
FIT_LAP_FIELD_NUM_START_TIME,
// ... other fields
});
Public Interface Methodsο
start(const AppInfo&): Begins FIT file creationaddRecord(const RecordData&): Adds GPS/HR data pointsaddLap(const LapData&): Adds lap summariespause/resume(std::time_t): Handles activity pausesstop(const TrackData&): Finalizes the FIT filediscard(): Aborts file creation
Data Structures in ActivityWriterο
AppInfo Structο
struct AppInfo {
std::time_t timestamp; // UTC start time
uint32_t appVersion; // Version as 4-byte LE
std::string devID; // Developer ID (max 16 chars)
std::string appID; // Application ID (max 16 chars)
};
Used to populate File ID and Developer Data ID messages.
RecordData Structο
struct RecordData {
enum class Field : uint8_t {
COORDS = 1u << 0, // lat/long valid
SPEED = 1u << 1,
ALTITUDE = 1u << 2,
HEART_RATE = 1u << 3,
BATTERY = 1u << 4,
};
// Methods to set/clear/check fields
void set(Field f) { mFlags |= mask(f); }
bool has(Field f) const { return (mFlags & mask(f)) != 0; }
// Data members
std::time_t timestamp;
float latitude, longitude;
float speed; // m/s
float altitude; // m
float heartRate; // bpm
uint8_t batteryLevel; // %
uint16_t batteryVoltage; // mV
private:
uint8_t mFlags = 0;
};
Uses bit flags for efficient field presence checking.
LapData and TrackData Structsο
Similar structures for lap and session data, containing timing, distance, speed, and physiological metrics.
FitHelper Component Deep Diveο
What is FitHelper?ο
FitHelper is a wrapper class that simplifies FIT message creation. It handles:
Message definition generation
Data serialization
Developer field management
File I/O operations
Constructor Variantsο
Standard Messages:
FitHelper(uint8_t msgID, FIT_MESG_DEF* msgDef);
For predefined FIT messages like File ID, Record, etc.
Developer Fields:
FitHelper(uint8_t msgID, uint8_t fieldID, std::initializer_list<FitHelper*> container, FIT_UINT8 itemsCount = 1, FIT_UINT8 devIndex = 0);
For custom fields attached to parent messages.
Initialization Processο
bool init(std::initializer_list<FIT_UINT8> fields = {});
Validates field numbers against the base definition
Creates optimized message definition with only specified fields
Builds internal field offset/size mappings
Writing Messagesο
bool writeDef(SDK::Interface::IFile* fp); // Write definition
bool writeMessage(const void* data, SDK::Interface::IFile* fp); // Write data
void writeFieldMessage(uint8_t idx, const void* data, SDK::Interface::IFile* fp); // Write developer field
Internal Mechanicsο
Maintains a reduced
FIT_MESG_DEFstructureTracks field offsets and sizes for efficient data extraction
Handles endianness conversion
Manages developer field associations
Step-by-Step FIT File Creationο
1. Initialization Phaseο
ActivityWriter writer(kernel, "/path/to/fit/files");
2. Start Activityο
AppInfo info = {timestamp, version, devID, appID};
writer.start(info);
Creates/opens FIT file with timestamp-based name
Writes file header (placeholder)
Writes File ID message
Writes Developer Data ID message
Writes Field Description messages for developer fields
Writes definition messages for all used message types
Writes initial Event (START) message
3. Record Data Pointsο
RecordData record = {/* populate data */};
record.set(RecordData::Field::COORDS);
record.set(RecordData::Field::HEART_RATE);
// ...
writer.addRecord(record);
Prepares
FIT_RECORD_MESGwith scaled/converted dataSelects appropriate FitHelper based on available fields (GPS, battery, etc.)
Writes record message and any developer fields
4. Add Lapsο
LapData lap = {/* lap summary data */};
writer.addLap(lap);
Populates
FIT_LAP_MESGwith scaled dataWrites lap message
Attaches developer fields (steps, floors for Hiking)
5. Handle Pauses/Resumesο
writer.pause(timestamp);
// ... paused activity
writer.resume(timestamp);
Writes Event messages for STOP/START
6. Stop and Finalizeο
TrackData track = {/* final summary */};
writer.stop(track);
Writes final Event (STOP)
Writes Session message with developer fields
Writes Activity message
Updates file header with correct data size
Calculates and appends CRC
Saves JSON summary file
File Naming Conventionο
Files are named: activity_YYYYMMDDTHHMMSS.fit
Based on activity start time in local timezone
Stored in year/month subdirectories:
path/YYYY/MM/
Message Types and Fields in Detailο
File ID Messageο
Purpose: Identifies the file and creator device/app.
Fields:
type:FIT_FILE_ACTIVITY(4)manufacturer:FIT_MANUFACTURER_DEVELOPMENT(255)product: 0 (development product)serial_number: 0time_created: FIT timestamp of activity startproduct_name: βUNA Watchβ (truncated to 20 chars)
Developer Data ID Messageο
Purpose: Registers developer and app for custom fields.
Fields:
developer_id: Developer identifier stringapplication_id: Application identifier stringapplication_version: Version numbermanufacturer_id:FIT_MANUFACTURER_DEVELOPMENTdeveloper_data_index: 0
Field Description Messagesο
Purpose: Define custom developer fields.
Common Structure:
field_name: Field name string (e.g., βbatteryLevelβ)units: Units string (e.g., β%β, βmVβ)developer_data_index: 0field_definition_number: Unique field IDfit_base_type_id: Data type (e.g.,FIT_BASE_TYPE_UINT8)
Record Messagesο
Purpose: Individual data points during activity.
Base Fields (always included):
timestamp: FIT timestampenhanced_altitude: Altitude in mm (scaled)enhanced_speed: Speed in mm/s (scaled)heart_rate: BPM
Optional Fields:
position_lat: Latitude in semicirclesposition_long: Longitude in semicircles
Developer Fields (conditional):
Battery level (%)
Battery voltage (mV)
Lap Messagesο
Purpose: Summarize data for each lap segment.
Fields:
message_index: Always 0timestamp: Lap end time (FIT timestamp)start_time: Lap start time (FIT timestamp)total_elapsed_time: Time including pauses (ms)total_timer_time: Active time (ms)total_distance: Distance (cm)avg_speed: Average speed (mm/s)max_speed: Max speed (mm/s)avg_heart_rate: Average HR (bpm)max_heart_rate: Max HR (bpm)total_ascent: Ascent (m)total_descent: Descent (m)
Developer Fields (Hiking):
steps: Step count (uint32)floors: Floor count (uint32)
Session Messagesο
Purpose: Overall activity summary.
Fields: Similar to Lap, plus:
sport: Activity type (e.g.,FIT_SPORT_RUNNING)sub_sport: Sub-type (e.g.,FIT_SUB_SPORT_GENERIC)num_laps: Number of laps
Developer Fields: Same as Lap (steps, floors for Hiking)
Event Messagesο
Purpose: Mark activity state changes.
Fields:
timestamp: Event time (FIT timestamp)event:FIT_EVENT_TIMER(0)event_type:FIT_EVENT_TYPE_START(0) orFIT_EVENT_TYPE_STOP(1)
Activity Messagesο
Purpose: Top-level activity metadata.
Fields:
timestamp: Activity end time (FIT timestamp)local_timestamp: End time in local timezone (FIT timestamp)total_timer_time: Total active time (ms)num_sessions: Always 1
Message Types and Fieldsο
File ID Messageο
Purpose: Identifies the file type and creator.
Fields:
product_name: βUNA Watchβserial_number: 0time_created: Unix timestamp converted to FIT timemanufacturer:FIT_MANUFACTURER_DEVELOPMENTproduct: 0number: 0type:FIT_FILE_ACTIVITY
Developer Data ID Messageο
Purpose: Registers the developer and app for custom fields.
Fields:
developer_id: App-specific developer IDapplication_id: App-specific application IDapplication_version: App versionmanufacturer_id:FIT_MANUFACTURER_DEVELOPMENTdeveloper_data_index: 0
Field Description Messagesο
Purpose: Describe custom developer fields.
Fields (per field):
field_name: e.g., βbatteryLevelβ, βstepsβunits: e.g., β%β, βmVβdeveloper_data_index: 0field_definition_number: Unique ID for the fieldfit_base_type_id: Data type (e.g.,FIT_BASE_TYPE_UINT8)
Record Messagesο
Purpose: Individual data points during the activity (e.g., every second).
Base Fields (always present):
timestamp: FIT timestampenhanced_altitude: Altitude in mm (scaled)enhanced_speed: Speed in mm/s (scaled)heart_rate: BPM
Variants:
Basic Record: No GPS or battery
Record with GPS: Adds
position_lat,position_long(semicircles)Record with Battery: Adds developer fields for battery level and voltage
Combined: GPS + Battery
Lap Messagesο
Purpose: Summarize data for each lap.
Fields:
message_index: 0timestamp: End timestart_time: Start timetotal_elapsed_time: Time including pauses (ms)total_timer_time: Active time (ms)total_distance: Distance (cm)avg_speed: Average speed (mm/s)max_speed: Max speed (mm/s)avg_heart_rate: Average HRmax_heart_rate: Max HRtotal_ascent: Ascent (m)total_descent: Descent (m)
Developer Fields: Steps and floors (Hiking only)
Session Messagesο
Purpose: Overall activity summary.
Fields: Similar to Lap, but for the entire session.
Additional:
sport,sub_sport,num_lapsDeveloper Fields: Steps and floors (Hiking only)
Event Messagesο
Purpose: Mark start/stop/pause/resume events.
Fields:
timestamp: Event timeevent:FIT_EVENT_TIMERevent_type: START or STOP
Activity Messagesο
Purpose: Top-level activity info.
Fields:
timestamp: Activity end timelocal_timestamp: Local timetotal_timer_time: Total active time (ms)num_sessions: 1
Activity-Specific Variationsο
Running (Examples/Apps/Running/)ο
Sport Type: FIT_SPORT_RUNNING
Message Numbers (enum class MsgNumber):
FILE = 1, DEVELOP, RECORD, RECORD_G, RECORD_B, RECORD_GB, LAP, SESSION, ACTIVITY, EVENT, BATTERY
Record Variants:
Basic: timestamp, altitude, speed, HR
With GPS: + lat/long
With Battery: + battery level, voltage
Combined: GPS + Battery
Session Fields: Full set (distance, speed, elevation, HR)
Developer Fields:
Battery level (field 2, uint8, %)
Battery voltage (field 3, uint16, mV)
Code Example:
session_mesg.sport = FIT_SPORT_RUNNING;
session_mesg.sub_sport = FIT_SUB_SPORT_GENERIC;
Cycling (Examples/Apps/Cycling/)ο
Sport Type: FIT_SPORT_CYCLING
Identical to Running except sport type. All record variants, battery fields, and session fields are the same.
Code Difference:
session_mesg.sport = FIT_SPORT_CYCLING;
Hiking (Examples/Apps/Hiking/)ο
Sport Type: FIT_SPORT_HIKING
Additional Message Numbers:
STEPS, FLOORS (for developer fields)
Extra FitHelper Initializations:
mFHStepsField(static_cast<uint8_t>(MsgNumber::STEPS), 0, { &mFHLap, &mFHSession })
mFHFloorField(static_cast<uint8_t>(MsgNumber::FLOORS), 1, { &mFHLap, &mFHSession })
Field Descriptions:
Steps: uint32, no units
Floors: uint32, no units
Lap/Session Enhancement:
FIT_UINT32 steps = lap.steps;
mFHLap.writeFieldMessage(0, &steps, fp);
FIT_UINT32 floors = lap.floors;
mFHLap.writeFieldMessage(1, &floors, fp);
Session Enhancement: Same field writes for track.steps and track.floors.
HRMonitor (Examples/Apps/HRMonitor/)ο
Sport Type: FIT_SPORT_GENERIC
Simplified Structure:
Uses constants: skFileMsgNum = 1, skDevelopMsgNum = 2, etc.
Minimal record fields: only timestamp and heart_rate
No GPS, speed, altitude, distance fields
Custom developer field: hr_trust_level
Record Message:
mFHRecord.init({FIT_RECORD_FIELD_NUM_TIMESTAMP,
FIT_RECORD_FIELD_NUM_HEART_RATE});
Trust Level Field:
mFHTrustLevelField(skHrTrustLevelMsgNum, { &mFHRecord })
// Field description
strncpy(trustLevel.field_name, "hr_trust_level", FIT_FIELD_DESCRIPTION_MESG_FIELD_NAME_COUNT);
strncpy(trustLevel.units, "percents", FIT_FIELD_DESCRIPTION_MESG_UNITS_COUNT);
trustLevel.fit_base_type_id = FIT_BASE_TYPE_UINT8;
// Writing
FIT_UINT8 trustLevel = record.trustLevel;
mFHRecord.writeFieldMessage(0, &trustLevel, fp);
Session Fields: Reduced set, no distance/speed/elevation.
Developer Fields Implementationο
Overviewο
Developer fields allow adding custom data to standard FIT messages while maintaining compatibility. The process involves:
Developer Data ID: Registers the developer/app
Field Descriptions: Define each custom field
Field Attachments: Associate fields with message types
Data Writing: Include field values in data messages
Battery Fields (Running, Cycling, Hiking)ο
Field Definitions:
// Field 2: Battery Level
mFHBatteryLevelField.init({ FIT_FIELD_DESCRIPTION_FIELD_NUM_FIELD_NAME,
FIT_FIELD_DESCRIPTION_FIELD_NUM_UNITS,
FIT_FIELD_DESCRIPTION_FIELD_NUM_DEVELOPER_DATA_INDEX,
FIT_FIELD_DESCRIPTION_FIELD_NUM_FIELD_DEFINITION_NUMBER,
FIT_FIELD_DESCRIPTION_FIELD_NUM_FIT_BASE_TYPE_ID });
FIT_FIELD_DESCRIPTION_MESG battLevel{};
strncpy(battLevel.field_name, "batteryLevel", FIT_FIELD_DESCRIPTION_MESG_FIELD_NAME_COUNT - 1);
strncpy(battLevel.units, "%", FIT_FIELD_DESCRIPTION_MESG_UNITS_COUNT - 1);
battLevel.developer_data_index = 0;
battLevel.field_definition_number = mFHBatteryLevelField.getFieldID(); // 2
battLevel.fit_base_type_id = FIT_BASE_TYPE_UINT8;
mFHBatteryLevelField.writeMessage(&battLevel, fp);
// Field 3: Battery Voltage
// Similar setup with "battVoltage", "mV", FIT_BASE_TYPE_UINT16
Attachment to Records:
mFHBatteryLevelField(static_cast<uint8_t>(MsgNumber::BATTERY), 2, { &mFHRecordB, &mFHRecordGB })
mFHBatteryVoltageField(static_cast<uint8_t>(MsgNumber::BATTERY), 3, { &mFHRecordB, &mFHRecordGB })
Writing Data:
if (record.has(RecordData::Field::BATTERY)) {
const FIT_UINT8 soc = record.batteryLevel;
const FIT_UINT16 voltage = record.batteryVoltage;
if (record.has(RecordData::Field::COORDS)) {
mFHRecordGB.writeMessage(&msg, mFile.get());
mFHRecordGB.writeFieldMessage(0, &soc, mFile.get()); // Battery level
mFHRecordGB.writeFieldMessage(1, &voltage, mFile.get()); // Battery voltage
} else {
mFHRecordB.writeMessage(&msg, mFile.get());
mFHRecordB.writeFieldMessage(0, &soc, mFile.get());
mFHRecordB.writeFieldMessage(1, &voltage, mFile.get());
}
}
Steps and Floors (Hiking)ο
Field Definitions:
// Field 0: Steps
mFHStepsField.init({/* field description fields */});
FIT_FIELD_DESCRIPTION_MESG steps{};
strncpy(steps.field_name, "steps", FIT_FIELD_DESCRIPTION_MESG_FIELD_NAME_COUNT - 1);
steps.fit_base_type_id = FIT_BASE_TYPE_UINT32;
mFHStepsField.writeMessage(&steps, fp);
// Field 1: Floors (similar)
Attachment:
mFHStepsField(static_cast<uint8_t>(MsgNumber::STEPS), 0, { &mFHLap, &mFHSession })
mFHFloorField(static_cast<uint8_t>(MsgNumber::FLOORS), 1, { &mFHLap, &mFHSession })
Writing to Lap/Session:
// In addLap()
FIT_UINT32 steps = lap.steps;
mFHLap.writeFieldMessage(0, &steps, fp);
FIT_UINT32 floors = lap.floors;
mFHLap.writeFieldMessage(1, &floors, fp);
// Similar in stop() for session
Trust Level (HRMonitor)ο
Simplified Setup:
mFHTrustLevelField(skHrTrustLevelMsgNum, { &mFHRecord })
// Field description
FIT_FIELD_DESCRIPTION_MESG trustLevel{};
strncpy(trustLevel.field_name, "hr_trust_level", FIT_FIELD_DESCRIPTION_MESG_FIELD_NAME_COUNT);
strncpy(trustLevel.units, "percents", FIT_FIELD_DESCRIPTION_MESG_UNITS_COUNT);
trustLevel.fit_base_type_id = FIT_BASE_TYPE_UINT8;
// Writing
FIT_UINT8 trustLevel = record.trustLevel;
mFHRecord.writeFieldMessage(0, &trustLevel, fp);
Best Practices for Developer Fieldsο
Use descriptive field names and units
Choose appropriate data types to minimize space
Document field meanings for parsers
Test with FIT validation tools
Consider backward compatibility when changing fields
Visual Representations and Diagramsο
Complete FIT File Structureο
FIT File Binary Layout:
βββββββββββββββββββ
β File Header β 14 bytes
β β - header_size: 14
β β - protocol_version: 32 (2.0)
β β - profile_version: XXX
β β - data_size: XXX (updated later)
β β - data_type: ".FIT"
β β - crc: XXX (calculated later)
βββββββββββββββββββ
βββββββββββββββββββ
β Data Section β Variable size
β β
β βββββββββββββββ β
β β File ID Msg β β
β βββββββββββββββ β
β βββββββββββββββ β
β β Dev Data ID β β
β βββββββββββββββ β
β βββββββββββββββ β
β β Field Desc β β (battery, steps, etc.)
β β Messages β β
β βββββββββββββββ β
β βββββββββββββββ β
β β Definitions β β (Record, Lap, Session, etc.)
β βββββββββββββββ β
β βββββββββββββββ β
β β Event START β β
β βββββββββββββββ β
β βββββββββββββββ β
β β Record 1 β β
β βββββββββββββββ€ β
β β Record 2 β β
β βββββββββββββββ€ β
β β ... β β
β βββββββββββββββ β
β βββββββββββββββ β
β β Lap 1 β β
β βββββββββββββββ€ β
β β Lap 2 β β
β βββββββββββββββ€ β
β β ... β β
β βββββββββββββββ β
β βββββββββββββββ β
β β Event STOP β β
β βββββββββββββββ β
β βββββββββββββββ β
β β Session β β
β βββββββββββββββ β
β βββββββββββββββ β
β β Activity β β
β βββββββββββββββ β
βββββββββββββββββββ
βββββββββββββββββββ
β CRC β 2 bytes
βββββββββββββββββββ
Message Definition Structureο
Each definition message contains:
Definition Message:
βββ Header Byte (bit field)
β βββ Local Message Number (0-15)
β βββ Message Type (0 = Definition)
β βββ Reserved
βββ Reserved Byte
βββ Architecture (0 = little-endian)
βββ Global Message Number (e.g., 20 for Record)
βββ Number of Fields
βββ Field Definitions (repeated)
βββ Field Number
βββ Size (bytes)
βββ Base Type
βββ Reserved
Record Message Variants Treeο
Record Messages
βββ Basic Record (mFHRecord)
β βββ timestamp (uint32)
β βββ enhanced_altitude (uint32, scaled)
β βββ enhanced_speed (uint32, scaled)
β βββ heart_rate (uint8)
βββ Record + GPS (mFHRecordG)
β βββ (Basic fields)
β βββ position_lat (sint32, semicircles)
β βββ position_long (sint32, semicircles)
βββ Record + Battery (mFHRecordB)
β βββ (Basic fields)
β βββ batteryLevel (uint8, dev field 2)
β βββ battVoltage (uint16, dev field 3)
βββ Record + GPS + Battery (mFHRecordGB)
βββ (Basic + GPS fields)
βββ batteryLevel (uint8, dev field 2)
βββ battVoltage (uint16, dev field 3)
ActivityWriter Method Flowο
ActivityWriter Lifecycle:
Constructor
βββ Initialize FitHelper objects
βββ Set message numbers and definitions
βββ Configure field subsets
start(AppInfo)
βββ createAndOpenFile()
βββ WriteFileHeader() [placeholder]
βββ Write File ID message
βββ Write Developer Data ID message
βββ Write Field Description messages
βββ Write Definition messages for all types
βββ Write Event (START)
addRecord(RecordData)
βββ prepareRecordMsg() - scale/convert data
βββ Select appropriate FitHelper variant
βββ Write record message
βββ Write developer fields (battery if present)
addLap(LapData)
βββ Populate FIT_LAP_MESG with scaled data
βββ Write lap message
βββ Write developer fields (steps/floors for Hiking)
pause/resume(time_t)
βββ Write Event (STOP/START)
stop(TrackData)
βββ Write Event (STOP)
βββ Write Session message + dev fields
βββ Write Activity message
βββ Update file header with data size
βββ WriteCRC()
βββ saveFile()
βββ saveSummary() - create JSON file
discard()
βββ deleteFile()
Code Usage Examples and Walkthroughsο
Constructor Deep Diveο
ActivityWriter::ActivityWriter(const SDK::Kernel& kernel, const char* pathToDir)
: mKernel(kernel), mPath(pathToDir)
// Initialize all FitHelper objects with message numbers and definitions
, mFHFileID(static_cast<uint8_t>(MsgNumber::FILE), (FIT_MESG_DEF*)fit_mesg_defs[FIT_MESG_FILE_ID])
, mFHDeveloper(static_cast<uint8_t>(MsgNumber::DEVELOP), (FIT_MESG_DEF*)fit_mesg_defs[FIT_MESG_DEVELOPER_DATA_ID])
, mFHLap(static_cast<uint8_t>(MsgNumber::LAP), (FIT_MESG_DEF*)fit_mesg_defs[FIT_MESG_LAP])
, mFHSession(static_cast<uint8_t>(MsgNumber::SESSION), (FIT_MESG_DEF*)fit_mesg_defs[FIT_MESG_SESSION])
, mFHEvent(static_cast<uint8_t>(MsgNumber::EVENT), (FIT_MESG_DEF*)fit_mesg_defs[FIT_MESG_EVENT])
, mFHActivity(static_cast<uint8_t>(MsgNumber::ACTIVITY), (FIT_MESG_DEF*)fit_mesg_defs[FIT_MESG_ACTIVITY])
// Record variants for different field combinations
, mFHRecord(static_cast<uint8_t>(MsgNumber::RECORD), (FIT_MESG_DEF*)fit_mesg_defs[FIT_MESG_RECORD])
, mFHRecordG(static_cast<uint8_t>(MsgNumber::RECORD_G), (FIT_MESG_DEF*)fit_mesg_defs[FIT_MESG_RECORD])
, mFHRecordB(static_cast<uint8_t>(MsgNumber::RECORD_B), (FIT_MESG_DEF*)fit_mesg_defs[FIT_MESG_RECORD])
, mFHRecordGB(static_cast<uint8_t>(MsgNumber::RECORD_GB), (FIT_MESG_DEF*)fit_mesg_defs[FIT_MESG_RECORD])
// Developer fields for battery (attached to battery-enabled records)
, mFHBatteryLevelField(static_cast<uint8_t>(MsgNumber::BATTERY), 2, { &mFHRecordB, &mFHRecordGB })
, mFHBatteryVoltageField(static_cast<uint8_t>(MsgNumber::BATTERY), 3, { &mFHRecordB, &mFHRecordGB })
{
assert(pathToDir != nullptr);
// Initialize standard messages
mFHFileID.init();
mFHDeveloper.init();
// Initialize Lap with specific fields
mFHLap.init({ FIT_LAP_FIELD_NUM_TIMESTAMP,
FIT_LAP_FIELD_NUM_START_TIME,
FIT_LAP_FIELD_NUM_TOTAL_ELAPSED_TIME,
FIT_LAP_FIELD_NUM_TOTAL_TIMER_TIME,
FIT_LAP_FIELD_NUM_TOTAL_DISTANCE,
FIT_LAP_FIELD_NUM_MESSAGE_INDEX,
FIT_LAP_FIELD_NUM_AVG_SPEED,
FIT_LAP_FIELD_NUM_MAX_SPEED,
FIT_LAP_FIELD_NUM_TOTAL_ASCENT,
FIT_LAP_FIELD_NUM_TOTAL_DESCENT,
FIT_LAP_FIELD_NUM_AVG_HEART_RATE,
FIT_LAP_FIELD_NUM_MAX_HEART_RATE });
// Similar for Session, Event, Activity...
// Initialize Record variants with different field sets
mFHRecord.init({ FIT_RECORD_FIELD_NUM_TIMESTAMP,
FIT_RECORD_FIELD_NUM_ENHANCED_ALTITUDE,
FIT_RECORD_FIELD_NUM_ENHANCED_SPEED,
FIT_RECORD_FIELD_NUM_HEART_RATE });
mFHRecordG.init({ FIT_RECORD_FIELD_NUM_TIMESTAMP,
FIT_RECORD_FIELD_NUM_POSITION_LAT,
FIT_RECORD_FIELD_NUM_POSITION_LONG,
FIT_RECORD_FIELD_NUM_ENHANCED_ALTITUDE,
FIT_RECORD_FIELD_NUM_ENHANCED_SPEED,
FIT_RECORD_FIELD_NUM_HEART_RATE });
// Battery variants (same fields as basic/GPS but with dev fields attached)
mFHRecordB.init({ FIT_RECORD_FIELD_NUM_TIMESTAMP,
FIT_RECORD_FIELD_NUM_ENHANCED_ALTITUDE,
FIT_RECORD_FIELD_NUM_ENHANCED_SPEED,
FIT_RECORD_FIELD_NUM_HEART_RATE });
mFHRecordGB.init({ FIT_RECORD_FIELD_NUM_TIMESTAMP,
FIT_RECORD_FIELD_NUM_POSITION_LAT,
FIT_RECORD_FIELD_NUM_POSITION_LONG,
FIT_RECORD_FIELD_NUM_ENHANCED_ALTITUDE,
FIT_RECORD_FIELD_NUM_ENHANCED_SPEED,
FIT_RECORD_FIELD_NUM_HEART_RATE });
// Initialize developer field descriptions
mFHBatteryLevelField.init({ FIT_FIELD_DESCRIPTION_FIELD_NUM_FIELD_NAME,
FIT_FIELD_DESCRIPTION_FIELD_NUM_UNITS,
FIT_FIELD_DESCRIPTION_FIELD_NUM_DEVELOPER_DATA_INDEX,
FIT_FIELD_DESCRIPTION_FIELD_NUM_FIELD_DEFINITION_NUMBER,
FIT_FIELD_DESCRIPTION_FIELD_NUM_FIT_BASE_TYPE_ID });
mFHBatteryVoltageField.init({/* same fields */});
}
start() Method Walkthroughο
void ActivityWriter::start(const AppInfo& info)
{
// Reset counters
mLapCounter = 0;
mDataCRC = 0;
// Create and open the FIT file
createAndOpenFile(info.timestamp);
if (!mFile) return;
SDK::Interface::IFile* fp = mFile.get();
// Write placeholder header (will be updated in stop())
WriteFileHeader(fp);
// 1. File ID Message - identifies the file
{
mFHFileID.writeDef(fp); // Write definition first
FIT_FILE_ID_MESG file_id_mesg{};
strncpy(file_id_mesg.product_name, "UNA Watch", FIT_FILE_ID_MESG_PRODUCT_NAME_COUNT);
file_id_mesg.serial_number = 0;
file_id_mesg.time_created = unixToFitTimestamp(info.timestamp);
file_id_mesg.manufacturer = FIT_MANUFACTURER_DEVELOPMENT;
file_id_mesg.product = 0;
file_id_mesg.number = 0;
file_id_mesg.type = FIT_FILE_ACTIVITY;
mFHFileID.writeMessage(&file_id_mesg, fp);
}
// 2. Developer Data ID - registers custom fields
{
mFHDeveloper.writeDef(fp);
FIT_DEVELOPER_DATA_ID_MESG developer{};
strncpy(reinterpret_cast<char*>(developer.developer_id), info.devID.c_str(),
FIT_DEVELOPER_DATA_ID_MESG_DEVELOPER_ID_COUNT);
strncpy(reinterpret_cast<char*>(developer.application_id), info.appID.c_str(),
FIT_DEVELOPER_DATA_ID_MESG_APPLICATION_ID_COUNT);
developer.application_version = info.appVersion;
developer.manufacturer_id = FIT_MANUFACTURER_DEVELOPMENT;
developer.developer_data_index = 0;
mFHDeveloper.writeMessage(&developer, fp);
}
// 3. Field Descriptions for developer fields
{
// Battery Level
mFHBatteryLevelField.writeDef(fp);
FIT_FIELD_DESCRIPTION_MESG battLevel{};
strncpy(battLevel.field_name, "batteryLevel", FIT_FIELD_DESCRIPTION_MESG_FIELD_NAME_COUNT - 1);
strncpy(battLevel.units, "%", FIT_FIELD_DESCRIPTION_MESG_UNITS_COUNT - 1);
battLevel.developer_data_index = 0;
battLevel.field_definition_number = mFHBatteryLevelField.getFieldID();
battLevel.fit_base_type_id = FIT_BASE_TYPE_UINT8;
mFHBatteryLevelField.writeMessage(&battLevel, fp);
// Battery Voltage (similar)
// ...
}
// 4. Write definitions for all message types that will be used
mFHEvent.writeDef(fp);
mFHActivity.writeDef(fp);
mFHRecord.writeDef(fp);
mFHRecordG.writeDef(fp);
mFHRecordB.writeDef(fp);
mFHRecordGB.writeDef(fp);
mFHLap.writeDef(fp);
mFHSession.writeDef(fp);
// 5. Start the activity with an Event message
AddMessageEvent(info.timestamp, FIT_EVENT_TYPE_START);
}
prepareRecordMsg() - Data Conversionο
FIT_RECORD_MESG ActivityWriter::prepareRecordMsg(const RecordData& record)
{
FIT_RECORD_MESG msg;
// Initialize with base definition
Fit_InitMesg(fit_mesg_defs[FIT_MESG_RECORD], &msg);
// Convert Unix timestamp to FIT timestamp
msg.timestamp = unixToFitTimestamp(record.timestamp);
// GPS coordinates (if available)
if (record.has(RecordData::Field::COORDS)) {
msg.position_lat = ConvertDegreesToSemicircles(record.latitude);
msg.position_long = ConvertDegreesToSemicircles(record.longitude);
}
// Speed scaling: float m/s -> uint32 mm/s
if (record.has(RecordData::Field::SPEED)) {
msg.enhanced_speed = static_cast<FIT_UINT32>(record.speed * 1000);
}
// Altitude scaling: float m -> uint32 mm with offset
if (record.has(RecordData::Field::ALTITUDE)) {
msg.enhanced_altitude = static_cast<FIT_UINT32>((record.altitude + 500) * 5);
}
// Heart rate (direct mapping)
if (record.has(RecordData::Field::HEART_RATE)) {
msg.heart_rate = static_cast<FIT_UINT8>(record.heartRate);
}
return msg;
}
addRecord() - Variant Selection Logicο
void ActivityWriter::addRecord(const RecordData& record)
{
if (!mFile) return;
const FIT_RECORD_MESG msg = prepareRecordMsg(record);
// Select FitHelper variant based on available data
if (record.has(RecordData::Field::BATTERY)) {
const FIT_UINT8 soc = record.batteryLevel;
const FIT_UINT16 voltage = record.batteryVoltage;
if (record.has(RecordData::Field::COORDS)) {
// GPS + Battery variant
mFHRecordGB.writeMessage(&msg, mFile.get());
mFHRecordGB.writeFieldMessage(0, &soc, mFile.get()); // Battery level
mFHRecordGB.writeFieldMessage(1, &voltage, mFile.get()); // Battery voltage
} else {
// Battery only variant
mFHRecordB.writeMessage(&msg, mFile.get());
mFHRecordB.writeFieldMessage(0, &soc, mFile.get());
mFHRecordB.writeFieldMessage(1, &voltage, mFile.get());
}
} else {
// No battery data
if (record.has(RecordData::Field::COORDS)) {
// GPS variant
mFHRecordG.writeMessage(&msg, mFile.get());
} else {
// Basic variant
mFHRecord.writeMessage(&msg, mFile.get());
}
}
}
stop() Method - Finalizationο
void ActivityWriter::stop(const TrackData& track)
{
if (!mFile) return;
SDK::Interface::IFile* fp = mFile.get();
// Write final STOP event
AddMessageEvent(std::time(nullptr), FIT_EVENT_TYPE_STOP);
// Session message
{
FIT_SESSION_MESG session_mesg{};
Fit_InitMesg(fit_mesg_defs[FIT_MESG_SESSION], &session_mesg);
session_mesg.message_index = 0;
session_mesg.sport = FIT_SPORT_RUNNING; // Activity-specific
session_mesg.sub_sport = FIT_SUB_SPORT_GENERIC;
session_mesg.timestamp = unixToFitTimestamp(track.timestamp);
session_mesg.start_time = unixToFitTimestamp(track.timeStart);
// Scaled timing data
session_mesg.total_elapsed_time = static_cast<FIT_UINT32>(track.elapsed * 1000);
session_mesg.total_timer_time = static_cast<FIT_UINT32>(track.duration * 1000);
// Scaled distance, speed, etc.
session_mesg.total_distance = static_cast<FIT_UINT32>(track.distance * 100);
session_mesg.avg_speed = static_cast<FIT_UINT16>(track.speedAvg * 1000);
session_mesg.max_speed = static_cast<FIT_UINT16>(track.speedMax * 1000);
session_mesg.avg_heart_rate = static_cast<FIT_UINT8>(track.hrAvg);
session_mesg.max_heart_rate = static_cast<FIT_UINT8>(track.hrMax);
session_mesg.total_ascent = static_cast<FIT_UINT16>(track.ascent);
session_mesg.total_descent = static_cast<FIT_UINT16>(track.descent);
session_mesg.num_laps = mLapCounter;
mFHSession.writeMessage(&session_mesg, fp);
// Add developer fields for Hiking
// FIT_UINT32 steps = track.steps;
// mFHSession.writeFieldMessage(0, &steps, fp);
// etc.
}
// Activity message
{
FIT_ACTIVITY_MESG activity_mesg{};
activity_mesg.timestamp = unixToFitTimestamp(track.timestamp);
activity_mesg.local_timestamp = unixToFitTimestamp(epochToLocal(track.timestamp));
activity_mesg.total_timer_time = static_cast<FIT_UINT32>(track.duration * 1000);
activity_mesg.num_sessions = 1;
mFHActivity.writeMessage(&activity_mesg, fp);
}
// Update header with actual data size
fp->seek(0);
WriteFileHeader(fp);
// Calculate and append CRC
WriteCRC(fp);
// Close file
saveFile();
// Create summary JSON
saveSummary(track);
}
Advanced Topics and Best Practicesο
FIT Timestamp Handlingο
FIT uses a different epoch than Unix. The FIT epoch starts on December 31, 1989, at 00:00:00 UTC.
Conversion Function:
FIT_DATE_TIME ActivityWriter::unixToFitTimestamp(std::time_t unixTimestamp)
{
const std::time_t FIT_EPOCH_OFFSET = 631065600; // 1989-12-31 00:00:00 UTC
return static_cast<FIT_DATE_TIME>(unixTimestamp - FIT_EPOCH_OFFSET);
}
Key Points:
FIT timestamps are uint32, representing seconds since FIT epoch
Unix epoch is 1970-01-01, FIT epoch is 1989-12-31
Difference is exactly 631065600 seconds
Always use this conversion for timestamp fields
Data Scaling and Precisionο
FIT uses integer types for efficiency. Floating-point values are scaled to integers.
Scaling Formulas:
// Speed: float m/s -> uint32 mm/s (1000 * m/s + 0)
uint32_t scaled_speed = static_cast<uint32_t>(speed_mps * 1000);
// Distance: float m -> uint32 cm (100 * m + 0)
uint32_t scaled_distance = static_cast<uint32_t>(distance_m * 100);
// Altitude: float m -> uint32 mm with offset (5 * m + 500)
uint32_t scaled_altitude = static_cast<uint32_t>((altitude_m + 500) * 5);
// Time: float s -> uint32 ms (1000 * s + 0)
uint32_t scaled_time = static_cast<uint32_t>(time_s * 1000);
Reverse Scaling for Reading:
float speed_mps = scaled_speed / 1000.0f;
float distance_m = scaled_distance / 100.0f;
float altitude_m = (scaled_altitude / 5.0f) - 500.0f;
float time_s = scaled_time / 1000.0f;
GPS Coordinate Conversionο
GPS coordinates are stored as semicircles (1/2^31 degrees) for precision.
Conversion Function:
FIT_SINT32 ActivityWriter::ConvertDegreesToSemicircles(float degrees)
{
return static_cast<FIT_SINT32>(degrees * (2147483648.0 / 180.0));
}
Range and Precision:
Valid range: -180 to +180 degrees for longitude, -90 to +90 for latitude
1 semicircle = 180 / 2^31 degrees β 8.38e-8 degrees
Precision: ~1 cm at equator
CRC Calculation and Validationο
FIT files include a 16-bit CRC for data integrity.
CRC Implementation:
void ActivityWriter::WriteCRC(SDK::Interface::IFile* fp)
{
fp->close();
fp->open(false); // Read mode
FIT_UINT8 buffer[512];
size_t size = fp->size();
size_t pos = 0;
uint16_t crc = 0;
while (pos < size) {
size_t toRead = std::min(size - pos, sizeof(buffer));
size_t br;
fp->read(reinterpret_cast<char*>(buffer), toRead, br);
crc = FitCRC_Update16(crc, buffer, static_cast<FIT_UINT32>(br));
pos += br;
}
fp->close();
fp->open(true, false); // Write mode, append
fp->seek(fp->size());
size_t bw;
fp->write(reinterpret_cast<const char*>(&crc), sizeof(FIT_UINT16), bw);
fp->flush();
}
CRC Properties:
Calculated over header + data (excluding the CRC field itself)
Uses CRC-16-CCITT polynomial
Ensures data integrity during transfer/storage
File Header Managementο
The file header contains metadata and must be updated with final data size.
Header Structure:
void ActivityWriter::WriteFileHeader(SDK::Interface::IFile* fp)
{
FIT_FILE_HDR file_header{};
file_header.header_size = FIT_FILE_HDR_SIZE; // 14
file_header.profile_version = FIT_PROFILE_VERSION;
file_header.protocol_version = FIT_PROTOCOL_VERSION_20;
memcpy(file_header.data_type, ".FIT", 4);
// Calculate data size (file size - header size)
fp->flush();
size_t fileSize = fp->size();
if (fileSize > FIT_FILE_HDR_SIZE) {
file_header.data_size = static_cast<FIT_UINT32>(fileSize - FIT_FILE_HDR_SIZE);
} else {
file_header.data_size = 0;
}
// Calculate header CRC
file_header.crc = FitCRC_Calc16(&file_header, FIT_STRUCT_OFFSET(crc, FIT_FILE_HDR));
// Write header at file start
fp->seek(0);
size_t bw;
fp->write(reinterpret_cast<const char*>(&file_header), FIT_FILE_HDR_SIZE, bw);
fp->flush();
// Seek back to end for data writing
if (fileSize > 0) {
fp->seek(fileSize);
}
}
Memory Management and Performanceο
FitHelper Memory Usage:
Each FitHelper maintains optimized message definitions
Reduced field sets minimize memory and processing
Developer fields add minimal overhead
File I/O Optimization:
Messages written sequentially
Buffered writes for efficiency
File flushed at appropriate intervals
Best Practices:
Initialize FitHelpers once in constructor
Reuse message structures where possible
Handle file errors gracefully
Validate data before writing
Troubleshooting Common Issuesο
File Creation Failuresο
Symptom: createAndOpenFile() returns false
Possible Causes:
Invalid path or permissions
Insufficient storage space
Directory creation failure
Debug Steps:
// Check path validity
LOG_DEBUG("Path: %s", mPath);
// Verify directory creation
int len = snprintf(buff, sizeof(buff), "%s/%04u%02u/", mPath, year, month);
if (len <= 0 || !mKernel.fs.mkdir(buff)) {
LOG_ERROR("Failed to create dir [%s]", buff);
return false;
}
// Check file opening
mFile = mKernel.fs.file(buff);
if (!mFile || !mFile->open(true, true)) {
LOG_ERROR("Failed to create file [%s]", buff);
return false;
}
Invalid FIT Filesο
Symptom: FIT parsers reject the file
Common Issues:
Incorrect message sequencing
Missing definitions
Invalid field values
Wrong CRC
Validation Tools:
Use Garmin FIT SDK validator
Check with third-party FIT parsers
Verify message definitions match data
Data Scaling Errorsο
Symptom: Incorrect values in parsed files
Check Scaling:
// Verify scaling constants
assert(FIT_EPOCH_OFFSET == 631065600);
// Check conversions
float original_speed = 5.5f; // m/s
uint32_t scaled = original_speed * 1000; // 5500
float restored = scaled / 1000.0f; // 5.5
assert(abs(original_speed - restored) < 0.001f);
Developer Field Problemsο
Symptom: Custom fields not appearing in parsers
Verify Setup:
Developer Data ID written before field descriptions
Field definition numbers are unique
Base types match data types
Fields attached to correct message types
Memory Issuesο
Symptom: Crashes or undefined behavior
Check Bounds:
// String copies with bounds checking
strncpy(file_id_mesg.product_name, "UNA Watch", FIT_FILE_ID_MESG_PRODUCT_NAME_COUNT);
// Array size validation
static_assert(FIT_FILE_ID_MESG_PRODUCT_NAME_COUNT >= sizeof("UNA Watch"));
Extending ActivityWriter for New Activitiesο
Adding a New Sport Typeο
Define Sport Constants:
// In your app's ActivityWriter.hpp enum class MsgNumber { // ... existing NEW_SPORT = 10 };
Modify Session Message:
session_mesg.sport = FIT_SPORT_YOUR_SPORT; session_mesg.sub_sport = FIT_SUB_SPORT_GENERIC;
Add Sport-Specific Fields:
// If needed, add developer fields mFHSportSpecificField(static_cast<uint8_t>(MsgNumber::NEW_SPORT), 4, { &mFHSession });
Adding New Developer Fieldsο
Define Field in Constructor:
, mFHNewField(static_cast<uint8_t>(MsgNumber::NEW_FIELD), field_id, { &mFHRecord })
Initialize Field Description:
mFHNewField.init({ FIT_FIELD_DESCRIPTION_FIELD_NUM_FIELD_NAME, FIT_FIELD_DESCRIPTION_FIELD_NUM_UNITS, /* ... */ });
Write Field Description in start():
FIT_FIELD_DESCRIPTION_MESG newFieldDesc{}; strncpy(newFieldDesc.field_name, "newField", FIT_FIELD_DESCRIPTION_MESG_FIELD_NAME_COUNT); // Set units, type, etc. mFHNewField.writeMessage(&newFieldDesc, fp);
Write Data in Appropriate Methods:
// In addRecord() or wherever applicable FIT_UINT16 newValue = calculateNewValue(); mFHNewField.writeFieldMessage(0, &newValue, fp);
Creating New Record Variantsο
Add FitHelper for New Variant:
, mFHRecordNew(static_cast<uint8_t>(MsgNumber::RECORD_NEW), (FIT_MESG_DEF*)fit_mesg_defs[FIT_MESG_RECORD])
Initialize with Custom Fields:
mFHRecordNew.init({ FIT_RECORD_FIELD_NUM_TIMESTAMP, FIT_RECORD_FIELD_NUM_HEART_RATE, // Add your custom fields });
Write Definition in start():
mFHRecordNew.writeDef(fp);
Use in addRecord():
if (hasNewCondition()) { mFHRecordNew.writeMessage(&msg, fp); // Write developer fields }
Best Practices for Extensionsο
Follow FIT profile guidelines
Use appropriate data types and scaling
Document custom fields clearly
Test with multiple FIT parsers
Maintain backward compatibility
Validate field ranges and units
FIT File Parsing and Validationο
Using Garmin FIT SDKο
The official FIT SDK provides parsing capabilities.
Basic Parsing Example:
#include "fit_decode.hpp"
#include "fit_mesg_listener.hpp"
class MyListener : public fit::MesgListener {
public:
void OnMesg(fit::Mesg& mesg) override {
if (mesg.GetNum() == FIT_MESG_NUM_RECORD) {
// Handle record message
FIT_UINT32 timestamp = mesg.GetFieldUINT32Value(253); // timestamp field
// Process other fields...
}
}
};
fit::Decode decode;
MyListener listener;
decode.AddListener(listener);
std::ifstream file("activity.fit", std::ios::binary);
decode.Read(file);
Third-Party Librariesο
Python:
fitparselibraryJavaScript:
fit-file-parserJava:
FitSDK
Validation Toolsο
FIT SDK Validator: Command-line tool from Garmin
Online Validators: Various web-based FIT file checkers
Garmin Connect: Upload and check for errors
Common Parsing Errorsο
Invalid Header: Check protocol/profile versions
Missing Definitions: Ensure all messages have definitions
Field Mismatches: Verify field numbers and types
CRC Errors: File corrupted during transfer
Debugging FIT Filesο
Use FIT SDK Dump Tool:
fitdump activity.fitCheck Message Sequence:
File ID first
Developer Data ID before field descriptions
Definitions before data messages
Validate Field Values:
Check ranges (e.g., HR 0-255)
Verify scaling
Confirm timestamps are reasonable
References and Resourcesο
Official Documentationο
FIT SDK: https://developer.garmin.com/fit/
FIT Profile: https://developer.garmin.com/fit/overview/
FIT Protocol: https://developer.garmin.com/fit/protocol/
Key FIT Documentsο
FIT Protocol Description
FIT Profile XLS (spreadsheet of all message types and fields)
FIT SDK Examples
Community Resourcesο
FIT File Tools: https://www.fitfiletools.com/
FIT Developer Forum: Garmin developer forums
Open Source Projects: Various FIT parsing libraries on GitHub
UNA SDK Specificο
SDK Documentation:
Docs/directoryExample Apps:
Examples/Apps/FitHelper Implementation:
Libs/Header/SDK/FitHelper/
Books and Tutorialsο
βFIT Protocol Guideβ (Garmin documentation)
Online tutorials for custom FIT file creation
Version Historyο
FIT Protocol 2.0: Current version used in UNA SDK
Profile Versions: Updated periodically with new fields/messages
This comprehensive guide should provide new developers with everything needed to understand, use, and extend the FIT file generation in the UNA SDK. Remember to always validate your FIT files with official tools before deployment.