Skip to main content

fit/transforms/
components.rs

1//! Components — split a packed integer field into multiple LSB-first lanes.
2//!
3//! When a [`crate::profile::FieldInfo`] has a non-empty `components` slice,
4//! the parent field's wire bytes are interpreted as a packed integer that
5//! unpacks bit-for-bit into the named sub-fields. Each component's `bits`
6//! value defines its width; reading proceeds least-significant bit first.
7//!
8//! Reference: `guide/fit_binary_learning_notes.md` §"补充知识:BitStream".
9
10use crate::profile::Component;
11use crate::raw_value::RawValue;
12use crate::transforms::bit_stream::BitStream;
13
14/// Result of unpacking one component.
15#[derive(Debug, Clone, PartialEq)]
16pub struct UnpackedComponent {
17    /// Component's name from Profile.xlsx — points at another field on the
18    /// same message that the value should be stored under.
19    pub target_name: &'static str,
20    /// Raw integer extracted from the bit stream. Apply scale/offset
21    /// downstream if needed.
22    pub raw: u64,
23    pub bits: u8,
24    pub scale: Option<f64>,
25    pub offset: Option<f64>,
26    pub units: Option<&'static str>,
27    pub accumulate: bool,
28}
29
30/// Unpack a parent field's wire bytes into one [`UnpackedComponent`] per
31/// declared component. Returns an empty vec if `components` is empty.
32///
33/// `wire_bytes` should be the raw bytes of the parent field in the order
34/// they appeared in the file (the decoder handles endianness when filling
35/// `RawValue`, but Components specifically read **bit-by-bit LSB-first
36/// from the original byte sequence** — independent of the parent's logical
37/// endianness, because the operation is on a contiguous bit string).
38pub fn unpack_bytes(components: &'static [Component], wire_bytes: &[u8]) -> Vec<UnpackedComponent> {
39    if components.is_empty() {
40        return Vec::new();
41    }
42    let mut bs = BitStream::new(wire_bytes);
43    components
44        .iter()
45        .map(|c| UnpackedComponent {
46            target_name: c.name,
47            raw: bs.read_bits(c.bits as u32),
48            bits: c.bits,
49            scale: c.scale,
50            offset: c.offset,
51            units: c.units,
52            accumulate: c.accumulate,
53        })
54        .collect()
55}
56
57/// Materialise a single u64 (e.g. from a numeric `RawValue`) into LE bytes
58/// big enough to hold all the component bits, then unpack it. Convenience
59/// wrapper for fields whose [`RawValue`] is a length-1 numeric.
60pub fn unpack_scalar(components: &'static [Component], scalar: u64) -> Vec<UnpackedComponent> {
61    let total_bits: u32 = components.iter().map(|c| c.bits as u32).sum();
62    let nbytes = total_bits.div_ceil(8).max(1) as usize;
63    let bytes: Vec<u8> = (0..nbytes)
64        .map(|i| ((scalar >> (i * 8)) & 0xFF) as u8)
65        .collect();
66    unpack_bytes(components, &bytes)
67}
68
69/// Best-effort conversion of a numeric [`RawValue`] to a u64. Returns `None`
70/// for non-numeric variants (`String`, `Bytes`, `Invalid`).
71#[inline]
72pub fn scalar_as_u64(raw: &RawValue) -> Option<u64> {
73    raw.scalar_u64()
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    /// Mimic the four 8-bit lanes of `gear_change_data`.
81    static GEAR_CHANGE_COMPONENTS: &[Component] = &[
82        Component {
83            name: "rear_gear",
84            bits: 8,
85            scale: None,
86            offset: None,
87            units: None,
88            accumulate: false,
89        },
90        Component {
91            name: "rear_gear_num",
92            bits: 8,
93            scale: None,
94            offset: None,
95            units: None,
96            accumulate: false,
97        },
98        Component {
99            name: "front_gear",
100            bits: 8,
101            scale: None,
102            offset: None,
103            units: None,
104            accumulate: false,
105        },
106        Component {
107            name: "front_gear_num",
108            bits: 8,
109            scale: None,
110            offset: None,
111            units: None,
112            accumulate: false,
113        },
114    ];
115
116    #[test]
117    fn unpacks_gear_change_data_from_bytes() {
118        let bytes = [0x04, 0x03, 0x02, 0x01]; // LSB → rear=4, rear_num=3, front=2, front_num=1
119        let unpacked = unpack_bytes(GEAR_CHANGE_COMPONENTS, &bytes);
120        assert_eq!(unpacked.len(), 4);
121        assert_eq!(unpacked[0].target_name, "rear_gear");
122        assert_eq!(unpacked[0].raw, 4);
123        assert_eq!(unpacked[1].raw, 3);
124        assert_eq!(unpacked[2].raw, 2);
125        assert_eq!(unpacked[3].raw, 1);
126    }
127
128    #[test]
129    fn unpacks_gear_change_from_scalar() {
130        // Same value as a packed u32: 0x01020304 (front_num=1, front=2, rear_num=3, rear=4)
131        let scalar = 0x01_02_03_04u64;
132        let unpacked = unpack_scalar(GEAR_CHANGE_COMPONENTS, scalar);
133        assert_eq!(unpacked[0].raw, 4);
134        assert_eq!(unpacked[3].raw, 1);
135    }
136
137    #[test]
138    fn empty_components_yields_empty() {
139        let unpacked = unpack_bytes(&[], &[1, 2, 3, 4]);
140        assert!(unpacked.is_empty());
141    }
142
143    #[test]
144    fn scalar_as_u64_handles_common_cases() {
145        assert_eq!(scalar_as_u64(&RawValue::U8Scalar(42)), Some(42));
146        assert_eq!(scalar_as_u64(&RawValue::U16Scalar(1234)), Some(1234));
147        assert_eq!(
148            scalar_as_u64(&RawValue::U32Scalar(995749880)),
149            Some(995749880)
150        );
151        assert_eq!(scalar_as_u64(&RawValue::Invalid), None);
152        assert_eq!(scalar_as_u64(&RawValue::String("foo".into())), None);
153        // Multi-element arrays are not scalars.
154        assert_eq!(
155            scalar_as_u64(&RawValue::U8Array(vec![1u8, 2].into_boxed_slice())),
156            None
157        );
158    }
159}