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 thesemesgdefbuilding 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
profilemodule represents FIT Global Profile containing types and messages generated from Profile.xlsx. - proto
- The
protomodule provides FIT Protocol low level representation. - semconv
- Semicircles/Degrees converter.
Structs§
- Decoder
- Decoder for decoding FIT file.
- Decoder
Builder - Build Decoder with some options.
- Encoder
- Encoder for encoding FIT file.
- Encoder
Builder - Build Encoder with some options.
- Stream
Decoder - Creates a
Streamfrom a mutably borrowedDecoderfor streaming decoding. - Stream
Encoder - A
Streamfrom a mutably borrowedEncoderfor streaming encoding.
Enums§
- Decoder
Error - Decoder Error
- Decoder
Event - Event is FIT segments encountered by the
StreamDecoder. - Encoder
Error - Encoder Error
- Endianness
- Byte order used when encoding FIT file. Default: LittleEndian.
- Field
Validation Error - Error occurs during value
fieldsordeveloper_fieldsvalidation. The context depends on where the validation takes place. - Header
Option - HeaderOption to pick when encoding FIT file, this will optimize the size of the resulting FIT file.
- Message
Validation Error - Error occurs during message validation
Traits§
- Streaming
Iterator - An Iterator-like trait that return borrowed Item rather than owned Item.
StreamingIterator is lazy and do nothing unless
next()is called.