fitparser/profile/
mod.rs

1//! Defines the FIT profile used to convert raw parser output into final values that can be
2//! interpreted without using the FIT profile.
3use crate::de::DecodeOption;
4use crate::error::{ErrorKind, Result};
5use crate::{FitDataField, Value};
6use chrono::{DateTime, Duration, Local, NaiveDate, TimeZone};
7use std::collections::{HashMap, HashSet};
8use std::convert::TryInto;
9
10pub mod field_types;
11pub use field_types::{get_field_variant_as_string, FieldDataType, MesgNum};
12
13pub mod decode;
14pub use decode::VERSION;
15
16impl Value {
17    /// Convert the value into a vector of bytes
18    fn to_ne_bytes(&self) -> Vec<u8> {
19        match self {
20            Value::Byte(val) => vec![*val],
21            Value::Enum(val) => vec![*val],
22            Value::SInt8(val) => vec![*val as u8],
23            Value::UInt8(val) => vec![*val],
24            Value::SInt16(val) => val.to_ne_bytes().to_vec(),
25            Value::UInt16(val) => val.to_ne_bytes().to_vec(),
26            Value::SInt32(val) => val.to_ne_bytes().to_vec(),
27            Value::UInt32(val) => val.to_ne_bytes().to_vec(),
28            Value::String(val) => val.as_bytes().to_vec(),
29            Value::Timestamp(val) => val.timestamp().to_ne_bytes().to_vec(),
30            Value::Float32(val) => val.to_ne_bytes().to_vec(),
31            Value::Float64(val) => val.to_ne_bytes().to_vec(),
32            Value::UInt8z(val) => vec![*val],
33            Value::UInt16z(val) => val.to_ne_bytes().to_vec(),
34            Value::UInt32z(val) => val.to_ne_bytes().to_vec(),
35            Value::SInt64(val) => val.to_ne_bytes().to_vec(),
36            Value::UInt64(val) => val.to_ne_bytes().to_vec(),
37            Value::UInt64z(val) => val.to_ne_bytes().to_vec(),
38            Value::Array(vals) => vals.iter().flat_map(|v| v.to_ne_bytes()).collect(),
39            Value::Invalid => Vec::new(),
40        }
41    }
42}
43
44/// Stores the timestamp offset from the FIT reference date in seconds
45#[derive(Debug, Copy, Clone)]
46pub enum TimestampField {
47    /// TimestampField generated from Value of type FieldDataType::LocalDateTime
48    Local(i64),
49    /// TimestampField generated from Value of type FieldDataType::DateTime
50    Utc(i64),
51}
52
53impl TimestampField {
54    /// Return the timestamp as an i64
55    pub fn as_i64(&self) -> i64 {
56        match self {
57            Self::Local(value) => *value,
58            Self::Utc(value) => *value,
59        }
60    }
61
62    /// converts offset value into a proper timestamp
63    fn to_date_time(self) -> DateTime<Local> {
64        // reference date defined in FIT profile, it's either in UTC or local TZ
65        let ref_date = NaiveDate::from_ymd_opt(1989, 12, 31)
66            .and_then(|d: NaiveDate| d.and_hms_opt(0, 0, 0))
67            .unwrap();
68        match self {
69            Self::Local(value) => {
70                TimeZone::from_local_datetime(&Local, &ref_date).unwrap() + Duration::seconds(value)
71            }
72            Self::Utc(value) => {
73                TimeZone::from_utc_datetime(&Local, &ref_date) + Duration::seconds(value)
74            }
75        }
76    }
77}
78
79impl From<TimestampField> for Value {
80    fn from(timestamp: TimestampField) -> Value {
81        Value::Timestamp(timestamp.to_date_time())
82    }
83}
84
85/// Extracts a component of a defined size from the provided byte slice
86/// Returns an updated slice, new starting offset and the extracted value.
87fn extract_component(input: &[u8], mut offset: usize, nbits: usize) -> ((&[u8], usize), Value) {
88    let bit_mask = [1u8, 2u8, 4u8, 8u8, 16u8, 32u8, 64u8, 128u8];
89    let mut bytes = input.iter().copied();
90    let mut idx = 0;
91    let mut byte = bytes.next().unwrap_or(0);
92    let mut acc: u64 = 0;
93
94    for pos in 0..nbits {
95        acc |= (((byte & bit_mask[offset]) >> offset) as u64) << pos;
96        if offset == 7 {
97            byte = bytes.next().unwrap_or(0);
98            idx += 1;
99            offset = 0;
100        } else {
101            offset += 1;
102        }
103    }
104    if input.len() > idx {
105        ((&input[idx..], offset), Value::UInt64(acc))
106    } else {
107        ((&[], offset), Value::UInt64(acc))
108    }
109}
110
111/// Increment the stored field value
112pub fn calculate_cumulative_value(
113    accumulate_fields: &mut HashMap<u32, Value>,
114    msg_num: u16,
115    def_num: u8,
116    value: Value,
117) -> Result<Value> {
118    // use macro to duplicate same type only addition logic
119    macro_rules! only_add_like_values {
120        ($key:ident, $val:ident, $stored_value:ident, $variant:ident) => {
121            if let Value::$variant(other) = value {
122                let value = Value::$variant($val + other);
123                accumulate_fields.insert($key, value.clone());
124                Ok(value)
125            } else {
126                Err(ErrorKind::ValueError(format!(
127                    "Mixed type addition {} and {} cannot be combined",
128                    $stored_value, value
129                ))
130                .into())
131            }
132        };
133    }
134
135    let key = (msg_num as u32) << 8 | def_num as u32;
136    if let Some(stored_value) = accumulate_fields.get(&key) {
137        match stored_value {
138            Value::Timestamp(_) => {
139                // TODO: fix this, probably done as u32 math but I probably need to keep timestamps
140                // as u32 values until the "11th hour" so to speak to deal with them more easily.
141                Err(ErrorKind::ValueError("Cannot accumlate timestamp fields".to_string()).into())
142            }
143            Value::Byte(val) => only_add_like_values!(key, val, stored_value, Byte),
144            Value::Enum(_) => {
145                Err(ErrorKind::ValueError("Cannot accumlate enum fields".to_string()).into())
146            }
147            Value::SInt8(val) => only_add_like_values!(key, val, stored_value, SInt8),
148            Value::UInt8(val) => only_add_like_values!(key, val, stored_value, UInt8),
149            Value::UInt8z(val) => only_add_like_values!(key, val, stored_value, UInt8z),
150            Value::SInt16(val) => only_add_like_values!(key, val, stored_value, SInt16),
151            Value::UInt16(val) => only_add_like_values!(key, val, stored_value, UInt16),
152            Value::UInt16z(val) => only_add_like_values!(key, val, stored_value, UInt16z),
153            Value::SInt32(val) => only_add_like_values!(key, val, stored_value, SInt32),
154            Value::UInt32(val) => only_add_like_values!(key, val, stored_value, UInt32),
155            Value::UInt32z(val) => only_add_like_values!(key, val, stored_value, UInt32z),
156            Value::SInt64(val) => only_add_like_values!(key, val, stored_value, SInt64),
157            Value::UInt64(val) => only_add_like_values!(key, val, stored_value, UInt64),
158            Value::UInt64z(val) => only_add_like_values!(key, val, stored_value, UInt64z),
159            Value::Float32(val) => only_add_like_values!(key, val, stored_value, Float32),
160            Value::Float64(val) => only_add_like_values!(key, val, stored_value, Float64),
161            Value::String(_) => {
162                Err(ErrorKind::ValueError("Cannot accumlate string fields".to_string()).into())
163            }
164            // add arrays by value if they are equal in length, we'll have to find FIT files
165            // to validate this behavior against the SDK. The Java SDK also always does this
166            // with longs, which makes sense I supppose since floats are large enough to store
167            // the explicit value so were always going to be working with integers for accumlated
168            // fields.
169            Value::Array(vals) => {
170                if let Value::Array(other_vals) = value {
171                    if vals.len() == other_vals.len() {
172                        let mut new_vals = Vec::with_capacity(vals.len());
173                        for (v1, v2) in vals.iter().zip(other_vals.iter()) {
174                            let v1_i64: i64 = v1.try_into()?;
175                            let v2_i64: i64 = v2.try_into()?;
176                            new_vals.push(Value::SInt64(v1_i64 + v2_i64));
177                        }
178                        accumulate_fields.insert(key, Value::Array(new_vals.clone()));
179                        Ok(Value::Array(new_vals))
180                    } else {
181                        Err(ErrorKind::ValueError(format!(
182                            "Array lengths differ, {} != {}",
183                            vals.len(),
184                            other_vals.len()
185                        ))
186                        .into())
187                    }
188                } else {
189                    Err(ErrorKind::ValueError(format!(
190                        "Mixed type addition {} and {} cannot be combined",
191                        stored_value, value
192                    ))
193                    .into())
194                }
195            }
196            Value::Invalid => {
197                Err(ErrorKind::ValueError("Cannot accumlate invalid fields".to_string()).into())
198            }
199        }
200    } else {
201        accumulate_fields.insert(key, value.clone());
202        Ok(value)
203    }
204}
205
206/// Build a data field using the provided FIT profile information
207#[allow(clippy::too_many_arguments)]
208pub fn data_field_with_info(
209    def_number: u8,
210    developer_data_index: Option<u8>,
211    name: &str,
212    data_type: FieldDataType,
213    scale: f64,
214    offset: f64,
215    units: &str,
216    value: Value,
217    options: &HashSet<DecodeOption>,
218) -> Result<FitDataField> {
219    let value = convert_value(data_type, scale, offset, value, options)?;
220    Ok(FitDataField::new(
221        name.to_string(),
222        def_number,
223        developer_data_index,
224        value,
225        units.to_string(),
226    ))
227}
228
229/// Create an "unknown" field as a placeholder if we don't have any field information
230pub fn unknown_field(field_def_num: u8, value: Value) -> FitDataField {
231    FitDataField::new(
232        format!("unknown_field_{}", field_def_num),
233        field_def_num,
234        None,
235        value,
236        String::new(),
237    )
238}
239
240/// Applies any necessary value conversions based on the field specification
241fn convert_value(
242    field_type: FieldDataType,
243    scale: f64,
244    offset: f64,
245    value: Value,
246    options: &HashSet<DecodeOption>,
247) -> Result<Value> {
248    // for array types return inner vector unmodified
249    if let Value::Array(vals) = value {
250        let vals: Result<Vec<Value>> = vals
251            .into_iter()
252            .map(|v| apply_scale_and_offset(v, scale, offset))
253            .collect();
254        return vals.map(Value::Array);
255    }
256
257    // handle time types specially, if for some reason I can't convert to an integer we will
258    // just dump the reference timestamp by passing it a 0
259    match field_type {
260        FieldDataType::DateTime => {
261            return Ok(Value::from(TimestampField::Utc(
262                value.try_into().unwrap_or(0),
263            )));
264        }
265        FieldDataType::LocalDateTime => {
266            return Ok(Value::from(TimestampField::Local(
267                value.try_into().unwrap_or(0),
268            )));
269        }
270        _ => (),
271    }
272
273    // convert enum or rescale integer value into floating point
274    if field_type.is_enum_type() {
275        let val: i64 = value.try_into()?;
276        if options.contains(&DecodeOption::ReturnNumericEnumValues) {
277            Ok(Value::SInt64(val))
278        } else if field_type.is_named_variant(val) {
279            Ok(Value::String(get_field_variant_as_string(field_type, val)))
280        } else {
281            Ok(Value::SInt64(val))
282        }
283    } else {
284        apply_scale_and_offset(value, scale, offset)
285    }
286}
287
288fn apply_scale_and_offset(value: Value, scale: f64, offset: f64) -> Result<Value> {
289    if value != Value::Invalid
290        && (((scale - 1.0).abs() > f64::EPSILON) || ((offset - 0.0).abs() > f64::EPSILON))
291    {
292        let val: f64 = value.try_into()?;
293        Ok(Value::Float64(val / scale - offset))
294    } else {
295        Ok(value)
296    }
297}