# 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](#introduction-to-fit)** - Understand what FIT is and why it's used 2. **[FIT File Format Basics](#fit-file-format-basics)** - Learn binary format, data types, and scaling 3. **[ActivityWriter Class Overview](#activitywriter-class-overview)** - Get familiar with the main class 4. **[Data Structures in ActivityWriter](#data-structures-in-activitywriter)** - Understand the data you work with #### **Phase 2: Implementation (Core Development)** 5. **[FitHelper Component Deep Dive](#fithelper-component-deep-dive)** - Learn the helper class that does the work 6. **[Step-by-Step FIT File Creation](#step-by-step-fit-file-creation)** - Follow the file creation process 7. **[Code Usage Examples and Walkthroughs](#code-usage-examples-and-walkthroughs)** - Study real code examples #### **Phase 3: Customization (When You Need to Modify)** 8. **[Activity-Specific Variations](#activity-specific-variations)** - See how different activities are implemented 9. **[Developer Fields Implementation](#developer-fields-implementation)** - Add custom data fields 10. **[Extending ActivityWriter for New Activities](#extending-activitywriter-for-new-activities)** - Create new activity types #### **Phase 4: Advanced Topics (As Needed)** 11. **[Advanced Topics and Best Practices](#advanced-topics-and-best-practices)** - Performance and edge cases 12. **[Troubleshooting Common Issues](#troubleshooting-common-issues)** - Debug problems 13. **[FIT File Parsing and Validation](#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** ```cpp // 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)** ```cpp // 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** ```cpp #include // Fixed-width integer types #include // Boolean type #include // String manipulation #include // Assertions #include // Smart pointers #include // String class ``` #### **UNA SDK Logger (Optional but Recommended)** ```cpp #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 ### Navigation Tips - **Use the Table of Contents** to jump to relevant sections - **Search for code examples** using your IDE's search function - **Refer to existing implementations** in `Examples/Apps/` directories - **Check the troubleshooting section** if you encounter issues - **Use the references** for official FIT documentation This document is designed to be both a tutorial and reference. Start with the phases above, then use it as a lookup guide as you develop. ## 🚀 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)** ```cpp // 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 - [FIT Files Structure](#fit-files-structure) - [How to Read This Document](#how-to-read-this-document) - [For New Developers: Getting Started Guide](#for-new-developers-getting-started-guide) - [**Phase 1: Foundations (Essential - Read First)**](#phase-1-foundations-essential---read-first) - [**Phase 2: Implementation (Core Development)**](#phase-2-implementation-core-development) - [**Phase 3: Customization (When You Need to Modify)**](#phase-3-customization-when-you-need-to-modify) - [**Phase 4: Advanced Topics (As Needed)**](#phase-4-advanced-topics-as-needed) - [Key Sections for Different Tasks](#key-sections-for-different-tasks) - [Essential Headers and Includes](#essential-headers-and-includes) - [**Core FIT Functionality**](#core-fit-functionality) - [**FIT SDK Headers (C-based)**](#fit-sdk-headers-c-based) - [**Standard Library Headers**](#standard-library-headers) - [**UNA SDK Logger (Optional but Recommended)**](#una-sdk-logger-optional-but-recommended) - [Quick Start Checklist](#quick-start-checklist) - [Navigation Tips](#navigation-tips) - [🚀 Getting Started: Where to Find ActivityWriter](#-getting-started-where-to-find-activitywriter) - [**Location in UNA SDK**](#location-in-una-sdk) - [**Available Implementations**](#available-implementations) - [**Quick Copy Steps**](#quick-copy-steps) - [**Basic Usage (3 lines of code)**](#basic-usage-3-lines-of-code) - [**Need Help?**](#need-help) - [Table of Contents](#table-of-contents) - [Introduction to FIT](#introduction-to-fit) - [What is FIT?](#what-is-fit) - [Key Concepts for Beginners](#key-concepts-for-beginners) - [FIT Protocol Versions](#fit-protocol-versions) - [Why FIT in UNA SDK?](#why-fit-in-una-sdk) - [Prerequisites for Understanding](#prerequisites-for-understanding) - [FIT File Format Basics](#fit-file-format-basics) - [File Structure Overview](#file-structure-overview) - [Byte Order and Endianness](#byte-order-and-endianness) - [Message Encoding](#message-encoding) - [Data Types in FIT](#data-types-in-fit) - [Scaling and Units](#scaling-and-units) - [ActivityWriter Class Overview](#activitywriter-class-overview) - [Class Purpose and Architecture](#class-purpose-and-architecture) - [Constructor and Initialization](#constructor-and-initialization) - [Public Interface Methods](#public-interface-methods) - [Data Structures in ActivityWriter](#data-structures-in-activitywriter) - [AppInfo Struct](#appinfo-struct) - [RecordData Struct](#recorddata-struct) - [LapData and TrackData Structs](#lapdata-and-trackdata-structs) - [FitHelper Component Deep Dive](#fithelper-component-deep-dive) - [What is FitHelper?](#what-is-fithelper) - [Constructor Variants](#constructor-variants) - [Initialization Process](#initialization-process) - [Writing Messages](#writing-messages) - [Internal Mechanics](#internal-mechanics) - [Step-by-Step FIT File Creation](#step-by-step-fit-file-creation) - [1. Initialization Phase](#1-initialization-phase) - [2. Start Activity](#2-start-activity) - [3. Record Data Points](#3-record-data-points) - [4. Add Laps](#4-add-laps) - [5. Handle Pauses/Resumes](#5-handle-pausesresumes) - [6. Stop and Finalize](#6-stop-and-finalize) - [File Naming Convention](#file-naming-convention) - [Message Types and Fields in Detail](#message-types-and-fields-in-detail) - [File ID Message](#file-id-message) - [Developer Data ID Message](#developer-data-id-message) - [Field Description Messages](#field-description-messages) - [Record Messages](#record-messages) - [Lap Messages](#lap-messages) - [Session Messages](#session-messages) - [Event Messages](#event-messages) - [Activity Messages](#activity-messages) - [Message Types and Fields](#message-types-and-fields) - [File ID Message](#file-id-message-1) - [Developer Data ID Message](#developer-data-id-message-1) - [Field Description Messages](#field-description-messages-1) - [Record Messages](#record-messages-1) - [Lap Messages](#lap-messages-1) - [Session Messages](#session-messages-1) - [Event Messages](#event-messages-1) - [Activity Messages](#activity-messages-1) - [Activity-Specific Variations](#activity-specific-variations) - [Running (Examples/Apps/Running/)](#running-examplesappsrunning) - [Cycling (Examples/Apps/Cycling/)](#cycling-examplesappscycling) - [Hiking (Examples/Apps/Hiking/)](#hiking-examplesappshiking) - [HRMonitor (Examples/Apps/HRMonitor/)](#hrmonitor-examplesappshrmonitor) - [Developer Fields Implementation](#developer-fields-implementation) - [Overview](#overview) - [Battery Fields (Running, Cycling, Hiking)](#battery-fields-running-cycling-hiking) - [Steps and Floors (Hiking)](#steps-and-floors-hiking) - [Trust Level (HRMonitor)](#trust-level-hrmonitor) - [Best Practices for Developer Fields](#best-practices-for-developer-fields) - [Visual Representations and Diagrams](#visual-representations-and-diagrams) - [Complete FIT File Structure](#complete-fit-file-structure) - [Message Definition Structure](#message-definition-structure) - [Record Message Variants Tree](#record-message-variants-tree) - [ActivityWriter Method Flow](#activitywriter-method-flow) - [Code Usage Examples and Walkthroughs](#code-usage-examples-and-walkthroughs) - [Constructor Deep Dive](#constructor-deep-dive) - [start() Method Walkthrough](#start-method-walkthrough) - [prepareRecordMsg() - Data Conversion](#preparerecordmsg---data-conversion) - [addRecord() - Variant Selection Logic](#addrecord---variant-selection-logic) - [stop() Method - Finalization](#stop-method---finalization) - [Advanced Topics and Best Practices](#advanced-topics-and-best-practices) - [FIT Timestamp Handling](#fit-timestamp-handling) - [Data Scaling and Precision](#data-scaling-and-precision) - [GPS Coordinate Conversion](#gps-coordinate-conversion) - [CRC Calculation and Validation](#crc-calculation-and-validation) - [File Header Management](#file-header-management) - [Memory Management and Performance](#memory-management-and-performance) - [Troubleshooting Common Issues](#troubleshooting-common-issues) - [File Creation Failures](#file-creation-failures) - [Invalid FIT Files](#invalid-fit-files) - [Data Scaling Errors](#data-scaling-errors) - [Developer Field Problems](#developer-field-problems) - [Memory Issues](#memory-issues) - [Extending ActivityWriter for New Activities](#extending-activitywriter-for-new-activities) - [Adding a New Sport Type](#adding-a-new-sport-type) - [Adding New Developer Fields](#adding-new-developer-fields) - [Creating New Record Variants](#creating-new-record-variants) - [Best Practices for Extensions](#best-practices-for-extensions) - [FIT File Parsing and Validation](#fit-file-parsing-and-validation) - [Using Garmin FIT SDK](#using-garmin-fit-sdk) - [Third-Party Libraries](#third-party-libraries) - [Validation Tools](#validation-tools) - [Common Parsing Errors](#common-parsing-errors) - [Debugging FIT Files](#debugging-fit-files) - [References and Resources](#references-and-resources) - [Official Documentation](#official-documentation) - [Key FIT Documents](#key-fit-documents) - [Community Resources](#community-resources) - [UNA SDK Specific](#una-sdk-specific) - [Books and Tutorials](#books-and-tutorials) - [Version History](#version-history) ## 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 ```cpp 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: ```cpp mFHFileID(static_cast(MsgNumber::FILE), (FIT_MESG_DEF*)fit_mesg_defs[FIT_MESG_FILE_ID]) mFHLap(static_cast(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: ```cpp 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 ```cpp 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 ```cpp 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**: ```cpp FitHelper(uint8_t msgID, FIT_MESG_DEF* msgDef); ``` For predefined FIT messages like File ID, Record, etc. 2. **Developer Fields**: ```cpp FitHelper(uint8_t msgID, uint8_t fieldID, std::initializer_list container, FIT_UINT8 itemsCount = 1, FIT_UINT8 devIndex = 0); ``` For custom fields attached to parent messages. ### Initialization Process ```cpp bool init(std::initializer_list fields = {}); ``` - Validates field numbers against the base definition - Creates optimized message definition with only specified fields - Builds internal field offset/size mappings ### Writing Messages ```cpp 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 ```cpp ActivityWriter writer(kernel, "/path/to/fit/files"); ``` ### 2. Start Activity ```cpp 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 ```cpp 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 ```cpp 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 ```cpp writer.pause(timestamp); // ... paused activity writer.resume(timestamp); ``` - Writes Event messages for STOP/START ### 6. Stop and Finalize ```cpp 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**: ```cpp 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**: ```cpp 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**: ```cpp mFHStepsField(static_cast(MsgNumber::STEPS), 0, { &mFHLap, &mFHSession }) mFHFloorField(static_cast(MsgNumber::FLOORS), 1, { &mFHLap, &mFHSession }) ``` **Field Descriptions**: - Steps: uint32, no units - Floors: uint32, no units **Lap/Session Enhancement**: ```cpp 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**: ```cpp mFHRecord.init({FIT_RECORD_FIELD_NUM_TIMESTAMP, FIT_RECORD_FIELD_NUM_HEART_RATE}); ``` **Trust Level Field**: ```cpp 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**: ```cpp // 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**: ```cpp mFHBatteryLevelField(static_cast(MsgNumber::BATTERY), 2, { &mFHRecordB, &mFHRecordGB }) mFHBatteryVoltageField(static_cast(MsgNumber::BATTERY), 3, { &mFHRecordB, &mFHRecordGB }) ``` **Writing Data**: ```cpp 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**: ```cpp // 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**: ```cpp mFHStepsField(static_cast(MsgNumber::STEPS), 0, { &mFHLap, &mFHSession }) mFHFloorField(static_cast(MsgNumber::FLOORS), 1, { &mFHLap, &mFHSession }) ``` **Writing to Lap/Session**: ```cpp // 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**: ```cpp 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 ```cpp 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(MsgNumber::FILE), (FIT_MESG_DEF*)fit_mesg_defs[FIT_MESG_FILE_ID]) , mFHDeveloper(static_cast(MsgNumber::DEVELOP), (FIT_MESG_DEF*)fit_mesg_defs[FIT_MESG_DEVELOPER_DATA_ID]) , mFHLap(static_cast(MsgNumber::LAP), (FIT_MESG_DEF*)fit_mesg_defs[FIT_MESG_LAP]) , mFHSession(static_cast(MsgNumber::SESSION), (FIT_MESG_DEF*)fit_mesg_defs[FIT_MESG_SESSION]) , mFHEvent(static_cast(MsgNumber::EVENT), (FIT_MESG_DEF*)fit_mesg_defs[FIT_MESG_EVENT]) , mFHActivity(static_cast(MsgNumber::ACTIVITY), (FIT_MESG_DEF*)fit_mesg_defs[FIT_MESG_ACTIVITY]) // Record variants for different field combinations , mFHRecord(static_cast(MsgNumber::RECORD), (FIT_MESG_DEF*)fit_mesg_defs[FIT_MESG_RECORD]) , mFHRecordG(static_cast(MsgNumber::RECORD_G), (FIT_MESG_DEF*)fit_mesg_defs[FIT_MESG_RECORD]) , mFHRecordB(static_cast(MsgNumber::RECORD_B), (FIT_MESG_DEF*)fit_mesg_defs[FIT_MESG_RECORD]) , mFHRecordGB(static_cast(MsgNumber::RECORD_GB), (FIT_MESG_DEF*)fit_mesg_defs[FIT_MESG_RECORD]) // Developer fields for battery (attached to battery-enabled records) , mFHBatteryLevelField(static_cast(MsgNumber::BATTERY), 2, { &mFHRecordB, &mFHRecordGB }) , mFHBatteryVoltageField(static_cast(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 ```cpp 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(developer.developer_id), info.devID.c_str(), FIT_DEVELOPER_DATA_ID_MESG_DEVELOPER_ID_COUNT); strncpy(reinterpret_cast(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 ```cpp 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(record.speed * 1000); } // Altitude scaling: float m -> uint32 mm with offset if (record.has(RecordData::Field::ALTITUDE)) { msg.enhanced_altitude = static_cast((record.altitude + 500) * 5); } // Heart rate (direct mapping) if (record.has(RecordData::Field::HEART_RATE)) { msg.heart_rate = static_cast(record.heartRate); } return msg; } ``` ### addRecord() - Variant Selection Logic ```cpp 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 ```cpp 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(track.elapsed * 1000); session_mesg.total_timer_time = static_cast(track.duration * 1000); // Scaled distance, speed, etc. session_mesg.total_distance = static_cast(track.distance * 100); session_mesg.avg_speed = static_cast(track.speedAvg * 1000); session_mesg.max_speed = static_cast(track.speedMax * 1000); session_mesg.avg_heart_rate = static_cast(track.hrAvg); session_mesg.max_heart_rate = static_cast(track.hrMax); session_mesg.total_ascent = static_cast(track.ascent); session_mesg.total_descent = static_cast(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(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**: ```cpp 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(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**: ```cpp // Speed: float m/s -> uint32 mm/s (1000 * m/s + 0) uint32_t scaled_speed = static_cast(speed_mps * 1000); // Distance: float m -> uint32 cm (100 * m + 0) uint32_t scaled_distance = static_cast(distance_m * 100); // Altitude: float m -> uint32 mm with offset (5 * m + 500) uint32_t scaled_altitude = static_cast((altitude_m + 500) * 5); // Time: float s -> uint32 ms (1000 * s + 0) uint32_t scaled_time = static_cast(time_s * 1000); ``` **Reverse Scaling for Reading**: ```cpp 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**: ```cpp FIT_SINT32 ActivityWriter::ConvertDegreesToSemicircles(float degrees) { return static_cast(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**: ```cpp 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(buffer), toRead, br); crc = FitCRC_Update16(crc, buffer, static_cast(br)); pos += br; } fp->close(); fp->open(true, false); // Write mode, append fp->seek(fp->size()); size_t bw; fp->write(reinterpret_cast(&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**: ```cpp 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(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(&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**: ```cpp // 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**: ```cpp // 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**: ```cpp // 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**: ```cpp // In your app's ActivityWriter.hpp enum class MsgNumber { // ... existing NEW_SPORT = 10 }; ``` 2. **Modify Session Message**: ```cpp session_mesg.sport = FIT_SPORT_YOUR_SPORT; session_mesg.sub_sport = FIT_SUB_SPORT_GENERIC; ``` 3. **Add Sport-Specific Fields**: ```cpp // If needed, add developer fields mFHSportSpecificField(static_cast(MsgNumber::NEW_SPORT), 4, { &mFHSession }); ``` ### Adding New Developer Fields 1. **Define Field in Constructor**: ```cpp , mFHNewField(static_cast(MsgNumber::NEW_FIELD), field_id, { &mFHRecord }) ``` 2. **Initialize Field Description**: ```cpp mFHNewField.init({ FIT_FIELD_DESCRIPTION_FIELD_NUM_FIELD_NAME, FIT_FIELD_DESCRIPTION_FIELD_NUM_UNITS, /* ... */ }); ``` 3. **Write Field Description in start()**: ```cpp 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**: ```cpp // In addRecord() or wherever applicable FIT_UINT16 newValue = calculateNewValue(); mFHNewField.writeFieldMessage(0, &newValue, fp); ``` ### Creating New Record Variants 1. **Add FitHelper for New Variant**: ```cpp , mFHRecordNew(static_cast(MsgNumber::RECORD_NEW), (FIT_MESG_DEF*)fit_mesg_defs[FIT_MESG_RECORD]) ``` 2. **Initialize with Custom Fields**: ```cpp mFHRecordNew.init({ FIT_RECORD_FIELD_NUM_TIMESTAMP, FIT_RECORD_FIELD_NUM_HEART_RATE, // Add your custom fields }); ``` 3. **Write Definition in start()**: ```cpp mFHRecordNew.writeDef(fp); ``` 4. **Use in addRecord()**: ```cpp 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**: ```cpp #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**: ```bash 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.