# 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. **[SDK::Fit Encoder Deep Dive](#sdkfit-encoder-deep-dive)** - Learn the encoder 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" // Native SDK::Fit streaming encoder #include "SDK/Fit/FitWriter.hpp" // FIT profile constants (message/field numbers, enums) #include "SDK/Fit/FitProfile.hpp" // Kernel for file system access #include "SDK/Kernel/Kernel.hpp" // File system interface #include "SDK/Interfaces/IFileSystem.hpp" ``` #### **Optional SDK::Fit Helpers** ```cpp // Base-type identifiers, sizes and invalid sentinels #include "SDK/Fit/FitBaseType.hpp" // FIT CRC-16 (used internally by FitWriter; rarely needed directly) #include "SDK/Fit/FitCrc.hpp" // Cadence / step_length record-field encoders (running/treadmill) #include "SDK/Fit/FitRecordCadence.hpp" ``` #### **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) - [**Optional SDK::Fit Helpers**](#optional-sdkfit-helpers) - [**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) - [SDK::Fit Encoder Deep Dive](#sdkfit-encoder-deep-dive) - [What is SDK::Fit?](#what-is-sdkfit) - [FitWriter Lifecycle](#fitwriter-lifecycle) - [Defining Messages](#defining-messages) - [Writing Data Records](#writing-data-records) - [FitProfile Constants](#fitprofile-constants) - [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) - [Developer Fields on Lap / Session](#developer-fields-on-lap--session) - [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) - [defineRecordMessages() - Record Variants](#definerecordmessages---record-variants) - [addRecord() - Variant Selection and Data Write](#addrecord---variant-selection-and-data-write) - [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) - [Payload / Definition Mismatch](#payload--definition-mismatch) - [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 (`SDK::Fit::kProtocolVersion20`, the byte `0x20`) - Defines the basic file structure and message encoding. - **Profile Version**: Passed to `FitWriter::begin(profileVersion)`. The UNA apps pass `0` (profile-agnostic) because the native encoder supplies the message/field numbers itself; the value is only advisory metadata in the header. 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. The native encoder models them with the `SDK::Fit::BaseType` enum (`SDK/Fit/FitBaseType.hpp`); each value is the on-wire base-type identifier byte (bit 7 marks multi-byte / endian-sensitive types): - `BaseType::UInt8` (1 byte) - `BaseType::SInt8` (1 byte) - `BaseType::UInt16` (2 bytes) - `BaseType::SInt16` (2 bytes) - `BaseType::UInt32` (4 bytes) - `BaseType::SInt32` (4 bytes) - `BaseType::String` (variable) - `BaseType::Float32` (4 bytes) - `BaseType::Float64` (8 bytes) - `BaseType::UInt8z` (1 byte, unsigned with invalid value) - `BaseType::UInt16z` (2 bytes) - `BaseType::UInt32z` (4 bytes) - `BaseType::Byte` (1 byte array) - `BaseType::Enum` (1 byte enumeration) - `BaseType::SInt64` / `BaseType::UInt64` / `BaseType::UInt64z` (8 bytes) Three free helpers operate on these: `baseTypeId(t)` (the identifier byte), `baseTypeSize(t)` (element size in bytes) and `baseTypeInvalid(t)` (the canonical "invalid" sentinel to write for an absent value). ### 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: - Own a single `SDK::Fit::FitWriter` encoder over the activity file - Create and manage FIT files - Emit message definitions and data records - Handle developer fields - Stream data to disk; back-patch the header and append the CRC on `finish()` ### Constructor and Initialization ```cpp ActivityWriter::ActivityWriter(const SDK::Kernel& kernel, const char* pathToDir) : mKernel(kernel), mPath(pathToDir) { assert(pathToDir != nullptr); } ``` - Takes a reference to the UNA Kernel for file system access - Stores the base path for FIT file storage - Does **no** FIT work itself: the encoder is profile-agnostic, so there are no per-message handler objects to set up. The `FitWriter` is constructed in `start()` once the file is open. Instead of constructing many helper objects, the class declares a small set of **local message types** (FIT record-header values, 0-15) and the **developer field numbers** it uses, then writes the definitions in `start()`: ```cpp // In ActivityWriter.hpp enum Local : uint8_t { L_FILE_ID = 0, L_DEV_ID, L_FIELD_DESC, L_EVENT, L_RECORD, L_RECORD_G, L_RECORD_B, L_RECORD_GB, L_LAP, L_SESSION, L_ACTIVITY, L_WORKOUT, L_WORKOUT_STEP, }; enum DevField : uint8_t { DF_BATTERY_LEVEL = 2, DF_BATTERY_VOLTAGE = 3, DF_HR_SOURCE = 4, DF_HR_OPTICAL = 5, DF_HR_EXTERNAL = 6, }; std::unique_ptr mFile; std::unique_ptr mFit; // constructed in start() ``` In `start()`, each message type is associated with a global message number and an ordered field list via `defineMessage(...)` -- for example the Lap message: ```cpp mFit->defineMessage(L_LAP, fit::mesgNum(fit::MesgNum::Lap), {fit::field::Lap::Timestamp, fit::field::Lap::StartTime, fit::field::Lap::TotalElapsedTime, fit::field::Lap::TotalTimerTime, fit::field::Lap::TotalDistance, fit::field::Lap::MessageIndex, fit::field::Lap::AvgSpeed, fit::field::Lap::MaxSpeed, fit::field::Lap::TotalAscent, fit::field::Lap::TotalDescent, fit::field::Lap::AvgHeartRate, fit::field::Lap::MaxHeartRate, fit::field::Lap::WktStepIndex}); ``` ### 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. ## SDK::Fit Encoder Deep Dive ### What is SDK::Fit? `SDK::Fit` is the SDK's native, dependency-free FIT-format encoder. It lives in `Libs/Header/SDK/Fit/` and has two parts: - **`FitWriter`** (`SDK/Fit/FitWriter.hpp`) -- the streaming engine. It writes the 14-byte header, a sequence of definition and data records keyed by *local message type*, and the trailing little-endian file CRC. The engine is *profile-agnostic*: the caller supplies global message numbers, field-definition numbers and base types, so one engine serves every message. - **`FitProfile`** (`SDK/Fit/FitProfile.hpp`) -- the data dictionary. It maps message/field names to the public FIT interoperability constants (`MesgNum`, the `field::::` definitions, and the enum value sets such as `Sport`, `Event`, `EventType`). Records are written as they are produced, so RAM use is constant and a partially recorded activity survives on disk if recording is interrupted. ### FitWriter Lifecycle A `FitWriter` wraps an already-open `IFile` and is driven through three phases: ```cpp SDK::Fit::FitWriter writer(file); // file is an open IFile& writer.begin(/*profileVersion=*/0); // 1. write header placeholder // ... defineMessage(...) / data(...).write() ... writer.finish(); // 3. back-patch size + append CRC ``` - `begin(profileVersion, protocolVersion = kProtocolVersion20)` writes the header placeholder (header CRC left 0x0000, data size back-patched later). - `finish()` back-patches the header data size, sets the header CRC, and appends the trailing file CRC (computed by reopening the file read-only). - `ok()` reports whether all IO and size checks have succeeded so far. ### Defining Messages Before any data record you must declare its layout. `defineMessage` associates a *local type* (0-15) with a global message number and an ordered field list, optionally followed by developer fields: ```cpp bool defineMessage(uint8_t localType, uint16_t globalMesgNum, std::initializer_list fields, std::initializer_list devFields = {}); ``` - `Field` is `{ uint8_t fieldDefNum; BaseType baseType; uint8_t count = 1; }`. The `field::*` constants in `FitProfile.hpp` are ready-made `Field` values. For variable-length string/array fields the profile exposes the field number only (e.g. `field::FieldDescription::kFieldNameNum`) so the caller can size the field at encode time: `{kFieldNameNum, BaseType::String, nameLen}`. - `DevField` is `{ uint8_t fieldNum; uint8_t sizeBytes; uint8_t devDataIndex; }`. Supplying any developer fields sets the developer-data flag in the record header. A local type may be **redefined** later (e.g. the `field_description` slot is redefined for each developer field so the name/units strings are sized exactly). ### Writing Data Records `data(localType)` returns a `FitWriter::Data` builder. Chain typed appenders in **definition order**, then call `write()`: ```cpp mFit->data(L_RECORD) .u32(unixToFitTimestamp(rec.timestamp)) // timestamp .u8(rec.heartRate) // heart_rate .u32(rec.steps) // steps (developer field) .write(); ``` Available appenders: `.u8/.i8/.u16/.i16/.u32/.i32/.u64/.i64/.f32/.f64`, `.str(s, fieldBytes)` (null-padded to exactly `fieldBytes`), `.bytes(p, n)` (raw byte array), and `.invalid(BaseType, count = 1)` (the canonical sentinel for a declared-but-absent value). `write()` fails if the assembled payload size does not match the active definition for that local type, which catches field/value mismatches early. For an absent scalar you can also write the sentinel explicitly, e.g. `.u8(static_cast(fit::baseTypeInvalid(fit::BaseType::UInt8)))`. ### FitProfile Constants `FitProfile.hpp` provides everything needed to address messages and fields: - `enum class MesgNum` and `mesgNum(MesgNum)` -- global message numbers (FileId, Session, Lap, Record, Event, Workout, WorkoutStep, Activity, FieldDescription, DeveloperDataId). - `namespace field::` -- per-field `FitWriter::Field` constants (e.g. `field::Record::HeartRate`, `field::Session::Sport`). - Enum value sets for enum fields: `File`, `Sport`, `SubSport`, `Event`, `EventType`, `ActivityType`, `Intensity`, `Manufacturer`, `WktStepDuration`, `WktStepTarget`, plus the `kMessageIndexInvalid` sentinel. Only the messages/fields the UNA apps actually write are included; add new ones to `FitProfile.hpp` rather than hard-coding numbers at the call site. ### Internal Mechanics - Tracks, per local type, the expected payload size and whether it is defined, validating each `write()` against it. - Writes all multi-byte values little-endian. - Computes the FIT CRC-16 (`SDK/Fit/FitCrc.hpp`) over the whole file in `finish()`. - Holds only the current record's payload in a small buffer -- no whole-file buffering. ## 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 the FIT file with a timestamp-based name - Constructs the `FitWriter` and calls `begin()` (writes the header placeholder) - Defines + writes the File ID message - Defines + writes the Developer Data ID message - Defines + writes the Field Description messages for developer fields - Defines all remaining message types (Event, Record variants, Lap, Session, Activity) - Writes the initial Event (START) data record ### 3. Record Data Points ```cpp RecordData record = {/* populate data */}; record.set(RecordData::Field::COORDS); record.set(RecordData::Field::HEART_RATE); // ... writer.addRecord(record); ``` - Selects the appropriate record local type based on available fields (GPS, battery, etc.) - Scales/converts the data and writes it field-by-field with `data(local).uXX(...).write()` - Appends any developer fields (battery, HR source) in definition order ### 4. Add Laps ```cpp LapData lap = {/* lap summary data */}; writer.addLap(lap); ``` - Writes the Lap data record with scaled values via `data(L_LAP)...write()` ### 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 the final Event (STOP) data record (where applicable) - Writes the Session message (with any developer fields) - Writes the Activity message - Calls `mFit->finish()`, which back-patches the header data size + CRC and appends the file CRC - Closes the file and saves a 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** (`field::FileId::*`, `MesgNum::FileId`): - `type`: `Sport`/`File::Activity` (4) - `manufacturer`: `Manufacturer::Development` (255) - `product`: 0 (development product) - `serial_number`: 0 - `time_created`: FIT timestamp of activity start ### Developer Data ID Message **Purpose**: Registers developer and app for custom fields. **Fields** (`field::DeveloperDataId::*`, `MesgNum::DeveloperDataId`): - `application_id`: Application identifier byte array (16 bytes, from `APP_ID`) - `developer_data_index`: 0 ### Field Description Messages **Purpose**: Define custom developer fields. **Common Structure** (`field::FieldDescription::*`, `MesgNum::FieldDescription`): - `developer_data_index`: 0 - `field_definition_number`: Unique field ID - `fit_base_type_id`: Base-type id, e.g. `baseTypeId(BaseType::UInt8)` - `field_name` (`kFieldNameNum`): Field name string (e.g., "batteryLevel"), sized at encode time - `units` (`kUnitsNum`): Units string (e.g., "%", "mV"), sized at encode time ### Record Messages **Purpose**: Individual data points during activity. **Base Fields** (`field::Record::*`, the common tail every variant shares): - `timestamp`: FIT timestamp - `enhanced_altitude`: Altitude, scale 5 + offset 500, m - `enhanced_speed`: Speed, scale 1000, m/s - `heart_rate`: BPM - `cadence` / `fractional_cadence`: rpm (encoded by `FitRecordCadence`) - `step_length`: scale 10, mm **Optional Fields** (GPS variants): - `position_lat`: Latitude in semicircles - `position_long`: Longitude in semicircles **Developer Fields** (conditional, attached to the chosen variant): - HR source / optical bpm / external bpm - Battery level (%) and voltage (mV) on the battery variants ### Lap Messages **Purpose**: Summarize data for each lap segment. **Fields** (`field::Lap::*`, `MesgNum::Lap`): - `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**: Defined per-app via `defineMessage(...)` developer fields (e.g. the Running writer attaches `hr_source`/`hr_optical`/`hr_external`, and battery level/voltage on the battery record variants). ### Session Messages **Purpose**: Overall activity summary. **Fields** (`field::Session::*`, `MesgNum::Session`): Similar to Lap, plus: - `sport`: Activity type (e.g., `Sport::Running`) - `sub_sport`: Sub-type (e.g., `SubSport::Generic`) - `num_laps`: Number of laps ### Event Messages **Purpose**: Mark activity state changes. **Fields** (`field::Event::*`, `MesgNum::Event`): - `timestamp`: Event time (FIT timestamp) - `event`: `Event::Timer` (0) - `event_type`: `EventType::Start` (0) or `EventType::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** (`field::FileId::*`): - `type`: `File::Activity` - `manufacturer`: `Manufacturer::Development` - `product`: 0 - `serial_number`: 0 - `time_created`: Unix timestamp converted to FIT time ### Developer Data ID Message - **Purpose**: Registers the developer and app for custom fields. - **Fields** (`field::DeveloperDataId::*`): - `application_id`: 16-byte application identifier (from `APP_ID`) - `developer_data_index`: 0 ### Field Description Messages - **Purpose**: Describe custom developer fields. - **Fields** (per field, `field::FieldDescription::*`): - `developer_data_index`: 0 - `field_definition_number`: Unique ID for the field - `fit_base_type_id`: Base-type id (e.g. `baseTypeId(BaseType::UInt8)`) - `field_name` (`kFieldNameNum`): e.g., "batteryLevel", "steps" - `units` (`kUnitsNum`): e.g., "%", "mV" ### Record Messages - **Purpose**: Individual data points during the activity (e.g., every second). - **Base Fields** (`field::Record::*`, the common tail): - `timestamp`: FIT timestamp - `enhanced_altitude`: Altitude, scale 5 + offset 500, m - `enhanced_speed`: Speed, scale 1000, m/s - `heart_rate`: BPM - `cadence` / `fractional_cadence`, `step_length` - **Variants** (selected by available data; one local type each): - Basic Record (`L_RECORD`): No GPS or battery - Record with GPS (`L_RECORD_G`): Adds `position_lat`, `position_long` (semicircles) - Record with Battery (`L_RECORD_B`): Adds developer fields for battery level and voltage - Combined (`L_RECORD_GB`): 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**: Defined per app via `defineMessage(...)` developer fields ### Session Messages - **Purpose**: Overall activity summary. - **Fields**: Similar to Lap, but for the entire session. - **Additional**: `sport` (`Sport::*`), `sub_sport` (`SubSport::*`), `num_laps` ### Event Messages - **Purpose**: Mark start/stop/pause/resume events. - **Fields** (`field::Event::*`): - `timestamp`: Event time - `event`: `Event::Timer` - `event_type`: `EventType::Start` or `EventType::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**: `Sport::Running` **Local Message Types** (`enum Local`): - `L_FILE_ID`, `L_DEV_ID`, `L_FIELD_DESC`, `L_EVENT`, `L_RECORD`, `L_RECORD_G`, `L_RECORD_B`, `L_RECORD_GB`, `L_LAP`, `L_SESSION`, `L_ACTIVITY`, `L_WORKOUT`, `L_WORKOUT_STEP` **Record Variants** (each a separate local type sharing the global Record number): - Basic (`L_RECORD`): timestamp, altitude, speed, HR, cadence, step_length - With GPS (`L_RECORD_G`): + lat/long - With Battery (`L_RECORD_B`): + battery level/voltage developer fields - Combined (`L_RECORD_GB`): GPS + Battery **Session Fields**: Full set (distance, speed, elevation, HR) **Developer Fields** (`enum DevField`): - `DF_BATTERY_LEVEL` (2, uint8, %), `DF_BATTERY_VOLTAGE` (3, uint16, mV) - `DF_HR_SOURCE` (4), `DF_HR_OPTICAL` (5), `DF_HR_EXTERNAL` (6) -- all uint8 **Code Example**: ```cpp mFit->data(L_SESSION) // ... preceding fields ... .u8(static_cast(fit::Sport::Running)) .u8(static_cast(fit::SubSport::Generic)) // ... .write(); ``` ### Cycling (Examples/Apps/Cycling/) **Sport Type**: `Sport::Cycling` **Identical to Running** except sport type. All record variants, battery fields, and session fields are the same. **Code Difference**: ```cpp .u8(static_cast(fit::Sport::Cycling)) ``` ### Hiking (Examples/Apps/Hiking/) **Sport Type**: `Sport::Hiking` Structurally the same as Running -- it shares the Record variants and the same developer-field approach. Any extra metrics are added as additional developer fields on the Lap/Session definitions (see "Developer Fields Implementation"), declared in the `defineMessage(...)` developer-field list and written in definition order at the tail of the `data(...)` builder. ```cpp .u8(static_cast(fit::Sport::Hiking)) ``` ### HRMonitor (Examples/Apps/HRMonitor/) **Sport Type**: `Sport::Generic` **Simplified Structure**: - Minimal record: only `timestamp` and `heart_rate` - No GPS, speed, altitude, distance fields - One custom developer field: `hr_trust_level` (`DF_HR_TRUST_LEVEL`, uint8, "percents") **Record Definition** (with the trust-level developer field): ```cpp mFit->defineMessage(L_RECORD, fit::mesgNum(fit::MesgNum::Record), {fit::field::Record::Timestamp, fit::field::Record::HeartRate}, {{DF_HR_TRUST_LEVEL, 1, 0}}); ``` **Field Description + data write**: ```cpp writeFieldDescription(DF_HR_TRUST_LEVEL, "hr_trust_level", "percents", fit::BaseType::UInt8); mFit->data(L_RECORD) .u32(unixToFitTimestamp(record.timestamp)) .u8(record.heartRate) .u8(record.trustLevel) // developer field hr_trust_level .write(); ``` **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. With the native encoder the process involves: 1. **Developer Data ID**: Register the developer/app (`MesgNum::DeveloperDataId`) 2. **Field Descriptions**: Define each custom field (`MesgNum::FieldDescription`) 3. **Declare the developer fields**: Pass a `DevField` list (field number, size in bytes, developer-data index) as the third argument to `defineMessage(...)` on the message that carries them. This sets the developer-data flag in that record's header. 4. **Data Writing**: Append the developer-field values, in declared order, at the **tail** of the same `data(...)` builder, after the standard fields. The `DevField` is `{ uint8_t fieldNum; uint8_t sizeBytes; uint8_t devDataIndex; }`. ### Battery Fields (Running, Cycling, Hiking) **Field Descriptions** (one redefine of the field_description slot per field): ```cpp writeFieldDescription(DF_BATTERY_LEVEL, "batteryLevel", "%", fit::BaseType::UInt8); writeFieldDescription(DF_BATTERY_VOLTAGE, "battVoltage", "mV", fit::BaseType::UInt16); ``` where `writeFieldDescription` redefines `L_FIELD_DESC` to size the name/units strings exactly, then writes the data record: ```cpp mFit->defineMessage(L_FIELD_DESC, fit::mesgNum(fit::MesgNum::FieldDescription), {fit::field::FieldDescription::DeveloperDataIndex, fit::field::FieldDescription::FieldDefinitionNumber, fit::field::FieldDescription::FitBaseTypeId, {fit::field::FieldDescription::kFieldNameNum, fit::BaseType::String, nameLen}, {fit::field::FieldDescription::kUnitsNum, fit::BaseType::String, unitsLen}}); mFit->data(L_FIELD_DESC) .u8(0) // developer_data_index .u8(devFieldNum) // field_definition_number .u8(fit::baseTypeId(baseType)) // fit_base_type_id .str(name, nameLen) .str(units, unitsLen) .write(); ``` **Declaration on the battery record variants** (third `defineMessage` argument): ```cpp const DevFieldDef batt5[] = { {DF_BATTERY_LEVEL, 1, 0}, {DF_BATTERY_VOLTAGE, 2, 0}, {DF_HR_SOURCE, 1, 0}, {DF_HR_OPTICAL, 1, 0}, {DF_HR_EXTERNAL, 1, 0}, }; mFit->defineMessage(L_RECORD_B, fit::mesgNum(fit::MesgNum::Record), { /* standard record fields */ }, {batt5[0], batt5[1], batt5[2], batt5[3], batt5[4]}); ``` **Writing Data** (developer fields at the tail, in declared order): ```cpp const uint8_t local = batt ? (gps ? L_RECORD_GB : L_RECORD_B) : (gps ? L_RECORD_G : L_RECORD); fit::FitWriter::Data d = mFit->data(local); // ... standard record fields appended first ... if (batt) { d.u8(record.batteryLevel).u16(record.batteryVoltage); } d.u8(record.hrSource).u8(record.hrOpticalBpm).u8(record.hrExternalBpm); d.write(); ``` ### Developer Fields on Lap / Session Lap- and session-level custom metrics follow the same pattern: declare the `DevField` list on the `L_LAP` / `L_SESSION` definition, write a field description for each, then append the values at the tail of the corresponding `data(L_LAP)` / `data(L_SESSION)` builder before `write()`. ### Trust Level (HRMonitor) **Setup** -- a single uint8 developer field on the record: ```cpp writeFieldDescription(DF_HR_TRUST_LEVEL, "hr_trust_level", "percents", fit::BaseType::UInt8); mFit->defineMessage(L_RECORD, fit::mesgNum(fit::MesgNum::Record), {fit::field::Record::Timestamp, fit::field::Record::HeartRate}, {{DF_HR_TRUST_LEVEL, 1, 0}}); mFit->data(L_RECORD) .u32(unixToFitTimestamp(record.timestamp)) .u8(record.heartRate) .u8(record.trustLevel) // developer field, written last .write(); ``` ### 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, hr_source, 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 (each a distinct local type; same global Record number 20) ├── Basic Record (L_RECORD) │ ├── timestamp (uint32) │ ├── enhanced_altitude (uint32, scaled) │ ├── enhanced_speed (uint32, scaled) │ ├── heart_rate (uint8) │ ├── cadence / fractional_cadence (uint8) │ ├── step_length (uint16, scaled) │ └── + dev fields: hr_source, hr_optical, hr_external (uint8) ├── Record + GPS (L_RECORD_G) │ ├── (Basic fields) │ └── position_lat / position_long (sint32, semicircles) ├── Record + Battery (L_RECORD_B) │ ├── (Basic fields) │ └── + dev fields: batteryLevel (uint8, #2), battVoltage (uint16, #3), │ hr_source/optical/external └── Record + GPS + Battery (L_RECORD_GB) ├── (Basic + GPS fields) └── + battery + hr dev fields ``` ### ActivityWriter Method Flow ``` ActivityWriter Lifecycle: Constructor └── Store kernel + path (no FIT work yet) start(AppInfo) ├── createAndOpenFile() ├── construct FitWriter over the open file ├── mFit->begin() [header placeholder] ├── defineMessage + data().write() for File ID ├── defineMessage + data().write() for Developer Data ID ├── writeFieldDescription() for each developer field ├── defineMessage for Event / Record variants / Lap / Session / Activity └── addMessageEvent(START) addRecord(RecordData) ├── Select record local type (GPS/battery) ├── Append standard fields (scaled/converted) to data(local) └── Append developer fields at the tail, then write() addLap(LapData) └── data(L_LAP) with scaled values, then write() pause/resume(time_t) └── addMessageEvent(STOP/START) stop(TrackData) ├── data(L_SESSION).write() (+ dev fields if any) ├── data(L_ACTIVITY).write() ├── mFit->finish() [back-patch size + header CRC, append file CRC] ├── close file └── saveSummary() - create JSON file discard() ├── mFit.reset() └── close + remove the file ``` ## Code Usage Examples and Walkthroughs ### Constructor Deep Dive The constructor is trivial: it stores the kernel reference and base path. All FIT work happens once the file is open, in `start()`. ```cpp ActivityWriter::ActivityWriter(const SDK::Kernel& kernel, const char* pathToDir) : mKernel(kernel), mPath(pathToDir) { assert(pathToDir != nullptr); } ``` The message layout lives in two places instead: the `Local` / `DevField` enums in the header (see "Constructor and Initialization"), and the `defineMessage(...)` calls in `start()`. ### start() Method Walkthrough ```cpp namespace fit = SDK::Fit; void ActivityWriter::start(const AppInfo& info) { mLapCounter = 0; if (!createAndOpenFile(info.timestamp)) { return; } // 1. Construct the encoder over the open file and write the header placeholder. mFit = std::make_unique(*mFile); mFit->begin(/*profileVersion=*/0); // 2. file_id: define, then write one data record. mFit->defineMessage(L_FILE_ID, fit::mesgNum(fit::MesgNum::FileId), {fit::field::FileId::Type, fit::field::FileId::Manufacturer, fit::field::FileId::Product, fit::field::FileId::SerialNumber, fit::field::FileId::TimeCreated}); mFit->data(L_FILE_ID) .u8(static_cast(fit::File::Activity)) .u16(static_cast(fit::Manufacturer::Development)) .u16(0) // product .u32(0) // serial_number .u32(unixToFitTimestamp(info.timestamp)) .write(); // 3. developer_data_id: registers the app for custom fields. mFit->defineMessage(L_DEV_ID, fit::mesgNum(fit::MesgNum::DeveloperDataId), {fit::field::DeveloperDataId::ApplicationId, fit::field::DeveloperDataId::DeveloperDataIndex}); { uint8_t appId[16] = {}; std::strncpy(reinterpret_cast(appId), info.appID.c_str(), sizeof(appId)); mFit->data(L_DEV_ID).bytes(appId, sizeof(appId)).u8(0).write(); } // 4. field_description for each developer field (label/units survive any profile). writeFieldDescription(DF_BATTERY_LEVEL, "batteryLevel", "%", fit::BaseType::UInt8); writeFieldDescription(DF_BATTERY_VOLTAGE, "battVoltage", "mV", fit::BaseType::UInt16); writeFieldDescription(DF_HR_SOURCE, "hr_source", nullptr, fit::BaseType::UInt8); writeFieldDescription(DF_HR_OPTICAL, "hr_optical", "bpm", fit::BaseType::UInt8); writeFieldDescription(DF_HR_EXTERNAL, "hr_external", "bpm", fit::BaseType::UInt8); // 5. Define the remaining message types up front. mFit->defineMessage(L_EVENT, fit::mesgNum(fit::MesgNum::Event), {fit::field::Event::Timestamp, fit::field::Event::EventField, fit::field::Event::EventType}); defineRecordMessages(); // L_RECORD / _G / _B / _GB (see below) // ... L_LAP, L_SESSION, L_ACTIVITY defined here too ... // 6. Start the activity with an Event (START) data record. addMessageEvent(info.timestamp, fit::EventType::Start); } ``` ### defineRecordMessages() - Record Variants Each variant is a distinct local type that reuses the global Record message number; the differences are the field list and the attached developer fields. ```cpp void ActivityWriter::defineRecordMessages() { const DevFieldDef hr3[] = { {DF_HR_SOURCE, 1, 0}, {DF_HR_OPTICAL, 1, 0}, {DF_HR_EXTERNAL, 1, 0}, }; const DevFieldDef batt5[] = { {DF_BATTERY_LEVEL, 1, 0}, {DF_BATTERY_VOLTAGE, 2, 0}, {DF_HR_SOURCE, 1, 0}, {DF_HR_OPTICAL, 1, 0}, {DF_HR_EXTERNAL, 1, 0}, }; // Plain record (HR/cadence) + 3 HR developer fields. mFit->defineMessage(L_RECORD, fit::mesgNum(fit::MesgNum::Record), {fit::field::Record::Timestamp, fit::field::Record::EnhancedAltitude, fit::field::Record::EnhancedSpeed, fit::field::Record::HeartRate, fit::field::Record::Cadence, fit::field::Record::FractionalCadence, fit::field::Record::StepLength}, {hr3[0], hr3[1], hr3[2]}); // + GPS (adds position_lat/long), + battery (5 dev fields), + GPS + battery // are defined the same way with L_RECORD_G / L_RECORD_B / L_RECORD_GB. } ``` ### addRecord() - Variant Selection and Data Write ```cpp void ActivityWriter::addRecord(const RecordData& record) { if (!mFit) return; const bool gps = record.has(RecordData::Field::COORDS); const bool batt = record.has(RecordData::Field::BATTERY); const uint8_t local = batt ? (gps ? L_RECORD_GB : L_RECORD_B) : (gps ? L_RECORD_G : L_RECORD); fit::FitWriter::Data d = mFit->data(local); d.u32(unixToFitTimestamp(record.timestamp)); if (gps) { d.i32(degreesToSemicircles(record.latitude)) .i32(degreesToSemicircles(record.longitude)); } // enhanced_altitude (5 * m + 500), enhanced_speed (1000 * m/s); write the // canonical invalid sentinel when a field is absent. d.u32(record.has(RecordData::Field::ALTITUDE) ? static_cast((record.altitude + 500.0f) * 5.0f) : static_cast(fit::baseTypeInvalid(fit::BaseType::UInt32))); d.u32(record.has(RecordData::Field::SPEED) ? static_cast(record.speed * 1000.0f) : static_cast(fit::baseTypeInvalid(fit::BaseType::UInt32))); d.u8(record.has(RecordData::Field::HEART_RATE) ? static_cast(record.heartRate) : static_cast(fit::baseTypeInvalid(fit::BaseType::UInt8))); // cadence / fractional_cadence, step_length (see FitRecordCadence) ... // Developer fields, in definition order (battery first if present, then HR). if (batt) { d.u8(record.batteryLevel).u16(record.batteryVoltage); } d.u8(record.hrSource).u8(record.hrOpticalBpm).u8(record.hrExternalBpm); d.write(); } ``` ### stop() Method - Finalization ```cpp void ActivityWriter::stop(const TrackData& track) { if (!mFit) return; // Session summary. mFit->data(L_SESSION) .u32(unixToFitTimestamp(track.timestamp)) .u32(unixToFitTimestamp(track.timeStart)) .u32(static_cast(track.elapsed * 1000)) // total_elapsed_time .u32(static_cast(track.duration * 1000)) // total_timer_time .u32(static_cast(track.distance * 100)) // total_distance .u16(0) // message_index .u16(static_cast(track.speedAvg * 1000)) .u16(static_cast(track.speedMax * 1000)) .u16(static_cast(track.ascent)) .u16(static_cast(track.descent)) .u16(mLapCounter) // num_laps .u8(static_cast(fit::Sport::Running)) // Activity-specific .u8(static_cast(fit::SubSport::Generic)) .u8(static_cast(track.hrAvg)) .u8(static_cast(track.hrMax)) .write(); // Activity summary. mFit->data(L_ACTIVITY) .u32(unixToFitTimestamp(track.timestamp)) .u32(static_cast(track.duration * 1000)) .u32(unixToFitTimestamp(epochToLocal(track.timestamp))) .u16(1) // num_sessions .write(); // Back-patch header data size + CRC and append the trailing file CRC. mFit->finish(); mFit.reset(); if (mFile) { mFile->flush(); mFile->close(); } saveSummary(track); // create JSON summary file } ``` ## 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 uint32_t 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, scale 5 + offset 500: (m + 500) * 5 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 int32_t ActivityWriter::degreesToSemicircles(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. **You don't write it yourself** -- `FitWriter::finish()` computes and appends it. Internally it uses the standard FIT nibble-table CRC-16 from `SDK/Fit/FitCrc.hpp`: ```cpp uint16_t fitCrcByte(uint16_t crc, uint8_t byte); // fold one byte uint16_t fitCrcUpdate(uint16_t crc, const void* d, size_t n); // fold a buffer ``` In `finish()` the encoder reopens the file read-only, folds every byte through `fitCrcUpdate` (seeded with 0), and appends the resulting CRC little-endian as the final two bytes. **CRC Properties**: - Calculated over the entire file (header + data), excluding the trailing CRC bytes - Standard FIT CRC-16 - Ensures data integrity during transfer/storage ### File Header Management The 14-byte header is also managed by `FitWriter`, not by the app: - `begin(profileVersion, protocolVersion)` writes a header *placeholder*: the data size is left at 0 and the header CRC at `0x0000` (permitted by the spec). - `finish()` back-patches the header with the actual data size (file size minus the 14-byte header) and recomputes the header CRC. This streaming approach keeps RAM use constant and means a partially recorded activity is still a valid prefix on disk if recording is interrupted before `finish()` runs (only the final size/CRC patch is missing). ### Memory Management and Performance **FitWriter Memory Usage**: - One small per-record payload buffer; no whole-file buffering - Records are streamed to the `IFile` as they are produced - Per local type, only an expected-size value and a "defined" flag are tracked **File I/O Optimization**: - Messages written sequentially as definition/data records - The only re-read of the file is the single read-only CRC pass in `finish()` **Best Practices**: - Construct one `FitWriter` per file, after the file is open - Define each local type once (redefine only for variable-size fields like strings) - Append fields in definition order; `write()` validates the payload size - Handle file errors gracefully (`FitWriter::ok()` reports IO failures) ## 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 ### Payload / Definition Mismatch **Symptom**: `data(local).write()` returns false; the field count or sizes in a data record do not match its definition. **Check**: - Append values in the **same order** as the field list passed to `defineMessage`. - Use the typed appender that matches each field's base type/size (`.u8` for `UInt8`, `.i32` for `SInt32`, `.u16` for `UInt16`, etc.). - Append all declared developer fields too, at the tail, in declared order. - For string fields, `.str(s, fieldBytes)` must use the same `fieldBytes` the definition declared. ### Memory Issues **Symptom**: Crashes or undefined behavior **Check Bounds**: ```cpp // Size fixed byte-array fields exactly; strncpy null-pads the buffer. uint8_t appId[16] = {}; strncpy(reinterpret_cast(appId), info.appID.c_str(), sizeof(appId)); mFit->data(L_DEV_ID).bytes(appId, sizeof(appId)).u8(0).write(); ``` ## Extending ActivityWriter for New Activities ### Adding a New Sport Type 1. **Use the desired `Sport` enum value** when writing the Session message. If the value you need isn't yet in `FitProfile.hpp`, add it to `enum class Sport` / `enum class SubSport` there (use the public FIT profile number -- don't invent one): ```cpp // In SDK/Fit/FitProfile.hpp enum class Sport : uint8_t { Generic = 0, Running = 1, Cycling = 2, Hiking = 17 }; ``` 2. **Write it in the Session data record**: ```cpp mFit->data(L_SESSION) // ... preceding fields ... .u8(static_cast(fit::Sport::Hiking)) .u8(static_cast(fit::SubSport::Generic)) // ... .write(); ``` ### Adding New Developer Fields 1. **Assign a developer field number** (in your app's `enum DevField`): ```cpp enum DevField : uint8_t { /* ... */ DF_NEW_FIELD = 7 }; ``` 2. **Write a field_description** in `start()` (sizes name/units exactly): ```cpp writeFieldDescription(DF_NEW_FIELD, "newField", "units", fit::BaseType::UInt16); ``` 3. **Declare it on the carrying message** in its `defineMessage(...)` developer-field list: ```cpp mFit->defineMessage(L_RECORD, fit::mesgNum(fit::MesgNum::Record), { /* standard fields */ }, {{DF_NEW_FIELD, fit::baseTypeSize(fit::BaseType::UInt16), 0}}); ``` 4. **Append the value** at the tail of the matching `data(...)` builder, in declared order: ```cpp mFit->data(L_RECORD) // ... standard fields ... .u16(calculateNewValue()) .write(); ``` ### Creating New Record Variants 1. **Add a local message type** to your `enum Local` (must be 0-15): ```cpp enum Local : uint8_t { /* ... */ L_RECORD_NEW = 14 }; ``` 2. **Define it in start()** (reuse the global Record number, choose its fields): ```cpp mFit->defineMessage(L_RECORD_NEW, fit::mesgNum(fit::MesgNum::Record), {fit::field::Record::Timestamp, fit::field::Record::HeartRate, /* add your fields */}); ``` 3. **Select and write it in addRecord()**: ```cpp if (hasNewCondition()) { mFit->data(L_RECORD_NEW) .u32(unixToFitTimestamp(record.timestamp)) .u8(static_cast(record.heartRate)) /* ... your fields, then any developer fields ... */ .write(); } ``` ### 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/` (Running, Cycling, Hiking, HRMonitor, ...) - Native FIT encoder: `Libs/Header/SDK/Fit/` (`FitWriter.hpp`, `FitProfile.hpp`, `FitBaseType.hpp`, `FitCrc.hpp`, `FitRecordCadence.hpp`) - Worked tutorial: `Docs/Tutorials/FitFiles/` (Service.cpp / Service.hpp) ### 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.