Skip to main content

fit/
record_header.rs

1//! The 1-byte Record Header that prefixes every record in a FIT file.
2//!
3//! Layout:
4//!
5//! ```text
6//!   Bit 7  Bit 6  Bit 5  Bits [4:0]
7//!   ─────────────────────────────────────────────────────────
8//!     1     T      T     Timestamp[2:0]   → CompressedTimestamp data
9//!     0     1      D     LocalMsgNum      → Definition (D=1: contains dev fields)
10//!     0     0      —     LocalMsgNum      → Data
11//! ```
12//!
13//! In the Definition / Data forms, `LocalMsgNum` is bits `[3:0]` (mask
14//! `0x0F`, range 0..=15 — the 16-slot table).
15//!
16//! In the CompressedTimestamp form, `LocalMsgNum` is only 2 bits at `[6:5]`
17//! (range 0..=3, so compressed records can only reference the first four
18//! local definitions).
19//!
20//! Reference: `guide/fit_binary_learning_notes.md` §2.1.
21
22/// Classification of a single record-header byte.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum RecordHeader {
25    /// Definition message — declares the schema for one of the 16 local slots.
26    Definition {
27        /// Local message number (0..=15).
28        local_mesg_num: u8,
29        /// True when bit 5 is set: Definition is followed by developer-field
30        /// definitions in addition to the standard fields.
31        has_dev_data: bool,
32    },
33    /// Data message — payload follows, parsed against the Definition stored
34    /// in `local_mesg_num`'s slot.
35    Data {
36        /// Local message number (0..=15).
37        local_mesg_num: u8,
38    },
39    /// Compressed-timestamp data message. Bit 7 = 1; only 2 bits available
40    /// for `local_mesg_num`, 5 bits for the timestamp delta against the most
41    /// recent full timestamp seen.
42    CompressedTimestamp {
43        /// Local message number (0..=3, 2-bit field).
44        local_mesg_num: u8,
45        /// Timestamp delta in seconds against the last full timestamp (0..=31).
46        timestamp_offset: u8,
47    },
48}
49
50impl RecordHeader {
51    /// Bit 7. When set, this is a CompressedTimestamp data record.
52    pub const COMPRESSED_TIMESTAMP_MASK: u8 = 0x80;
53    /// Bit 6 (only meaningful when bit 7 = 0). When set, this is a Definition.
54    pub const DEFINITION_MASK: u8 = 0x40;
55    /// Bit 5 (only meaningful when bit 6 = 1). When set, the Definition
56    /// contains developer-field declarations.
57    pub const DEV_DATA_MASK: u8 = 0x20;
58    /// Bits `[3:0]`. Local message number for Definition / Data records.
59    pub const LOCAL_MESG_NUM_MASK: u8 = 0x0F;
60    /// Bits `[6:5]`. Local message number when the compressed-timestamp bit is set.
61    pub const COMPRESSED_LOCAL_MASK: u8 = 0x60;
62    /// Bits `[4:0]`. Timestamp delta for compressed-timestamp records (0..=31 seconds).
63    pub const TIMESTAMP_OFFSET_MASK: u8 = 0x1F;
64
65    /// Decode a single record-header byte.
66    pub fn classify(byte: u8) -> Self {
67        if byte & Self::COMPRESSED_TIMESTAMP_MASK != 0 {
68            Self::CompressedTimestamp {
69                local_mesg_num: (byte & Self::COMPRESSED_LOCAL_MASK) >> 5,
70                timestamp_offset: byte & Self::TIMESTAMP_OFFSET_MASK,
71            }
72        } else if byte & Self::DEFINITION_MASK != 0 {
73            Self::Definition {
74                local_mesg_num: byte & Self::LOCAL_MESG_NUM_MASK,
75                has_dev_data: byte & Self::DEV_DATA_MASK != 0,
76            }
77        } else {
78            Self::Data {
79                local_mesg_num: byte & Self::LOCAL_MESG_NUM_MASK,
80            }
81        }
82    }
83
84    /// Local message number, common to all three record types.
85    pub fn local_mesg_num(&self) -> u8 {
86        match self {
87            Self::Definition { local_mesg_num, .. }
88            | Self::Data { local_mesg_num }
89            | Self::CompressedTimestamp { local_mesg_num, .. } => *local_mesg_num,
90        }
91    }
92
93    /// True iff this is a Definition with the developer-data flag set.
94    pub fn has_dev_data(&self) -> bool {
95        matches!(
96            self,
97            Self::Definition {
98                has_dev_data: true,
99                ..
100            }
101        )
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn classifies_data_record() {
111        // 0b0000_0000 — bit 7 = 0, bit 6 = 0 → Data, local_mesg_num = 0
112        let h = RecordHeader::classify(0x00);
113        assert_eq!(h, RecordHeader::Data { local_mesg_num: 0 });
114
115        // 0b0000_0101 — Data with local_mesg_num = 5
116        let h = RecordHeader::classify(0x05);
117        assert_eq!(h, RecordHeader::Data { local_mesg_num: 5 });
118
119        // 0b0000_1111 — Data with local_mesg_num = 15
120        let h = RecordHeader::classify(0x0F);
121        assert_eq!(h, RecordHeader::Data { local_mesg_num: 15 });
122    }
123
124    #[test]
125    fn classifies_definition_record() {
126        // 0b0100_0000 — Definition, no dev data, local_mesg_num = 0
127        let h = RecordHeader::classify(0x40);
128        assert_eq!(
129            h,
130            RecordHeader::Definition {
131                local_mesg_num: 0,
132                has_dev_data: false
133            }
134        );
135
136        // 0b0100_0011 — Definition, no dev, local = 3
137        let h = RecordHeader::classify(0x43);
138        assert_eq!(
139            h,
140            RecordHeader::Definition {
141                local_mesg_num: 3,
142                has_dev_data: false
143            }
144        );
145    }
146
147    #[test]
148    fn classifies_definition_with_dev_data() {
149        // 0b0110_0000 — Definition + dev data flag, local = 0
150        let h = RecordHeader::classify(0x60);
151        assert_eq!(
152            h,
153            RecordHeader::Definition {
154                local_mesg_num: 0,
155                has_dev_data: true
156            }
157        );
158        assert!(h.has_dev_data());
159
160        // 0b0110_1010 — Definition + dev, local = 10
161        let h = RecordHeader::classify(0x6A);
162        assert_eq!(
163            h,
164            RecordHeader::Definition {
165                local_mesg_num: 10,
166                has_dev_data: true
167            }
168        );
169    }
170
171    #[test]
172    fn classifies_compressed_timestamp() {
173        // 0b1000_0000 — compressed, local = 0, offset = 0
174        let h = RecordHeader::classify(0x80);
175        assert_eq!(
176            h,
177            RecordHeader::CompressedTimestamp {
178                local_mesg_num: 0,
179                timestamp_offset: 0
180            }
181        );
182
183        // 0b1110_1010 — compressed, local = 3 (bits 6:5 = 11), offset = 10 (bits 4:0 = 01010)
184        let h = RecordHeader::classify(0xEA);
185        assert_eq!(
186            h,
187            RecordHeader::CompressedTimestamp {
188                local_mesg_num: 3,
189                timestamp_offset: 10
190            }
191        );
192
193        // 0b1011_1111 — compressed, local = 1 (bits 6:5 = 01), offset = 31 (max)
194        let h = RecordHeader::classify(0xBF);
195        assert_eq!(
196            h,
197            RecordHeader::CompressedTimestamp {
198                local_mesg_num: 1,
199                timestamp_offset: 31
200            }
201        );
202    }
203
204    #[test]
205    fn local_mesg_num_accessor() {
206        assert_eq!(
207            RecordHeader::Definition {
208                local_mesg_num: 7,
209                has_dev_data: false
210            }
211            .local_mesg_num(),
212            7
213        );
214        assert_eq!(
215            RecordHeader::Data { local_mesg_num: 12 }.local_mesg_num(),
216            12
217        );
218        assert_eq!(
219            RecordHeader::CompressedTimestamp {
220                local_mesg_num: 2,
221                timestamp_offset: 5
222            }
223            .local_mesg_num(),
224            2
225        );
226    }
227
228    #[test]
229    fn dev_data_only_set_for_definition() {
230        assert!(!RecordHeader::Data { local_mesg_num: 0 }.has_dev_data());
231        assert!(!RecordHeader::CompressedTimestamp {
232            local_mesg_num: 0,
233            timestamp_offset: 0
234        }
235        .has_dev_data());
236    }
237}