Skip to main content

Crate rustyfit

Crate rustyfit 

Source
Expand description

A stateless and static-friendly #![no_std] library to decode and encode Garmin FIT files, supporting FIT Protocol V2.

The Flexible and Interoperable Data Transfer (FIT) Protocol is a protocol developed by Garmin for storing and sharing data originating from sports, fitness, and health devices. Activities recorded using devices such as smartwatch and cycling computer are now mostly in a FIT file format (*.fit).

This library is a rewrite of FIT SDK for Go and is designed to run on baremetal Rust, where performance and memory efficiency is carefully considered.

§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 simplicity and a wider audience, since #![no_std] is platform-dependent.

§

§Decoding

Decoder‘s decode method allows us to interact with FIT files directly through their original protocol messages’ structure. This method can be invoked multiple times to decode chained FIT file until it return Ok(None) or Err(err).

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 = "tests/data/from_official_sdk/Activity.fit";
    let f = File::open(name)?;
    let br = BufReader::new(f);
    let mut reader = FromStd::new(br);

    let mut dec = Decoder::new();

    let fit = match dec.decode(&mut reader)? {
        Some(fit) => fit,
        None => {
            // First decode call to reader should be `Ok` or `Err`.
            // Except, reader is already empty to begin with.
            return Err(Box::from("empty reader"));
        }
    };

    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
}
§Streaming Decoding

StreamDecoder allows 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 efficiently. And since this is lazily evaluated, users can decide when to stop without required to read the whole reader.

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

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

    let mut dec = Decoder::new();              // stateless and static-friendly
    let mut stream = dec.stream(&mut reader);  // stateful but small since it borrows Decoder.

    while let Some(event) = stream.next() {
        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
}

Users can also use discard() to discard this current FIT sequence and direct the StreamDecoder to point to next FIT sequence in the reader. If desired, users can also stop the process entirely.

    while let Some(event) = stream.next() {
        if let DecoderEvent::Message(mesg) = event? {
            if mesg.num == typedef::MesgNum::FILE_ID {
                // Let's say we just want to decode Activity file,
                let file_id = mesgdef::FileId::from(mesg);
                if file_id.r#type != typedef::File::ACTIVITY {
                    stream.discard()?; // discard this sequence
                    continue;
                }
            }
            // It's an Activity File!
        }
    }
§DecoderBuilder

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

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

These associated functions and method are const fn, so we can use it to declare a static variable as long as we wrap it with a lock, e.g. Mutex. This is useful on microcrontrollers where RAM is only hundred KBs.

§

§Encoding

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 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()
    };

    let fout_name = "output.fit";
    let fout = File::create(fout_name)?;
    let mut bw = BufWriter::new(fout);
    let mut writer = FromStd::new(&mut bw);

    let mut enc = Encoder::new();
    enc.encode(&mut writer, &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 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()
    };
     
    let fout_name = "output.fit";
    let fout = File::create(fout_name)?;
    let mut bw = BufWriter::new(fout);
    let mut writer = FromStd::new(&mut bw);

    let mut enc = Encoder::new();
    enc.encode(&mut writer, &mut fit)?;
    bw.flush()?;

    Ok(())
}
§Streaming Encoding

StreamEncoder allows us to encode in streaming fashion. Write each message directly without retain them first in the memory. This is useful when the device is the one who produce the data such as smartwatch, cycling computer or other health devices.

use embedded_io_adapters::std::FromStd;
use rustyfit::{Encoder, profile::{mesgdef, typedef}, proto::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 writer = FromStd::new(&mut bw);

    let mut enc = Encoder::new();             // stateless and static-friendly
    let mut stream = enc.stream(&mut writer); // stateful but small since it borrows Encoder.

    stream.write_message(&mut {
        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)
    })?;

    stream.write_message(&mut {
        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)
    })?;

    stream.finish()?;
    bw.flush()?;

    Ok(())
}

NOTE:

  • For #![no_std] on MCU with only few hundred KBs of RAM, we recommend allocating a Message once and reuse it instead of using these mesgdef building blocks which might yield few KBs stack memory. MCU may only has 2KB or less stack memory configuration.

  • For std, you don’t need to worry about this, on Linux for example, stack can have a range from 2MB to 8MB, which is abundant.

§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();

These associated functions and method are const fn, so we can use it to declare a static variable as long as we wrap it with a lock, e.g. Mutex. This is useful on microcrontrollers where RAM is only hundred KBs.

Modules§

profile
The profile module represents FIT Global Profile containing types and messages generated from Profile.xlsx.
proto
The proto module provides FIT Protocol low level representation.
semconv
Semicircles/Degrees converter.

Structs§

Decoder
Decoder for decoding FIT file.
DecoderBuilder
Build Decoder with some options.
Encoder
Encoder for encoding FIT file.
EncoderBuilder
Build Encoder with some options.
StreamDecoder
Creates a Stream from a mutably borrowed Decoder for streaming decoding.
StreamEncoder
A Stream from a mutably borrowed Encoder for streaming encoding.

Enums§

DecoderError
Decoder Error
DecoderEvent
Event is FIT segments encountered by the StreamDecoder.
EncoderError
Encoder Error
Endianness
Byte order used when encoding FIT file. Default: LittleEndian.
FieldValidationError
Error occurs during value fields or developer_fields validation. The context depends on where the validation takes place.
HeaderOption
HeaderOption to pick when encoding FIT file, this will optimize the size of the resulting FIT file.
MessageValidationError
Error occurs during message validation

Traits§

StreamingIterator
An Iterator-like trait that return borrowed Item rather than owned Item. StreamingIterator is lazy and do nothing unless next() is called.