Skip to main content

fit/
dev_fields.rs

1//! Developer field schema registry.
2//!
3//! Collects `developer_data_id` (mesg 207) and `field_description` (mesg 206)
4//! messages during decode, then resolves raw dev-field bytes into typed values.
5
6use std::collections::HashMap;
7
8use crate::base_type::BaseType;
9
10/// Runtime description of a single developer field.
11#[derive(Debug, Clone)]
12pub struct DevFieldInfo {
13    /// Human-readable name from `field_description.field_name`.
14    pub name: String,
15    /// Base type code (raw `fit_base_type_id` byte, masked to `& 0x1F`).
16    pub base_type: BaseType,
17    /// Scale factor (`None` or `1.0` means identity).
18    pub scale: Option<f64>,
19    /// Offset (`None` or `0.0` means identity).
20    pub offset: Option<f64>,
21    /// Display units from `field_description.units`.
22    pub units: Option<String>,
23}
24
25/// Key for looking up a dev field: `(developer_data_index, field_def_num)`.
26type DevFieldKey = (u8, u8);
27
28/// Registry of developer field schemas, populated from `developer_data_id` and
29/// `field_description` messages flowing through the typed decoder.
30#[derive(Debug, Default, Clone)]
31pub struct DevFieldRegistry {
32    fields: HashMap<DevFieldKey, DevFieldInfo>,
33}
34
35impl DevFieldRegistry {
36    /// Create an empty registry.
37    pub fn new() -> Self {
38        Self::default()
39    }
40
41    /// Register a field description. Called when a `field_description` message
42    /// (global_mesg_num=206) flows through the typed decoder.
43    #[allow(clippy::too_many_arguments)]
44    pub fn register_field(
45        &mut self,
46        developer_data_index: u8,
47        field_definition_number: u8,
48        field_name: String,
49        fit_base_type_id: u8,
50        scale: Option<f64>,
51        offset: Option<f64>,
52        units: Option<String>,
53    ) {
54        let base_type = match BaseType::from_byte(fit_base_type_id & BaseType::TYPE_CODE_MASK) {
55            Ok(bt) => bt,
56            Err(_) => return, // unknown base type — skip silently
57        };
58        let key = (developer_data_index, field_definition_number);
59        self.fields.insert(
60            key,
61            DevFieldInfo {
62                name: field_name,
63                base_type,
64                scale,
65                offset,
66                units,
67            },
68        );
69    }
70
71    /// Look up a developer field by `(developer_data_index, field_def_num)`.
72    pub fn get(&self, developer_data_index: u8, field_def_num: u8) -> Option<&DevFieldInfo> {
73        self.fields.get(&(developer_data_index, field_def_num))
74    }
75
76    /// Number of registered fields (for tests).
77    pub fn len(&self) -> usize {
78        self.fields.len()
79    }
80
81    /// Returns `true` if no fields are registered.
82    pub fn is_empty(&self) -> bool {
83        self.fields.is_empty()
84    }
85}
86
87/// Map a [`BaseType`] to the `type_name` string expected by
88/// [`crate::transforms::enum_strings::enum_str_by_value`] and
89/// `typed_decoder::transform_value`.
90pub fn base_type_to_type_name(bt: BaseType) -> &'static str {
91    match bt {
92        BaseType::Enum => "enum",
93        BaseType::SInt8 => "sint8",
94        BaseType::UInt8 => "uint8",
95        BaseType::SInt16 => "sint16",
96        BaseType::UInt16 => "uint16",
97        BaseType::SInt32 => "sint32",
98        BaseType::UInt32 => "uint32",
99        BaseType::String => "string",
100        BaseType::Float32 => "float32",
101        BaseType::Float64 => "float64",
102        BaseType::UInt8z => "uint8z",
103        BaseType::UInt16z => "uint16z",
104        BaseType::UInt32z => "uint32z",
105        BaseType::Byte => "byte",
106        BaseType::SInt64 => "sint64",
107        BaseType::UInt64 => "uint64",
108        BaseType::UInt64z => "uint64z",
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn register_and_lookup() {
118        let mut reg = DevFieldRegistry::new();
119        reg.register_field(
120            0,
121            0,
122            "heart_rate".to_string(),
123            0x02, // UInt8
124            None,
125            None,
126            Some("bpm".to_string()),
127        );
128        let info = reg.get(0, 0).unwrap();
129        assert_eq!(info.name, "heart_rate");
130        assert_eq!(info.base_type, BaseType::UInt8);
131        assert_eq!(info.units.as_deref(), Some("bpm"));
132    }
133
134    #[test]
135    fn unknown_base_type_is_skipped() {
136        let mut reg = DevFieldRegistry::new();
137        reg.register_field(0, 0, "bad".to_string(), 0xFF, None, None, None);
138        assert!(reg.is_empty());
139    }
140
141    #[test]
142    fn base_type_to_type_name_round_trip() {
143        for code in 0x00..=0x10 {
144            let bt = BaseType::from_byte(code).unwrap();
145            let name = base_type_to_type_name(bt);
146            assert!(!name.is_empty());
147        }
148    }
149}