rustyfit 0.7.0

The #![no_std] Rust implementation of The Flexible and Interoperable Data Transfer (FIT) Protocol for decoding and encoding Garmin FIT files, supporting FIT Protocol V2.
Documentation

RustyFIT

GitHub Workflow Status docs.rs Crates.io Version Crates.io Downloads Profile Version

The #![no_std] Rust implementation of The Flexible and Interoperable Data Transfer (FIT) Protocol for decoding and encoding Garmin FIT files, supporting FIT Protocol V2.

This library is a rewrite of FIT SDK for Go and is designed to run on baremetal Rust.

Current State

At the moment, I can't make this library heapless without complicating the code. I have no elegant solution for pure baremetal without dynamic memory allocation. That being said, this library is efficient enough to work on resource-constrained environments. I've tested it on a #![no_std] #![no_main] program to decode a FIT file (statically embedded) and print some of its Session's fields to stdout with a custom global allocator having less than 50 KB heap size and it works like a charm.

Usage

For #![no_std], you need to provide #[global_allocator] since this library requires allocation.

For std, you need to wrap std::io's Read or Write with embedded_io_adapters::std:FromStd. We will provide examples in std for a broad audience and simplicity.

Decoding

Decode

Decoder's decode allows us to interact with FIT files directly through their original protocol messages' structure.

NOTE: First call will return either Ok(Some(fit)) or Err(err), never Ok(None). On next call, it may return Ok(None) to indicate that no more FIT sequence in the file.

use embedded_io_adapters::std::FromStd;
use rustyfit::{Decoder, profile::{mesgdef, typedef}, proto::Value};
use std::{error::Error, fs::File, io::BufReader};

fn main() -> Result<(), Box<dyn Error>> {
    let name = "Activity.fit";
    let f = File::open(name)?;
    let br = BufReader::new(f);
    let mut dec = Decoder::new(FromStd::new(br));

    // SAFETY: First decode call is either Ok(Some(fit)) or Err(err), never Ok(None).
    let fit = dec.decode()?.unwrap(); 

    println!("file_header's data_size: {}", fit.file_header.data_size);
    println!("messages count: {}", fit.messages.len());
    for field in &fit.messages[0].fields {
        // first message: file_id
        if field.num == mesgdef::FileId::TYPE
            && let Value::Uint8(v) = field.value
        {
            println!("file type: {}", typedef::File(v));
        }
    }
    
    Ok(())
    
    // # Output:
    // file_header's data_size: 94080
    // messages count: 3611
    // file type: activity
}

The decode method can be invoked multiple times to decode chained FIT file until it return Ok(None) or Err(err). We can decode chained FIT file using while let, e.g:

    while let Some(fit) = dec.decode()? {
        println!("file_header's data_size: {}", fit.file_header.data_size);
        println!("messages count: {}", fit.messages.len());
        for field in &fit.messages[0].fields {
            // first message: file_id
            if field.num == mesgdef::FileId::TYPE
                && let Value::Uint8(v) = field.value
            {
                println!("file type: {}", typedef::File(v));
            }
        }
    }

Decode with Closure

Decoder's decode_with allow us to retrieve event data (FileHeader, MessageDefinition, Message, CRC) as soon as it is being decoded. This way, users can have fine-grained control on how to interact with the data.

use embedded_io_adapters::std::FromStd;
use rustyfit::{Decoder, DecoderEvent, profile::{mesgdef, typedef}};
use std::{error::Error, fs::File, io::BufReader};

fn main() -> Result<(), Box<dyn Error>> {
    let name = "Activity.fit";
    let f = File::open(name)?;
    let br = BufReader::new(f);
    let mut dec = Decoder::new(FromStd::new(br));

    dec.decode_with(|event| match event {
        DecoderEvent::FileHeader(_) => {},
        DecoderEvent::MessageDefinition(_) => {},
        DecoderEvent::Message(mesg) => {
            if mesg.num == typedef::MesgNum::SESSION {
                // Convert mesg into Session struct
                let ses = mesgdef::Session::from(mesg);
                println!(
                    "session:\n start_time: {}\n sport: {}\n num_laps: {}",
                    ses.start_time.0, ses.sport, ses.num_laps
                );
            }
        }
        DecoderEvent::Crc(_) => {}
    })?;
    
    Ok(())

    // # Output
    // session:
    //  start_time: 995749880
    //  sport: stand_up_paddleboarding
    //  num_laps: 1
}

The decode_with method can be invoked multiple times to decode chained FIT file until it return Ok(false) or Err(err).

    while dec.decode_with(|event| {
        if let DecoderEvent::Message(mesg) = event {
            if mesg.num == typedef::MesgNum::SESSION {
                // Convert mesg into Session struct
                let ses = mesgdef::Session::from(mesg);
                println!(
                    "session:\n start_time: {}\n sport: {}\n num_laps: {}",
                    ses.start_time.0, ses.sport, ses.num_laps
                );
            }
        }
    })? {}

