rustyfit

Crates.iorustyfit
lib.rsrustyfit
version0.2.2
created_at2025-04-10 07:44:38.798956+00
updated_at2025-07-22 06:28:29.086814+00
descriptionThis project hosts the Rust implementation for The Flexible and Interoperable Data Transfer (FIT) Protocol
homepage
repositoryhttps://github.com/muktihari/rustyfit
max_upload_size
id1627746
size2,898,428
Mukti (muktihari)

documentation

README

RustyFIT

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

Rewrite of FIT SDK for Go in Rust.

Current State

This project serves as an exercise for me to learn Rust. I believe the fastest way to learn something new is by reinventing the wheel or rewriting something that already exists. Although this is a learning project, this library generally works, is usable, and quite fast.

Missing features, test completeness, and more robust documentation may be added later through release iteration.

Usage

Decoding

Decode

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

use rustyfit::{
    Decoder,
    profile::{mesgdef, typedef},
};
use std::{fs::File, io::BufReader};

fn main() {
    let name = "Activity.fit";
    let f = File::open(name).unwrap();
    let br = BufReader::new(f);
    let mut dec = Decoder::new(br);

    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 {
            println!("file type: {}", typedef::File(field.value.as_u8()));
        }
    }
    // # Output:
    // file_header's data_size: 94080
    // messages count: 3611
    // file type: activity
}

Decode with Closure

Decoder's decode_fn allow us to retrieve message definition or message data event as soon as it is being decoded. This way, users can have fine-grained control on how to interact with the data.

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

fn main() {
    let name = "Activity.fit";
    let f = File::open(name).unwrap();
    let br = BufReader::new(f);
    let mut dec = Decoder::new(br);

    dec.decode_fn(|event| match event {
        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::MessageDefinition(_) => {}
    })
    .unwrap();

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

DecoderBuilder

Create Decoder instance with options using DecoderBuilder.

let mut dec: Decoder = DecoderBuilder::new(br)
        .checksum(false)
        .expand_components(false)
        .build();

Encoding

Encode

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

use std::{
    fs::File,
    io::{BufWriter, Write},
};

use rustyfit::{
    Encoder,
    profile::{
        ProfileType, mesgdef,
        typedef::{self},
    },
    proto::{FIT, Field, Message, Value},
};

fn main() {
    let fout_name = "output.fit";
    let fout = File::create(fout_name).unwrap();
    let bw = BufWriter::new(fout);
    let mut enc = Encoder::new(bw);

    let mut fit = FIT {
        messages: vec![
            Message {
                num: typedef::MesgNum::FILE_ID,
                fields: vec![
                    Field {
                        num: mesgdef::FileId::MANUFACTURER,
                        profile_type: ProfileType::MANUFACTURER,
                        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::Uint16(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).unwrap();
    bw.flush().unwrap();
}

Encode using mesgdef module

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

use std::{
    fs::File,
    io::{BufWriter, Write},
};

use rustyfit::{
    Encoder,
    profile::{
        mesgdef,
        typedef::{self},
    },
    proto::{FIT, Message},
};

fn main() {
    let fout_name = "output.fit";
    let fout = File::create(fout_name).unwrap();
    let bw = BufWriter::new(fout);
    let mut enc = Encoder::new(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).unwrap();
    bw.flush().unwrap();
}

EncoderBuilder

Create Encoder instance with options using EncoderBuilder.

let mut enc: Encoder = EncoderBuilder::new(&mut bw)
        .endianness(Endianness::BigEndian)
        .protocol_version(ProtocolVersion::V2)
        .header_option(HeaderOption::Compressed(3))
        .omit_invalid_value(false)
        .build();
Commit count: 9

cargo fmt