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)

  1. Introduction to FIT - Understand what FIT is and why it’s used

  2. FIT File Format Basics - Learn binary format, data types, and scaling

  3. ActivityWriter Class Overview - Get familiar with the main class

  4. Data Structures in ActivityWriter - Understand the data you work with

Phase 2: Implementation (Core Development)

  1. FitHelper Component Deep Dive - Learn the helper class that does the work

  2. Step-by-Step FIT File Creation - Follow the file creation process

  3. Code Usage Examples and Walkthroughs - Study real code examples

Phase 3: Customization (When You Need to Modify)

  1. Activity-Specific Variations - See how different activities are implemented

  2. Developer Fields Implementation - Add custom data fields

  3. Extending ActivityWriter for New Activities - Create new activity types

Phase 4: Advanced Topics (As Needed)

  1. Advanced Topics and Best Practices - Performance and edge cases

  2. Troubleshooting Common Issues - Debug problems

  3. 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

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

  1. Choose your activity type (Running, Cycling, Hiking, or HRMonitor)

  2. Navigate to: Examples/Apps/[YourChoice]/Software/Libs/

  3. Copy both files:

    • Header/ActivityWriter.hpp

    • Sources/ActivityWriter.cpp

  4. Paste into your app’s Software/Libs/ directory

  5. Include 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:

  1. File Header (14 bytes): Contains metadata about the file.

  2. Data Records: A sequence of definition and data messages.

  3. 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 FitHelper objects 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 creation

  • addRecord(const RecordData&): Adds GPS/HR data points

  • addLap(const LapData&): Adds lap summaries

  • pause/resume(std::time_t): Handles activity pauses

  • stop(const TrackData&): Finalizes the FIT file

  • discard(): 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

  1. Standard Messages:

    FitHelper(uint8_t msgID, FIT_MESG_DEF* msgDef);
    

    For predefined FIT messages like File ID, Record, etc.

  2. 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_DEF structure

  • Tracks 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_MESG with scaled/converted data

  • Selects 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_MESG with scaled data

  • Writes 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: 0

  • time_created: FIT timestamp of activity start

  • product_name: β€œUNA Watch” (truncated to 20 chars)

Developer Data ID Message

Purpose: Registers developer and app for custom fields.

Fields:

  • developer_id: Developer identifier string

  • application_id: Application identifier string

  • application_version: Version number

  • manufacturer_id: FIT_MANUFACTURER_DEVELOPMENT

  • developer_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: 0

  • field_definition_number: Unique field ID

  • fit_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 timestamp

  • enhanced_altitude: Altitude in mm (scaled)

  • enhanced_speed: Speed in mm/s (scaled)

  • heart_rate: BPM

Optional Fields:

  • position_lat: Latitude in semicircles

  • position_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 0

  • timestamp: 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) or FIT_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: 0

    • time_created: Unix timestamp converted to FIT time

    • manufacturer: FIT_MANUFACTURER_DEVELOPMENT

    • product: 0

    • number: 0

    • type: FIT_FILE_ACTIVITY

Developer Data ID Message

  • Purpose: Registers the developer and app for custom fields.

  • Fields:

    • developer_id: App-specific developer ID

    • application_id: App-specific application ID

    • application_version: App version

    • manufacturer_id: FIT_MANUFACTURER_DEVELOPMENT

    • developer_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: 0

    • field_definition_number: Unique ID for the field

    • fit_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 timestamp

    • enhanced_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: 0

    • timestamp: End time

    • start_time: Start time

    • 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

    • max_heart_rate: Max HR

    • total_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_laps

  • Developer Fields: Steps and floors (Hiking only)

Event Messages

  • Purpose: Mark start/stop/pause/resume events.

  • Fields:

    • timestamp: Event time

    • event: FIT_EVENT_TIMER

    • event_type: START or STOP

Activity Messages

  • Purpose: Top-level activity info.

  • Fields:

    • timestamp: Activity end time

    • local_timestamp: Local time

    • total_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:

  1. Developer Data ID: Registers the developer/app

  2. Field Descriptions: Define each custom field

  3. Field Attachments: Associate fields with message types

  4. 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

  1. Define Sport Constants:

    // In your app's ActivityWriter.hpp
    enum class MsgNumber {
        // ... existing
        NEW_SPORT = 10
    };
    
  2. Modify Session Message:

    session_mesg.sport = FIT_SPORT_YOUR_SPORT;
    session_mesg.sub_sport = FIT_SUB_SPORT_GENERIC;
    
  3. Add Sport-Specific Fields:

    // If needed, add developer fields
    mFHSportSpecificField(static_cast<uint8_t>(MsgNumber::NEW_SPORT), 4, { &mFHSession });
    

Adding New Developer Fields

  1. Define Field in Constructor:

    , mFHNewField(static_cast<uint8_t>(MsgNumber::NEW_FIELD), field_id, { &mFHRecord })
    
  2. Initialize Field Description:

    mFHNewField.init({ FIT_FIELD_DESCRIPTION_FIELD_NUM_FIELD_NAME,
                       FIT_FIELD_DESCRIPTION_FIELD_NUM_UNITS,
                       /* ... */ });
    
  3. 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);
    
  4. Write Data in Appropriate Methods:

    // In addRecord() or wherever applicable
    FIT_UINT16 newValue = calculateNewValue();
    mFHNewField.writeFieldMessage(0, &newValue, fp);
    

Creating New Record Variants

  1. Add FitHelper for New Variant:

    , mFHRecordNew(static_cast<uint8_t>(MsgNumber::RECORD_NEW), (FIT_MESG_DEF*)fit_mesg_defs[FIT_MESG_RECORD])
    
  2. Initialize with Custom Fields:

    mFHRecordNew.init({ FIT_RECORD_FIELD_NUM_TIMESTAMP,
                        FIT_RECORD_FIELD_NUM_HEART_RATE,
                        // Add your custom fields
                        });
    
  3. Write Definition in start():

    mFHRecordNew.writeDef(fp);
    
  4. 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: fitparse library

  • JavaScript: fit-file-parser

  • Java: 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

  1. Use FIT SDK Dump Tool:

    fitdump activity.fit
    
  2. Check Message Sequence:

    • File ID first

    • Developer Data ID before field descriptions

    • Definitions before data messages

  3. 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/ directory

  • Example 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.