Files
BikeControl/crates/fit/src/crc.rs
T
dtourolleandClaude Opus 5 7c17ca6158 Core ride logic, FTMS client, FIT encoder and probe CLI
Adds backing state for Resistance and Erg control modes, which had no
value to hold and so could never satisfy FR-4.3/FR-4.6.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 13:34:27 +02:00

121 lines
3.7 KiB
Rust

//! The FIT CRC-16.
//!
//! The FIT specification defines a nibble-table CRC. It is bit-for-bit
//! CRC-16/ARC (reflected polynomial `0xA001`, init `0x0000`, no final XOR),
//! which gives us published test vectors to check against — see the tests.
//!
//! Two CRCs appear in every FIT file and both must be right or the file is
//! silently rejected on upload:
//!
//! * the *header CRC* — bytes 0..12 of a 14-byte header, stored at bytes 12..14;
//! * the *file CRC* — every byte from the start of the header through the end
//! of the data records, appended as the last two bytes of the file.
/// Nibble lookup table from the FIT SDK.
const CRC_TABLE: [u16; 16] = [
0x0000, 0xCC01, 0xD801, 0x1400, 0xF001, 0x3C00, 0x2800, 0xE401, 0xA001, 0x6C00, 0x7800, 0xB401,
0x5000, 0x9C01, 0x8801, 0x4400,
];
/// Running FIT CRC-16 state.
///
/// Lets the encoder checksum bytes as they are produced rather than buffering
/// the whole file twice.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct Crc16(u16);
impl Crc16 {
/// A fresh CRC with the FIT initial value (zero).
pub const fn new() -> Self {
Self(0)
}
/// Fold `data` into the running CRC.
pub fn update(&mut self, data: &[u8]) {
let mut crc = self.0;
for &byte in data {
// Low nibble, then high nibble.
let mut tmp = CRC_TABLE[(crc & 0xF) as usize];
crc = (crc >> 4) & 0x0FFF;
crc = crc ^ tmp ^ CRC_TABLE[(byte & 0xF) as usize];
tmp = CRC_TABLE[(crc & 0xF) as usize];
crc = (crc >> 4) & 0x0FFF;
crc = crc ^ tmp ^ CRC_TABLE[((byte >> 4) & 0xF) as usize];
}
self.0 = crc;
}
/// The current checksum.
pub const fn value(self) -> u16 {
self.0
}
}
/// One-shot FIT CRC-16 over `data`.
pub fn crc16(data: &[u8]) -> u16 {
let mut crc = Crc16::new();
crc.update(data);
crc.value()
}
#[cfg(test)]
mod tests {
use super::*;
/// The canonical CRC-16/ARC check value: `crc("123456789") == 0xBB3D`.
/// If this fails, every FIT file we produce is rejected.
#[test]
fn known_vector_check_string() {
assert_eq!(crc16(b"123456789"), 0xBB3D);
}
#[test]
fn known_vector_empty_and_zero() {
assert_eq!(crc16(b""), 0x0000);
// CRC-16/ARC of a single zero byte is 0.
assert_eq!(crc16(&[0x00]), 0x0000);
// Published CRC-16/ARC vectors.
assert_eq!(crc16(b"A"), 0x30C0);
assert_eq!(crc16(&[0x00, 0x00, 0x00, 0x00]), 0x0000);
}
#[test]
fn matches_reference_bitwise_implementation() {
// Independent, deliberately naive reflected-CRC implementation.
fn reference(data: &[u8]) -> u16 {
let mut crc: u16 = 0;
for &b in data {
crc ^= b as u16;
for _ in 0..8 {
if crc & 1 != 0 {
crc = (crc >> 1) ^ 0xA001;
} else {
crc >>= 1;
}
}
}
crc
}
// A deterministic pseudo-random corpus.
let mut data = Vec::new();
let mut x: u32 = 0x1234_5678;
for _ in 0..1000 {
x = x.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
data.push((x >> 16) as u8);
assert_eq!(crc16(&data), reference(&data), "mismatch at len {}", data.len());
}
}
#[test]
fn incremental_equals_one_shot() {
let data: Vec<u8> = (0u8..=255).cycle().take(777).collect();
let mut running = Crc16::new();
for chunk in data.chunks(13) {
running.update(chunk);
}
assert_eq!(running.value(), crc16(&data));
}
}