#![warn(missing_docs)]
use chrono::{DateTime, Local};
use profile::field_types::FitBaseType;
use serde::Serialize;
use std::collections::HashMap;
use std::convert;
use std::fmt;
pub mod de;
mod error;
pub mod profile;
pub use de::{from_bytes, from_reader};
pub use error::{Error, ErrorKind, Result};
#[derive(Clone, Debug, Serialize)]
pub struct FitDataRecord {
kind: profile::MesgNum,
fields: Vec<FitDataField>,
}
impl FitDataRecord {
pub fn new(kind: profile::MesgNum) -> Self {
FitDataRecord {
kind,
fields: Vec::new(),
}
}
pub fn kind(&self) -> profile::MesgNum {
self.kind
}
pub fn fields(&self) -> &[FitDataField] {
&self.fields
}
pub fn push(&mut self, field: FitDataField) {
self.fields.push(field)
}
pub fn extend(&mut self, fields: Vec<FitDataField>) {
self.fields.extend(fields)
}
pub fn into_vec(self) -> Vec<FitDataField> {
self.fields
}
}
#[derive(Clone, Debug, Serialize)]
pub struct FitDataField {
name: String,
number: u8,
developer_data_index: Option<u8>, value: Value,
units: String,
}
impl FitDataField {
pub fn new(
name: String,
number: u8,
developer_data_index: Option<u8>,
value: Value,
units: String,
) -> Self {
FitDataField {
name,
number,
developer_data_index,
value,
units,
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn number(&self) -> u8 {
self.number
}
pub fn value(&self) -> &Value {
&self.value
}
pub fn units(&self) -> &str {
&self.units
}
pub fn into_value(self) -> Value {
self.value
}
}
impl fmt::Display for FitDataField {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.units.is_empty() {
write!(f, "{}", self.value)
} else {
write!(f, "{} {}", self.value, self.units)
}
}
}
#[derive(Clone, Debug, PartialEq, PartialOrd, Serialize)]
#[serde(untagged)]
pub enum Value {
Timestamp(DateTime<Local>),
Byte(u8), Enum(u8),
SInt8(i8),
UInt8(u8),
SInt16(i16),
UInt16(u16),
SInt32(i32),
UInt32(u32),
String(String),
Float32(f32),
Float64(f64),
UInt8z(u8),
UInt16z(u16),
UInt32z(u32),
SInt64(i64),
UInt64(u64),
UInt64z(u64),
Array(Vec<Self>),
Invalid,
}
impl fmt::Display for Value {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self {
Value::Timestamp(val) => write!(f, "{}", val),
Value::Byte(val) => write!(f, "{}", val),
Value::Enum(val) => write!(f, "{}", val),
Value::SInt8(val) => write!(f, "{}", val),
Value::UInt8(val) => write!(f, "{}", val),
Value::UInt8z(val) => write!(f, "{}", val),
Value::SInt16(val) => write!(f, "{}", val),
Value::UInt16(val) => write!(f, "{}", val),
Value::UInt16z(val) => write!(f, "{}", val),
Value::SInt32(val) => write!(f, "{}", val),
Value::UInt32(val) => write!(f, "{}", val),
Value::UInt32z(val) => write!(f, "{}", val),
Value::SInt64(val) => write!(f, "{}", val),
Value::UInt64(val) => write!(f, "{}", val),
Value::UInt64z(val) => write!(f, "{}", val),
Value::Float32(val) => write!(f, "{}", val),
Value::Float64(val) => write!(f, "{}", val),
Value::String(val) => write!(f, "{}", val),
Value::Array(vals) => write!(f, "{:?}", vals), Value::Invalid => write!(f, ""),
}
}
}
impl convert::TryInto<f64> for Value {
type Error = error::Error;
fn try_into(self) -> Result<f64> {
match self {
Value::Timestamp(val) => Ok(val.timestamp() as f64),
Value::Byte(val) => Ok(val as f64),
Value::Enum(val) => Ok(val as f64),
Value::SInt8(val) => Ok(val as f64),
Value::UInt8(val) => Ok(val as f64),
Value::UInt8z(val) => Ok(val as f64),
Value::SInt16(val) => Ok(val as f64),
Value::UInt16(val) => Ok(val as f64),
Value::UInt16z(val) => Ok(val as f64),
Value::SInt32(val) => Ok(val as f64),
Value::UInt32(val) => Ok(val as f64),
Value::UInt32z(val) => Ok(val as f64),
Value::SInt64(val) => Ok(val as f64),
Value::UInt64(val) => Ok(val as f64),
Value::UInt64z(val) => Ok(val as f64),
Value::Float32(val) => Ok(val as f64),
Value::Float64(val) => Ok(val),
Value::String(_) => {
Err(ErrorKind::ValueError(format!("cannot convert {} into an f64", self)).into())
}
Value::Array(_) => {
Err(ErrorKind::ValueError(format!("cannot convert {} into an f64", self)).into())
}
Value::Invalid => Err(ErrorKind::ValueError(
"cannot convert an invalid value into an f64".to_string(),
)
.into()),
}
}
}
impl convert::TryInto<i64> for Value {
type Error = error::Error;
fn try_into(self) -> Result<i64> {
match self {
Value::Timestamp(val) => Ok(val.timestamp()),
Value::Byte(val) => Ok(val as i64),
Value::Enum(val) => Ok(val as i64),
Value::SInt8(val) => Ok(val as i64),
Value::UInt8(val) => Ok(val as i64),
Value::UInt8z(val) => Ok(val as i64),
Value::SInt16(val) => Ok(val as i64),
Value::UInt16(val) => Ok(val as i64),
Value::UInt16z(val) => Ok(val as i64),
Value::SInt32(val) => Ok(val as i64),
Value::UInt32(val) => Ok(val as i64),
Value::UInt32z(val) => Ok(val as i64),
Value::SInt64(val) => Ok(val),
Value::UInt64(val) => Ok(val as i64),
Value::UInt64z(val) => Ok(val as i64),
Value::Float32(_) => {
Err(ErrorKind::ValueError(format!("cannot convert {} into an i64", self)).into())
}
Value::Float64(_) => {
Err(ErrorKind::ValueError(format!("cannot convert {} into an i64", self)).into())
}
Value::String(_) => {
Err(ErrorKind::ValueError(format!("cannot convert {} into an i64", self)).into())
}
Value::Array(_) => {
Err(ErrorKind::ValueError(format!("cannot convert {} into an i64", self)).into())
}
Value::Invalid => Err(ErrorKind::ValueError(
"cannot convert an invalid value into an i64".to_string(),
)
.into()),
}
}
}
impl convert::TryInto<i64> for &Value {
type Error = error::Error;
fn try_into(self) -> Result<i64> {
match self {
Value::Timestamp(val) => Ok(val.timestamp()),
Value::Byte(val) => Ok(*val as i64),
Value::Enum(val) => Ok(*val as i64),
Value::SInt8(val) => Ok(*val as i64),
Value::UInt8(val) => Ok(*val as i64),
Value::UInt8z(val) => Ok(*val as i64),
Value::SInt16(val) => Ok(*val as i64),
Value::UInt16(val) => Ok(*val as i64),
Value::UInt16z(val) => Ok(*val as i64),
Value::SInt32(val) => Ok(*val as i64),
Value::UInt32(val) => Ok(*val as i64),
Value::UInt32z(val) => Ok(*val as i64),
Value::SInt64(val) => Ok(*val),
Value::UInt64(val) => Ok(*val as i64),
Value::UInt64z(val) => Ok(*val as i64),
Value::Float32(_) => {
Err(ErrorKind::ValueError(format!("cannot convert {} into an i64", self)).into())
}
Value::Float64(_) => {
Err(ErrorKind::ValueError(format!("cannot convert {} into an i64", self)).into())
}
Value::String(_) => {
Err(ErrorKind::ValueError(format!("cannot convert {} into an i64", self)).into())
}
Value::Array(_) => {
Err(ErrorKind::ValueError(format!("cannot convert {} into an i64", self)).into())
}
Value::Invalid => Err(ErrorKind::ValueError(
"cannot convert an invalid value into an i64".to_string(),
)
.into()),
}
}
}
#[derive(Clone, Debug, Serialize)]
pub struct ValueWithUnits {
value: Value,
units: String,
}
impl ValueWithUnits {
pub fn new(value: Value, units: String) -> Self {
ValueWithUnits { value, units }
}
}
impl convert::From<FitDataField> for ValueWithUnits {
fn from(field: FitDataField) -> Self {
ValueWithUnits::new(field.value, field.units)
}
}
impl fmt::Display for ValueWithUnits {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.units.is_empty() {
write!(f, "{}", self.value)
} else {
write!(f, "{} {}", self.value, self.units)
}
}
}
#[derive(Debug)]
pub struct DeveloperFieldDescription {
developer_data_index: u8,
field_definition_number: u8,
fit_base_type_id: FitBaseType,
field_name: String,
scale: f64,
offset: f64,
units: String,
}
impl TryFrom<&Vec<FitDataField>> for DeveloperFieldDescription {
type Error = ErrorKind;
fn try_from(
fields: &Vec<FitDataField>,
) -> std::result::Result<DeveloperFieldDescription, error::ErrorKind> {
let mut name_to_value = HashMap::new();
for field in fields {
name_to_value.insert(field.name.clone(), field.value.clone());
}
let developer_data_index = if let Value::UInt8(developer_data_index) = name_to_value
.get("developer_data_index")
.ok_or(ErrorKind::ValueError(
"developer_data_index is mandatory".to_string(),
))? {
*developer_data_index
} else {
return Err(ErrorKind::ValueError(
"developer_data_index must be u8".to_string(),
));
};
let field_definition_number = if let Value::UInt8(field_definition_number) = name_to_value
.get("field_definition_number")
.ok_or(ErrorKind::ValueError(
"field_definition_number is mandatory".to_string(),
))? {
*field_definition_number
} else {
return Err(ErrorKind::ValueError(
"field_definition_number must be u8".to_string(),
));
};
let fit_base_type_id = if let Value::String(fit_base_type_id) = name_to_value
.get("fit_base_type_id")
.ok_or(ErrorKind::ValueError(
"fit_base_type_id is mandatory".to_string(),
))? {
FitBaseType::from(fit_base_type_id as &str)
} else {
return Err(ErrorKind::ValueError(
"fit_base_type_id must be string".to_string(),
));
};
let field_name = if let Value::String(field_name) = name_to_value
.get("field_name")
.unwrap_or(&Value::String(format!(
"unknown_developer_field_{}",
field_definition_number
))) {
field_name.clone()
} else {
return Err(ErrorKind::ValueError(
"field_name must be string".to_string(),
));
};
let scale =
if let Value::UInt8(scale) = name_to_value.get("scale").unwrap_or(&Value::UInt8(1u8)) {
*scale as f64
} else {
return Err(ErrorKind::ValueError("scale must be u8".to_string()));
};
let offset = if let Value::SInt8(offset) =
name_to_value.get("offset").unwrap_or(&Value::SInt8(0i8))
{
*offset as f64
} else {
return Err(ErrorKind::ValueError("offset must be i8".to_string()));
};
let units = if let Value::String(units) = name_to_value
.get("units")
.unwrap_or(&Value::String(String::new()))
{
units.clone()
} else {
return Err(ErrorKind::ValueError("units must be string".to_string()));
};
Ok(DeveloperFieldDescription {
developer_data_index,
field_definition_number,
fit_base_type_id,
field_name,
scale,
offset,
units,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
#[test]
fn parse_activity() {
let data = include_bytes!("../tests/fixtures/Activity.fit").to_vec();
let fit_data = from_bytes(&data).unwrap();
assert_eq!(fit_data.len(), 22);
}
#[test]
fn parse_developer_data() {
let data = include_bytes!("../tests/fixtures/DeveloperData.fit").to_vec();
let fit_data = from_bytes(&data).unwrap();
assert_eq!(fit_data.len(), 6);
assert_eq!(fit_data[3].fields().len(), 5);
assert_eq!(fit_data[3].fields()[4].name(), "doughnuts_earned");
assert_eq!(fit_data[3].fields()[4].value(), &Value::SInt8(1));
assert_eq!(fit_data[3].fields()[4].units(), "doughnuts");
}
#[test]
fn parse_monitoring_file() {
let data = include_bytes!("../tests/fixtures/MonitoringFile.fit").to_vec();
let fit_data = from_bytes(&data).unwrap();
assert_eq!(fit_data.len(), 355);
}
#[test]
fn parse_settings() {
let data = include_bytes!("../tests/fixtures/Settings.fit").to_vec();
let fit_data = from_bytes(&data).unwrap();
assert_eq!(fit_data.len(), 3);
}
#[test]
fn parse_weight_scale_multi_user() {
let data = include_bytes!("../tests/fixtures/WeightScaleMultiUser.fit").to_vec();
let fit_data = from_bytes(&data).unwrap();
assert_eq!(fit_data.len(), 7);
}
#[test]
fn parse_weight_scale_single_user() {
let data = include_bytes!("../tests/fixtures/WeightScaleSingleUser.fit").to_vec();
let fit_data = from_bytes(&data).unwrap();
assert_eq!(fit_data.len(), 6);
}
#[test]
fn parse_workout_custom_target_values() {
let data = include_bytes!("../tests/fixtures/WorkoutCustomTargetValues.fit").to_vec();
let fit_data = from_bytes(&data).unwrap();
assert_eq!(fit_data.len(), 6);
}
#[test]
fn parse_workout_individual_steps() {
let data = include_bytes!("../tests/fixtures/WorkoutIndividualSteps.fit").to_vec();
let fit_data = from_bytes(&data).unwrap();
assert_eq!(fit_data.len(), 6);
}
#[test]
fn parse_workout_repeat_greater_than_step() {
let data = include_bytes!("../tests/fixtures/WorkoutRepeatGreaterThanStep.fit").to_vec();
let fit_data = from_bytes(&data).unwrap();
assert_eq!(fit_data.len(), 7);
}
#[test]
fn parse_workout_repeat_steps() {
let data = include_bytes!("../tests/fixtures/WorkoutRepeatSteps.fit").to_vec();
let fit_data = from_bytes(&data).unwrap();
assert_eq!(fit_data.len(), 7);
}
#[test]
fn parse_garmin_fenix_5_bike() {
let data = include_bytes!("../tests/fixtures/garmin-fenix-5-bike.fit").to_vec();
let fit_data = from_bytes(&data).unwrap();
assert_eq!(fit_data.len(), 143);
}
#[test]
fn parse_sample_mulitple_header() {
let data = include_bytes!("../tests/fixtures/sample_mulitple_header.fit").to_vec();
let fit_data = from_bytes(&data).unwrap();
assert_eq!(fit_data.len(), 3023);
}
#[test]
fn parse_activity_with_hrv() {
let data = include_bytes!("../tests/fixtures/hrv-activity.fit").to_vec();
let fit_data = from_bytes(&data).unwrap();
assert_eq!(fit_data.len(), 2260);
let field = fit_data
.into_iter()
.find(|rec| rec.kind == profile::MesgNum::Hrv)
.map(|rec| rec.fields)
.expect("We should have at least one HRV message")
.into_iter()
.find(|fld| fld.name == "time")
.expect("We should have at least one time field");
assert_eq!(
field.value,
Value::Array(vec![
Value::Float64(0.467),
Value::Float64(0.464),
Value::Invalid,
Value::Invalid,
Value::Invalid
])
);
}
#[test]
fn parse_with_header_crc_set_to_zero() {
let mut data = include_bytes!("../tests/fixtures/garmin-fenix-5-bike.fit").to_vec();
let leng = data.len();
data[12] = 0x00;
data[13] = 0x00;
match de::from_bytes(&data) {
Ok(_) => panic!(
"This test should fail without the data CRC value being recomputed to include the header."
),
Err(e) => match *e {
ErrorKind::InvalidCrc(..) => {}
_ => panic!("Incorrect error returned {:?}", e),
},
}
data[leng - 2] = 0x58;
data[leng - 1] = 0x65;
let fit_data = de::from_bytes(&data).unwrap();
assert_eq!(fit_data.len(), 143);
}
#[test]
fn parse_with_invalid_header_crc_with_options() {
let mut data = include_bytes!("../tests/fixtures/MonitoringFile.fit").to_vec();
data[12] = 0xFF;
data[13] = 0xFF;
let mut options = HashSet::new();
match de::from_bytes_with_options(&data, &options) {
Ok(_) => panic!("This test should fail without the SkipHeaderCrcValidation option."),
Err(e) => match *e {
ErrorKind::InvalidCrc(..) => {}
_ => panic!("Incorrect error returned {:?}", e),
},
}
options.insert(de::DecodeOption::SkipHeaderCrcValidation);
let fit_data = de::from_bytes_with_options(&data, &options).unwrap();
assert_eq!(fit_data.len(), 355);
}
#[test]
fn parse_with_invalid_data_crc_with_options() {
let mut data = include_bytes!("../tests/fixtures/MonitoringFile.fit").to_vec();
let leng = data.len();
data[leng - 2] = 0xFF;
data[leng - 1] = 0xFF;
let mut options = HashSet::new();
match de::from_bytes_with_options(&data, &options) {
Ok(_) => panic!("This test should fail without the SkipDataCrcValidation option."),
Err(e) => match *e {
ErrorKind::InvalidCrc(..) => {}
_ => panic!("Incorrect error returned {:?}", e),
},
}
options.insert(de::DecodeOption::SkipDataCrcValidation);
let fit_data = de::from_bytes_with_options(&data, &options).unwrap();
assert_eq!(fit_data.len(), 355);
}
}