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}