Skip to main content

fit/transforms/
subfields.rs

1//! SubField selection — pick the correct semantic interpretation of a
2//! field based on the value of *another* field in the same message.
3//!
4//! Reference: `guide/fit_binary_learning_notes.md` §"SubField 选择算法(重要)".
5//!
6//! Worked example: the `event` message's `data` field (u32) takes on
7//! different meanings depending on `event_type`:
8//!
9//! ```text
10//!   event_type = 0x10 (rear_gear_change)  →  data is gear_change_data
11//!   event_type = 0x2A (rider_position)    →  data is rider_position
12//!   ...                                   →  data stays as a generic u32
13//! ```
14
15use crate::profile::{FieldInfo, MesgInfo, SubField};
16use crate::transforms::components::scalar_as_u64;
17use crate::transforms::enum_strings::enum_str_by_value;
18use crate::RawMessage;
19
20/// Inspect a parent field's `sub_fields` and return the first one whose
21/// condition matches the parent message. Returns `None` when no SubField
22/// applies (in which case the parent field keeps its default semantics).
23pub fn select<'a>(
24    parent_field: &'a FieldInfo,
25    parent_mesg: &MesgInfo,
26    raw_message: &RawMessage<'_>,
27) -> Option<&'a SubField> {
28    if parent_field.sub_fields.is_empty() {
29        return None;
30    }
31    for sub in parent_field.sub_fields {
32        for (ref_name, expected) in sub.conditions {
33            if condition_matches(parent_mesg, raw_message, ref_name, expected) {
34                return Some(sub);
35            }
36        }
37    }
38    None
39}
40
41fn condition_matches(
42    parent_mesg: &MesgInfo,
43    raw_message: &RawMessage<'_>,
44    ref_field_name: &str,
45    expected: &str,
46) -> bool {
47    // Find the ref field's metadata in the parent message.
48    let Some(ref_field) = parent_mesg.field_by_name(ref_field_name) else {
49        return false;
50    };
51    // Find the ref field's decoded value in the raw message.
52    let Some(actual) = raw_message.field(ref_field.field_def_num) else {
53        return false;
54    };
55    let Some(actual_u64) = scalar_as_u64(&actual.value) else {
56        return false;
57    };
58
59    // Two acceptable forms for the SubField's expected condition value:
60    // (1) a numeric literal ("16", "0x10"), or
61    // (2) the snake_case name of a value in the ref field's enum type.
62    if let Some(expected_int) = parse_int_literal(expected) {
63        return actual_u64 == expected_int;
64    }
65    if let Some(actual_str) = enum_str_by_value(ref_field.type_name, actual_u64) {
66        return actual_str == expected;
67    }
68    false
69}
70
71fn parse_int_literal(s: &str) -> Option<u64> {
72    if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
73        return u64::from_str_radix(hex, 16).ok();
74    }
75    s.parse::<u64>().ok()
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81    use crate::profile::generated::messages;
82    use crate::raw_value::RawValue;
83
84    #[test]
85    fn parse_int_literal_accepts_hex_and_decimal() {
86        assert_eq!(parse_int_literal("16"), Some(16));
87        assert_eq!(parse_int_literal("0x10"), Some(16));
88        assert_eq!(parse_int_literal("0X10"), Some(16));
89        assert_eq!(parse_int_literal("running"), None);
90    }
91
92    #[test]
93    fn file_id_product_subfield_resolves_for_garmin_manufacturer() {
94        let file_id = messages::ALL_MESSAGES
95            .iter()
96            .find(|m| m.name == "file_id")
97            .expect("file_id must exist");
98
99        let product_field = file_id
100            .field_by_name("product")
101            .expect("file_id.product must exist");
102        assert!(!product_field.sub_fields.is_empty());
103
104        // Synthesize a RawMessage with manufacturer = 1 (garmin).
105        let raw = RawMessage {
106            global_mesg_num: 0,
107            fields: vec![
108                crate::RawField {
109                    field_def_num: 1, // manufacturer
110                    value: RawValue::U16Scalar(1),
111                },
112                crate::RawField {
113                    field_def_num: 2, // product (the parent of the SubField)
114                    value: RawValue::U16Scalar(0),
115                },
116            ],
117            dev_fields: vec![],
118            starts_new_chain: false,
119        };
120
121        let sub = select(product_field, file_id, &raw)
122            .expect("garmin_product subfield must match for manufacturer=1");
123        assert_eq!(sub.name, "garmin_product");
124    }
125
126    #[test]
127    fn returns_none_when_no_condition_matches() {
128        let file_id = messages::ALL_MESSAGES
129            .iter()
130            .find(|m| m.name == "file_id")
131            .unwrap();
132        let product = file_id.field_by_name("product").unwrap();
133
134        // manufacturer = 999 (not a known value, no subfield matches).
135        let raw = RawMessage {
136            global_mesg_num: 0,
137            fields: vec![crate::RawField {
138                field_def_num: 1,
139                value: RawValue::U16Scalar(999),
140            }],
141            dev_fields: vec![],
142            starts_new_chain: false,
143        };
144        assert!(select(product, file_id, &raw).is_none());
145    }
146}