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}