Skip to main content

fit/profile/
field_info.rs

1//! Runtime types describing a FIT message's schema.
2//!
3//! Every value here is `&'static str` / `&'static [...]` so the entire profile
4//! lives in `.rodata` with zero allocation. The generated code in
5//! [`super::generated`] populates these structs with const initialisers.
6
7/// Static metadata for a single field of a single message.
8#[derive(Debug)]
9pub struct FieldInfo {
10    /// Field definition number used on the wire (0..=255).
11    pub field_def_num: u8,
12    /// Snake-case canonical name from Profile.xlsx.
13    pub name: &'static str,
14    /// Type reference: either a base type ("uint16") or a Types-sheet name ("sport").
15    /// Resolution to a runtime base type happens in M3.
16    pub type_name: &'static str,
17    /// Raw array spec (e.g. `"[5]"`, `"[N]"`, `"[5x10]"`) or `None` for scalars.
18    pub array: Option<&'static str>,
19    /// Scale factor (`physical = raw / scale - offset`). First element only when
20    /// scale is per-component; component-specific scales live on `Component`.
21    pub scale: Option<f64>,
22    /// Offset.
23    pub offset: Option<f64>,
24    /// Display unit (e.g. `"m/s"`, `"bpm"`).
25    pub units: Option<&'static str>,
26    /// Components — non-empty when the wire field unpacks LSB-first into
27    /// multiple sub-values (see protocol §"Components").
28    pub components: &'static [Component],
29    /// SubFields — alternative semantic interpretations selected at runtime
30    /// based on the value of another field in the same message.
31    pub sub_fields: &'static [SubField],
32    /// Whether this field accumulates across messages (for rollover compensation).
33    pub accumulate: bool,
34}
35
36/// One LSB-first slot inside a Components field.
37#[derive(Debug)]
38pub struct Component {
39    /// Name of the synthesised sub-value (refers to another field in the same message).
40    pub name: &'static str,
41    /// Width in bits.
42    pub bits: u8,
43    pub scale: Option<f64>,
44    pub offset: Option<f64>,
45    pub units: Option<&'static str>,
46    pub accumulate: bool,
47}
48
49/// One alternative interpretation of a parent field.
50#[derive(Debug)]
51pub struct SubField {
52    /// Snake-case canonical name from Profile.xlsx.
53    pub name: &'static str,
54    /// Type reference for this SubField.
55    pub type_name: &'static str,
56    /// Conditions: parallel `(ref_field_name, ref_value)` pairs. The SubField
57    /// applies when **any** condition matches (i.e. ref-field decoded value
58    /// equals one of the listed values, after Profile-level type resolution).
59    pub conditions: &'static [(&'static str, &'static str)],
60    /// Components to unpack when this SubField is selected (empty for most SubFields).
61    pub components: &'static [Component],
62    /// Scale factor.
63    pub scale: Option<f64>,
64    /// Offset.
65    pub offset: Option<f64>,
66    /// Display unit.
67    pub units: Option<&'static str>,
68}
69
70/// Static metadata for a single message.
71#[derive(Debug)]
72pub struct MesgInfo {
73    /// Snake-case canonical name from Profile.xlsx.
74    pub name: &'static str,
75    /// Fields in spreadsheet order.
76    pub fields: &'static [FieldInfo],
77}
78
79impl MesgInfo {
80    /// Find a field by its definition number. O(n); messages have ≤ 50 fields
81    /// in practice, so linear scan beats a hashmap for both code size and cache.
82    pub fn field(&self, field_def_num: u8) -> Option<&FieldInfo> {
83        self.fields
84            .iter()
85            .find(|f| f.field_def_num == field_def_num)
86    }
87
88    /// Find a field by its snake-case name.
89    pub fn field_by_name(&self, name: &str) -> Option<&FieldInfo> {
90        self.fields.iter().find(|f| f.name == name)
91    }
92}