Skip to main content

fit/
typed_decoder.rs

1//! Typed decoder — composes the M5 transform pipeline on top of [`Decoder`].
2//!
3//! Builder API:
4//!
5//! ```ignore
6//! let (messages, errors) = fit::Decoder::builder(&bytes)
7//!     .apply_scale_and_offset(true)
8//!     .convert_datetime(true)
9//!     .build()
10//!     .read_all();
11//! ```
12//!
13//! The pipeline is:
14//!   1. Look up `MesgInfo` by `global_mesg_num`.
15//!   2. For each raw field, find its `FieldInfo`.
16//!   3. Apply SubField selection (alters effective type/scale/offset).
17//!   4. Convert the raw value through DateTime / enum-string / scale/offset.
18//!   5. If the field declares Components, additionally emit one synthetic
19//!      [`Field`] per component (LSB-first unpacking).
20//!   6. Resolve developer fields via [`DevFieldRegistry`] (populated from
21//!      `developer_data_id` / `field_description` messages).
22
23use std::borrow::Cow;
24
25use crate::base_type::BaseType;
26#[cfg(feature = "chrono")]
27use crate::datetime;
28use crate::decoder::Decoder;
29use crate::dev_fields::{self, DevFieldRegistry};
30use crate::error::FitError;
31use crate::profile;
32use crate::raw_value::RawValue;
33use crate::stream::Endian;
34use crate::transforms::{components, enum_strings, scale_offset, subfields, Accumulator};
35use crate::value::{Field, FieldKind, Message, Value};
36use crate::{RawDevField, RawField, RawMessage};
37
38/// Callback type for per-message notifications.
39type MesgCallback<'a> = Box<dyn Fn(&Message) + 'a>;
40
41/// Toggleable transforms. All default to `true` (matches the JS SDK's
42/// "decode richly" preset). Disable individually to get cheaper / less
43/// opinionated output.
44#[derive(Debug, Clone, Copy)]
45pub struct TransformOptions {
46    pub apply_scale_and_offset: bool,
47    pub expand_components: bool,
48    pub expand_subfields: bool,
49    pub convert_types_to_strings: bool,
50    pub convert_datetime: bool,
51    /// Run MemoGlob reassembly after `read_all()` collects messages.
52    pub decode_memo_glob: bool,
53    /// Run heart-rate merge (HR samples → averaged `record.heart_rate`) after
54    /// `read_all()` collects messages. Requires `expand_components` and
55    /// `apply_scale_and_offset` to be enabled to compute fractional sample
56    /// timestamps correctly. Only available with the `chrono` feature.
57    #[cfg(feature = "chrono")]
58    pub merge_heart_rates: bool,
59    /// Skip `file_id` messages from the output.
60    pub skip_header: bool,
61    /// Only keep messages whose `global_mesg_num` is in the Profile
62    /// (filters out unknown / metadata-only messages).
63    pub data_only: bool,
64}
65
66impl Default for TransformOptions {
67    fn default() -> Self {
68        Self {
69            apply_scale_and_offset: true,
70            expand_components: true,
71            expand_subfields: true,
72            convert_types_to_strings: true,
73            convert_datetime: true,
74            decode_memo_glob: false,
75            #[cfg(feature = "chrono")]
76            merge_heart_rates: false,
77            skip_header: false,
78            data_only: false,
79        }
80    }
81}
82
83/// Builder for [`TypedDecoder`]. Created by [`Decoder::builder`].
84pub struct DecoderBuilder<'a> {
85    bytes: &'a [u8],
86    options: TransformOptions,
87    on_mesg: Option<MesgCallback<'a>>,
88}
89
90impl<'a> DecoderBuilder<'a> {
91    pub fn new(bytes: &'a [u8]) -> Self {
92        Self {
93            bytes,
94            options: TransformOptions::default(),
95            on_mesg: None,
96        }
97    }
98
99    pub fn apply_scale_and_offset(mut self, v: bool) -> Self {
100        self.options.apply_scale_and_offset = v;
101        self
102    }
103    pub fn expand_components(mut self, v: bool) -> Self {
104        self.options.expand_components = v;
105        self
106    }
107    pub fn expand_subfields(mut self, v: bool) -> Self {
108        self.options.expand_subfields = v;
109        self
110    }
111    pub fn convert_types_to_strings(mut self, v: bool) -> Self {
112        self.options.convert_types_to_strings = v;
113        self
114    }
115    pub fn convert_datetime(mut self, v: bool) -> Self {
116        self.options.convert_datetime = v;
117        self
118    }
119    pub fn decode_memo_glob(mut self, v: bool) -> Self {
120        self.options.decode_memo_glob = v;
121        self
122    }
123    #[cfg(feature = "chrono")]
124    pub fn merge_heart_rates(mut self, v: bool) -> Self {
125        self.options.merge_heart_rates = v;
126        self
127    }
128    pub fn skip_header(mut self, v: bool) -> Self {
129        self.options.skip_header = v;
130        self
131    }
132    pub fn data_only(mut self, v: bool) -> Self {
133        self.options.data_only = v;
134        self
135    }
136
137    /// Register a callback invoked for each decoded data message.
138    pub fn on_mesg<F: Fn(&Message) + 'a>(mut self, f: F) -> Self {
139        self.on_mesg = Some(Box::new(f));
140        self
141    }
142
143    pub fn build(self) -> TypedDecoder<'a> {
144        TypedDecoder {
145            inner: Decoder::new(self.bytes),
146            options: self.options,
147            accumulator: Accumulator::new(),
148            dev_registry: DevFieldRegistry::new(),
149            on_mesg: self.on_mesg,
150        }
151    }
152}
153
154impl<'a> Decoder<'a> {
155    /// Entry point for the typed pipeline. The caller chains transform-
156    /// option toggles on the returned [`DecoderBuilder`] and finishes with
157    /// `.build()`.
158    pub fn builder(bytes: &'a [u8]) -> DecoderBuilder<'a> {
159        DecoderBuilder::new(bytes)
160    }
161}
162
163/// Typed-message iterator. Yields one [`Message`] per Data record from the
164/// underlying [`Decoder`].
165pub struct TypedDecoder<'a> {
166    inner: Decoder<'a>,
167    options: TransformOptions,
168    pub(crate) accumulator: Accumulator,
169    dev_registry: DevFieldRegistry,
170    on_mesg: Option<MesgCallback<'a>>,
171}
172
173impl<'a> TypedDecoder<'a> {
174    /// Drain into vectors, mirroring [`Decoder::read_all`].
175    pub fn read_all(mut self) -> (Vec<Message>, Vec<FitError>) {
176        let mut messages = Vec::new();
177        let mut errors = Vec::new();
178        for item in self.by_ref() {
179            match item {
180                Ok(m) => messages.push(m),
181                Err(e) => errors.push(e),
182            }
183        }
184
185        // Post-processing: skip_header
186        if self.options.skip_header {
187            messages.retain(|m| m.global_mesg_num != 0); // file_id = 0
188        }
189
190        // Post-processing: data_only — filter to known Profile messages.
191        if self.options.data_only {
192            messages.retain(|m| crate::profile::mesg_info_by_num(m.global_mesg_num).is_some());
193        }
194
195        // Post-processing: MemoGlob reassembly
196        if self.options.decode_memo_glob {
197            crate::transforms::memo_glob::decode_memo_glob(&mut messages);
198        }
199
200        // Post-processing: HR merge (chrono-only).
201        #[cfg(feature = "chrono")]
202        if self.options.merge_heart_rates {
203            crate::transforms::merge_heart_rates(&mut messages);
204        }
205
206        (messages, errors)
207    }
208}
209
210impl<'a> Iterator for TypedDecoder<'a> {
211    type Item = Result<Message, FitError>;
212
213    fn next(&mut self) -> Option<Self::Item> {
214        match self.inner.next()? {
215            Ok(raw) => {
216                // Reset per-chain accumulator state at chained-FIT boundaries.
217                // The raw decoder flags the first Data record after a boundary;
218                // accumulated counter totals must not leak between chained files.
219                if raw.starts_new_chain {
220                    self.accumulator.clear();
221                }
222                collect_dev_meta(&raw, &mut self.dev_registry);
223                let msg = transform_message(
224                    &raw,
225                    self.options,
226                    &mut self.accumulator,
227                    &self.dev_registry,
228                );
229                if let Some(ref cb) = self.on_mesg {
230                    cb(&msg);
231                }
232                Some(Ok(msg))
233            }
234            Err(e) => Some(Err(e)),
235        }
236    }
237}
238
239// ────────────────────────────────────────────────────────────────────
240// Per-message and per-field transformation
241// ────────────────────────────────────────────────────────────────────
242
243/// Intercept `developer_data_id` (207) and `field_description` (206) messages
244/// to populate the developer field registry.
245fn collect_dev_meta(raw: &RawMessage<'_>, registry: &mut DevFieldRegistry) {
246    match raw.global_mesg_num {
247        // developer_data_id — we just need to know the index exists.
248        // The registry doesn't store the id itself, but we could extend it.
249        207 => {}
250        // field_description — register the field schema.
251        206 => {
252            let dev_idx = raw.field(0).and_then(|f| f.value.as_u8()).unwrap_or(0);
253            let fdn = raw.field(1).and_then(|f| f.value.as_u8()).unwrap_or(0);
254            let fit_base_type_id = raw.field(2).and_then(|f| f.value.as_u8()).unwrap_or(0xFF);
255            let field_name = raw
256                .field(3)
257                .and_then(|f| f.value.as_str())
258                .unwrap_or("unknown")
259                .to_string();
260            let scale_raw = raw.field(6).and_then(|f| f.value.as_u8());
261            let offset_raw = raw.field(7).and_then(|f| f.value.as_u8());
262            let units_str = raw
263                .field(8)
264                .and_then(|f| f.value.as_str())
265                .map(|s| s.to_string());
266
267            let scale = scale_raw.map(|v| v as f64).filter(|&s| s != 1.0);
268            let offset = offset_raw.map(|v| v as f64).filter(|&o| o != 0.0);
269
270            registry.register_field(
271                dev_idx,
272                fdn,
273                field_name,
274                fit_base_type_id,
275                scale,
276                offset,
277                units_str,
278            );
279        }
280        _ => {}
281    }
282}
283
284fn transform_message(
285    raw: &RawMessage<'_>,
286    options: TransformOptions,
287    acc: &mut Accumulator,
288    dev_registry: &DevFieldRegistry,
289) -> Message {
290    let mesg_info = profile::mesg_info_by_num(raw.global_mesg_num);
291    let name = mesg_info.map(|m| m.name).unwrap_or("unknown");
292
293    let mut fields = Vec::with_capacity(raw.fields.len() + 4);
294
295    for rf in &raw.fields {
296        let Some(mesg) = mesg_info else {
297            fields.push(unknown_field(rf));
298            continue;
299        };
300        let Some(fi) = mesg.field(rf.field_def_num) else {
301            fields.push(unknown_field(rf));
302            continue;
303        };
304
305        // Step 1: SubField resolution — picks effective semantics for this field.
306        let selected_subfield: Option<&crate::profile::SubField> = if options.expand_subfields {
307            subfields::select(fi, mesg, raw)
308        } else {
309            None
310        };
311        let (effective_name, type_name, scale, offset, units) = match selected_subfield {
312            Some(sub) => (sub.name, sub.type_name, fi.scale, fi.offset, fi.units),
313            None => (fi.name, fi.type_name, fi.scale, fi.offset, fi.units),
314        };
315
316        // Step 2: Accumulator — rollover compensation for counter fields.
317        let raw_for_transform = if fi.accumulate {
318            if let Some(scalar) = components::scalar_as_u64(&rf.value) {
319                let bits = resolve_accumulator_bits(fi.type_name);
320                let accumulated =
321                    acc.accumulate(raw.global_mesg_num, rf.field_def_num, scalar, bits);
322                RawValue::U64Scalar(accumulated)
323            } else {
324                rf.value.clone()
325            }
326        } else {
327            rf.value.clone()
328        };
329
330        // Step 3: convert the value through the option-gated transforms.
331        let value = transform_value(&raw_for_transform, type_name, scale, offset, options);
332        fields.push(Field {
333            name: effective_name.to_string(),
334            kind: FieldKind::Standard {
335                field_def_num: rf.field_def_num,
336            },
337            value,
338            units: units.map(str::to_string),
339        });
340
341        // Step 4: Components expansion (after the parent field has been emitted).
342        if options.expand_components {
343            // Parent field components.
344            if !fi.components.is_empty() {
345                if let Some(scalar) = components::scalar_as_u64(&rf.value) {
346                    for comp in components::unpack_scalar(fi.components, scalar) {
347                        let comp_raw = if comp.accumulate {
348                            acc.accumulate(
349                                raw.global_mesg_num,
350                                rf.field_def_num,
351                                comp.raw,
352                                comp.bits as u32,
353                            )
354                        } else {
355                            comp.raw
356                        };
357                        let raw_v = RawValue::U64Scalar(comp_raw);
358                        let cv =
359                            transform_value(&raw_v, "uint64", comp.scale, comp.offset, options);
360                        fields.push(Field {
361                            name: comp.target_name.to_string(),
362                            kind: FieldKind::Standard { field_def_num: 0 },
363                            value: cv,
364                            units: comp.units.map(str::to_string),
365                        });
366                    }
367                }
368            }
369            // SubField components (if the selected SubField has its own components).
370            if let Some(sub) = selected_subfield {
371                if !sub.components.is_empty() {
372                    if let Some(scalar) = components::scalar_as_u64(&rf.value) {
373                        for comp in components::unpack_scalar(sub.components, scalar) {
374                            let comp_raw = if comp.accumulate {
375                                acc.accumulate(
376                                    raw.global_mesg_num,
377                                    rf.field_def_num,
378                                    comp.raw,
379                                    comp.bits as u32,
380                                )
381                            } else {
382                                comp.raw
383                            };
384                            let raw_v = RawValue::U64Scalar(comp_raw);
385                            let cv =
386                                transform_value(&raw_v, "uint64", comp.scale, comp.offset, options);
387                            fields.push(Field {
388                                name: comp.target_name.to_string(),
389                                kind: FieldKind::Standard { field_def_num: 0 },
390                                value: cv,
391                                units: comp.units.map(str::to_string),
392                            });
393                        }
394                    }
395                }
396            }
397        }
398    }
399
400    // Developer fields: resolve via registry if available, else pass through bytes.
401    for dev in &raw.dev_fields {
402        if let Some(info) = dev_registry.get(dev.developer_data_index, dev.field_def_num) {
403            let value = resolve_dev_field(dev, info, options);
404            fields.push(Field {
405                name: info.name.clone(),
406                kind: FieldKind::Developer {
407                    field_def_num: dev.field_def_num,
408                    developer_data_index: dev.developer_data_index,
409                },
410                value,
411                units: info.units.clone(),
412            });
413        } else {
414            fields.push(Field {
415                name: "developer_field".to_string(),
416                kind: FieldKind::Developer {
417                    field_def_num: dev.field_def_num,
418                    developer_data_index: dev.developer_data_index,
419                },
420                value: Value::Bytes(dev.bytes.clone().into_owned()),
421                units: None,
422            });
423        }
424    }
425
426    Message {
427        global_mesg_num: raw.global_mesg_num,
428        name,
429        fields,
430    }
431}
432
433fn unknown_field(rf: &RawField) -> Field {
434    Field {
435        name: "unknown".to_string(),
436        kind: FieldKind::Standard {
437            field_def_num: rf.field_def_num,
438        },
439        value: raw_to_value_passthrough(&rf.value),
440        units: None,
441    }
442}
443
444/// Resolve a Profile type_name to the accumulator bit width.
445///
446/// For counter fields, the wire type's element size determines the wraparound
447/// width (e.g. `uint8` → 8 bits, `uint16` → 16 bits, `uint32` → 32 bits).
448/// Falls back to 32 bits if the type cannot be resolved.
449fn resolve_accumulator_bits(type_name: &str) -> u32 {
450    // Try Profile enum types first (e.g. "sport", "date_time").
451    if let Some(bt) = crate::transforms::enum_strings::base_type_for_type_name(type_name) {
452        return (bt.element_size() * 8) as u32;
453    }
454    // Try raw base type names (e.g. "uint8", "uint16", "uint32").
455    let bt = match type_name {
456        "enum" | "sint8" | "uint8" | "uint8z" | "byte" | "string" | "bool" => BaseType::UInt8,
457        "sint16" | "uint16" | "uint16z" => BaseType::UInt16,
458        "sint32" | "uint32" | "uint32z" | "float32" | "date_time" | "local_date_time" => {
459            BaseType::UInt32
460        }
461        "sint64" | "uint64" | "uint64z" | "float64" => BaseType::UInt64,
462        _ => BaseType::UInt32,
463    };
464    (bt.element_size() * 8) as u32
465}
466
467/// Resolve a developer field's raw bytes into a typed [`Value`] using the
468/// registry entry's base type, scale, and offset.
469fn resolve_dev_field(
470    dev: &RawDevField<'_>,
471    info: &dev_fields::DevFieldInfo,
472    options: TransformOptions,
473) -> Value {
474    // Decode the raw bytes as the registered base type.
475    // Developer fields are always LE per the FIT protocol.
476    let raw = match crate::raw_value::decode_value(
477        info.base_type,
478        &dev.bytes,
479        Endian::Little,
480        dev.field_def_num,
481    ) {
482        Ok(v) => v,
483        Err(_) => return Value::Bytes(dev.bytes.clone().into_owned()),
484    };
485
486    let type_name = dev_fields::base_type_to_type_name(info.base_type);
487    transform_value(&raw, type_name, info.scale, info.offset, options)
488}
489
490fn transform_value(
491    raw: &RawValue,
492    type_name: &str,
493    scale: Option<f64>,
494    offset: Option<f64>,
495    options: TransformOptions,
496) -> Value {
497    if matches!(raw, RawValue::Invalid) {
498        return Value::Invalid;
499    }
500
501    // DateTime conversion (date_time / local_date_time).
502    // With chrono: produces `Value::DateTime(DateTime<Utc>)`.
503    // Without chrono: produces `Value::DateTime(u32)` carrying raw FIT seconds.
504    if options.convert_datetime && (type_name == "date_time" || type_name == "local_date_time") {
505        if let Some(secs) = components::scalar_as_u64(raw) {
506            if secs <= u32::MAX as u64 {
507                #[cfg(feature = "chrono")]
508                {
509                    if let Some(dt) = datetime::fit_to_datetime(secs as u32) {
510                        return Value::DateTime(dt);
511                    }
512                }
513                #[cfg(not(feature = "chrono"))]
514                {
515                    return Value::DateTime(secs as u32);
516                }
517            }
518        }
519    }
520
521    // Enum int → snake_case string.
522    if options.convert_types_to_strings {
523        if let Some(v) = components::scalar_as_u64(raw) {
524            if let Some(s) = enum_strings::enum_str_by_value(type_name, v) {
525                // `s` is `&'static str` from the codegen dispatcher — store
526                // as `Cow::Borrowed` so the hot path stays alloc-free.
527                return Value::Enum(Cow::Borrowed(s));
528            }
529        }
530    }
531
532    // Scale / Offset.
533    if options.apply_scale_and_offset && !scale_offset::is_identity(scale, offset) {
534        if let Some(v) = scalar_as_f64(raw) {
535            return Value::Float(scale_offset::apply(v, scale, offset));
536        }
537        if let Some(arr) = array_as_f64s(raw) {
538            return Value::Array(
539                arr.into_iter()
540                    .map(|v| Value::Float(scale_offset::apply(v, scale, offset)))
541                    .collect(),
542            );
543        }
544    }
545
546    raw_to_value_passthrough(raw)
547}
548
549fn raw_to_value_passthrough(raw: &RawValue) -> Value {
550    use RawValue::*;
551    match raw {
552        Invalid => Value::Invalid,
553        String(s) => Value::String(s.to_string()),
554        Byte(v) => Value::Bytes(v.to_vec()),
555
556        // Scalars — single element on the stack.
557        EnumScalar(v) | U8Scalar(v) | U8zScalar(v) => Value::UInt(*v as u64),
558        U16Scalar(v) | U16zScalar(v) => Value::UInt(*v as u64),
559        U32Scalar(v) | U32zScalar(v) => Value::UInt(*v as u64),
560        U64Scalar(v) | U64zScalar(v) => Value::UInt(*v),
561        I8Scalar(v) => Value::SInt(*v as i64),
562        I16Scalar(v) => Value::SInt(*v as i64),
563        I32Scalar(v) => Value::SInt(*v as i64),
564        I64Scalar(v) => Value::SInt(*v),
565        F32Scalar(v) => Value::Float(*v as f64),
566        F64Scalar(v) => Value::Float(*v),
567
568        // Arrays — multiple elements. (length ≥ 2 by construction)
569        EnumArray(a) | U8Array(a) | U8zArray(a) => {
570            Value::Array(a.iter().map(|x| Value::UInt(*x as u64)).collect())
571        }
572        U16Array(a) | U16zArray(a) => {
573            Value::Array(a.iter().map(|x| Value::UInt(*x as u64)).collect())
574        }
575        U32Array(a) | U32zArray(a) => {
576            Value::Array(a.iter().map(|x| Value::UInt(*x as u64)).collect())
577        }
578        U64Array(a) | U64zArray(a) => Value::Array(a.iter().map(|x| Value::UInt(*x)).collect()),
579        I8Array(a) => Value::Array(a.iter().map(|x| Value::SInt(*x as i64)).collect()),
580        I16Array(a) => Value::Array(a.iter().map(|x| Value::SInt(*x as i64)).collect()),
581        I32Array(a) => Value::Array(a.iter().map(|x| Value::SInt(*x as i64)).collect()),
582        I64Array(a) => Value::Array(a.iter().map(|x| Value::SInt(*x)).collect()),
583        F32Array(a) => Value::Array(a.iter().map(|x| Value::Float(*x as f64)).collect()),
584        F64Array(a) => Value::Array(a.iter().map(|x| Value::Float(*x)).collect()),
585    }
586}
587
588#[inline]
589fn scalar_as_f64(raw: &RawValue) -> Option<f64> {
590    raw.scalar_f64()
591}
592
593#[inline]
594fn array_as_f64s(raw: &RawValue) -> Option<Vec<f64>> {
595    // For `to_f64s` we only want to take the array path here; callers already
596    // tried the scalar route first, but `to_f64s` accepts both and returns a
597    // single-element vec for scalars, which is also valid output.
598    raw.to_f64s()
599}