DecoderBuilder

Create Decoder instance with options using Decoder::builder() or DecoderBuilder::new().

let mut dec = Decoder::builder()
        .checksum(false)
        .expand_components(false)
        .build(reader);

Encoding

Encode

Here is the example of manually encode FIT protocol using this library to give the idea how it works.

use embedded_io_adapters::std::FromStd;
use rustyfit::{Encoder, profile::{ProfileType, mesgdef, typedef}, proto::{FIT, Field, Message, Value}};
use std::{error::Error, fs::File, io::{BufWriter, Write}};

fn main() -> Result<(), Box<dyn Error>> {
    let fout_name = "output.fit";
    let fout = File::create(fout_name)?;
    let mut bw = BufWriter::new(fout);
    let mut enc = Encoder::new(FromStd::new(&mut bw));

    let mut fit = FIT {
        messages: vec![
            Message {
                num: typedef::MesgNum::FILE_ID,
                fields: vec![
                    Field {
                        num: mesgdef::FileId::MANUFACTURER,
                        profile_type: ProfileType::MANUFACTURER, // or ProfileType::UINT16 is also valid
                        value: Value::Uint16(typedef::Manufacturer::GARMIN.0),
                        is_expanded: false,
                    },
                    Field {
                        num: mesgdef::FileId::PRODUCT,
                        profile_type: ProfileType::UINT16,
                        value: Value::Uint16(typedef::GarminProduct::FENIX8_SOLAR.0),
                        is_expanded: false,
                    },
                    Field {
                        num: mesgdef::FileId::TYPE,
                        profile_type: ProfileType::UINT8,
                        value: Value::Uint8(typedef::File::ACTIVITY.0),
                        is_expanded: false,
                    },
                ],
                ..Default::default()
            },
            Message {
                num: typedef::MesgNum::RECORD,
                fields: vec![
                    Field {
                        num: mesgdef::Record::DISTANCE,
                        profile_type: ProfileType::UINT32,
                        value: Value::Uint32(100 * 100), // 100 m
                        is_expanded: false,
                    },
                    Field {
                        num: mesgdef::Record::HEART_RATE,
                        profile_type: ProfileType::UINT8,
                        value: Value::Uint8(70), // 70 bpm
                        is_expanded: false,
                    },
                    Field {
                        num: mesgdef::Record::SPEED,
                        profile_type: ProfileType::UINT16,
                        value: Value::Uint16(2 * 1000), // 2 m/s
                        is_expanded: false,
                    },
                ],
                ..Default::default()
            },
        ],
        ..Default::default()
    };

    enc.encode(&mut fit)?;
    bw.flush()?;

    Ok(())
}

Encode using mesgdef module

Alternatively, users can create messages using the mesgdef module for convenience.

use embedded_io_adapters::std::FromStd;
use rustyfit::{Encoder, profile::{mesgdef, typedef}, proto::{FIT, Message}};
use std::{error::Error, fs::File, io::{BufWriter, Write}};

fn main() -> Result<(), Box<dyn Error>> {
    let fout_name = "output.fit";
    let fout = File::create(fout_name)?;
    let mut bw = BufWriter::new(fout);
    let mut enc = Encoder::new(FromStd::new(&mut bw));

    let mut fit = FIT {
        messages: vec![
            {
                let mut file_id = mesgdef::FileId::new();
                file_id.manufacturer = typedef::Manufacturer::GARMIN;
                file_id.product = typedef::GarminProduct::FENIX8_SOLAR.0;
                file_id.r#type = typedef::File::ACTIVITY;
                Message::from(file_id)
            },
            {
                let mut record = mesgdef::Record::new();
                record.distance = 100 * 100; // 100 m
                record.heart_rate = 70; // 70 bpm
                record.speed = 2 * 1000; // 2 m/s
                Message::from(record)
            },
        ],
        ..Default::default()
    };

    enc.encode(&mut fit)?;
    bw.flush()?;

    Ok(())
}

EncoderBuilder

Create Encoder instance with options using Encoder::builder() or EncoderBuilder::new().

let mut enc = Encoder::builder()
        .endianness(Endianness::BigEndian)
        .protocol_version(ProtocolVersion::V2)
        .header_option(HeaderOption::Compressed(3))
        .build(writer);

License

This library is licensed under the BSD 3-Clause License.

The file fitgen/Profile.xlsx and all files under rustyfit/tests/data/from_official_sdk/* are licensed under the FIT SDK License. These files are used for code generation and testing only.

The FIT Protocol and FIT file format are proprietary to Garmin and are licensed under the FIT Protocol License. Use of this library may require compliance with the FIT Protocol License.

This project is not affiliated with or endorsed by Garmin. It is provided "as is" without warranty. Users are responsible for complying with all applicable licenses and laws.