From 8448d94d46ed51d9ab0760f78daae938b6bddc65 Mon Sep 17 00:00:00 2001 From: ju6ge Date: Fri, 6 Mar 2026 19:54:55 +0100 Subject: [PATCH 01/19] document design choices and implement hemming (26, 31) with basic tests --- lib-bms-protocol/src/lib.rs | 5 + lib-bms-protocol/src/types.rs | 197 ++++++++++++++++++++++++++++++++++ 2 files changed, 202 insertions(+) create mode 100644 lib-bms-protocol/src/types.rs diff --git a/lib-bms-protocol/src/lib.rs b/lib-bms-protocol/src/lib.rs index 95f8d90..3704cd9 100644 --- a/lib-bms-protocol/src/lib.rs +++ b/lib-bms-protocol/src/lib.rs @@ -1,5 +1,10 @@ #![no_std] +#[cfg(test)] +extern crate std; + +mod types; + use embedded_hal::i2c::{I2c, Operation, SevenBitAddress}; use thiserror::Error; diff --git a/lib-bms-protocol/src/types.rs b/lib-bms-protocol/src/types.rs new file mode 100644 index 0000000..b25775f --- /dev/null +++ b/lib-bms-protocol/src/types.rs @@ -0,0 +1,197 @@ +//! # Base Value Types +//! +//! This module implements multiples types used for storing and transmitting/receiving data with +//! build in error detection/correction. +//! +//! For the purposes in the context of this project the following assumptions where made to determine the required data size. +//! +//! The largest value that could be stored or transmitted would be the running counter for the total amount of charge +//! or discharge that has occured since the beginning of recording. The intended use here is the monitoring of single +//! 12V LiFePO4 batteries which come in sizes up to 300 Ah. Many of the larger scale ones already come with a more advanced +//! BMS, so this is targetting lower tier ones with sizes up to 15 Ah. +//! +//! Modern LiFePO4 battery can easily withstand past 3000 charge cycles with about 365 charge cycles per year correspoding to +//! a charge cycle per day with solar energy. +//! +//! Targeting a 10 year lifespan would result in an absolute maxium of +//! 10,000 mAh * 10 years * 365 days = 53,400,000 mAh of total charge or discharge. +//! +//! Using a u26 would give us a counter for up to 2**26 = 67,108,863 mAh which is sufficient and allows using up to 6 bits for +//! redundancy within the 32bit word width. +//! +//! ## Use cases: +//! +//! ### Storage +//! +//! Storing data on flash for long term history retention might yield errors when flash cells are faulty leading most probably to on +//! bit errors. Ussing a hemming code for storing data (giving us 1bit error correction) requires 5 bits which is easily fits in the 6 bit redundancy data. For extra +//! redundancy a crc sum could be stored additionally to non recoverable errors. +//! +//! ### Transmission +//! +//! During transmission of data via serial interfaces, on bit errors ar less of a concern, here burst errors are more common du to +//! possible interfence on the transmission line. Using hemming codes would be waistul here. Instead a crc code is used for error +//! detection. If an error is detected the client device is expected to rerequest the data. +//! +//! ## CRC Selection +//! +//! Selecting a good crc polynomial requires careful consideration on the use data length and possible room within the code desired +//! code word lenght. For a good summary on CRC tradeoffs refer to this [paper](https://users.ece.cmu.edu/~koopman/roses/dsn04/koopman04_crc_poly_embedded.pdf). +//! For the purposes of this project the following selection was made. +//! +//! CRC-6 using the generator polynomial 0x21 is the best choice for a data length of 26 bits and giving us a hemming distance of +//! 3 for every valid code word. Meaning it is guaranteed that any error up to 3 bits will always be deteced. Beyond that more +//! errors random errors in the transmission have a probabilty of 2**-6 = 0.015625 of matching the original messages CRC code. +//! Meaning the chance of errors going undetected is <2%. + + +#[derive(Debug)] +/// this type is used for storing data on memory devices the concern here is mostly protecting against memory corruption +/// du to faulty memory cells. Here one bit errors are the most probabl cause of errors so this types uses a build in +/// hemming code for one mit error correction. +/// +/// additionally it is advised to store an extra checks sum to protect against multi bit errors +/// +/// this type is stored in u32 in the following form: +/// | 26 bits u26 value | 5 hemming bits | 1 unused extrabit | +pub struct Stored_U26(u32); + +#[derive(Debug)] +/// this type is used for transmitting data, the concern here is protection against transmission errors which most likely occur +/// as burst errors effecting multiple bits, hemming codes would be a waist of space here so instead a 6 bit crc code is used +/// +/// this type is stored in u32 in the following form: +/// | 26 bits u26 value | 6 bits crc value | +pub struct Transit_U26(u32); + +// only calc hemming code for the first 24 data bits including the 5 hemming bits +// pos | | p1 | p2 | p3 | p4 | p5 | +// 00001 | p1 | o | | | | | +// 00010 | p2 | | o | | | | +// 00011 | d1 | x | x | | | | +// 00100 | p3 | | | o | | | +// 00101 | d2 | x | | x | | | +// 00110 | d3 | | x | x | | | +// 00111 | d4 | x | x | x | | | +// 01000 | p4 | | | | o | | +// 01001 | d5 | x | | | x | | +// 01010 | d6 | | x | | x | | +// 01011 | d7 | x | x | | x | | +// 01100 | d8 | | | x | x | | +// 01101 | d9 | x | | x | x | | +// 01110 | d10 | | x | x | x | | +// 01111 | d11 | x | x | x | x | | +// 10000 | p5 | | | | | o | +// 10001 | d12 | x | | | | x | +// 10010 | d13 | | x | | | x | +// 10011 | d14 | x | x | | | x | +// 10100 | d15 | | | x | | x | +// 10101 | d16 | x | | x | | x | +// 10110 | d17 | | x | x | | x | +// 10111 | d18 | x | x | x | | x | +// 11000 | d19 | | | | x | x | +// 11001 | d20 | x | | | x | x | +// 11010 | d21 | | x | | x | x | +// 11011 | d22 | x | x | | x | x | +// 11100 | d23 | | | x | x | x | +// 11101 | d24 | x | | x | x | x | +// 11110 | d25 | | x | x | x | x | +// 11111 | d26 | x | x | x | x | x | +fn calc_hemming(data_bits: u32) -> u8 { + let p1 = ((data_bits) + ^ (data_bits >> 1) + ^ (data_bits >> 3) + ^ (data_bits >> 4) + ^ (data_bits >> 6) + ^ (data_bits >> 8) + ^ (data_bits >> 10) + ^ (data_bits >> 11) + ^ (data_bits >> 13) + ^ (data_bits >> 15) + ^ (data_bits >> 17) + ^ (data_bits >> 19) + ^ (data_bits >> 21) + ^ (data_bits >> 23) + ^ (data_bits >> 25)) + & 0x0001; + let p2 = ((data_bits) + ^ (data_bits >> 2) + ^ (data_bits >> 3) + ^ (data_bits >> 5) + ^ (data_bits >> 6) + ^ (data_bits >> 9) + ^ (data_bits >> 10) + ^ (data_bits >> 12) + ^ (data_bits >> 13) + ^ (data_bits >> 16) + ^ (data_bits >> 17) + ^ (data_bits >> 20) + ^ (data_bits >> 21) + ^ (data_bits >> 24) + ^ (data_bits >> 25)) + & 0x0001; + let p3 = ((data_bits >> 1) + ^ (data_bits >> 2) + ^ (data_bits >> 3) + ^ (data_bits >> 7) + ^ (data_bits >> 8) + ^ (data_bits >> 9) + ^ (data_bits >> 10) + ^ (data_bits >> 14) + ^ (data_bits >> 15) + ^ (data_bits >> 16) + ^ (data_bits >> 17) + ^ (data_bits >> 22) + ^ (data_bits >> 23) + ^ (data_bits >> 24) + ^ (data_bits >> 25)) + & 0x0001; + let p4 = ((data_bits >> 4) + ^ (data_bits >> 5) + ^ (data_bits >> 6) + ^ (data_bits >> 7) + ^ (data_bits >> 8) + ^ (data_bits >> 9) + ^ (data_bits >> 10) + ^ (data_bits >> 18) + ^ (data_bits >> 19) + ^ (data_bits >> 20) + ^ (data_bits >> 21) + ^ (data_bits >> 22) + ^ (data_bits >> 23) + ^ (data_bits >> 24) + ^ (data_bits >> 25)) + & 0x001; + let p5 = ((data_bits >> 11) + ^ (data_bits >> 12) + ^ (data_bits >> 13) + ^ (data_bits >> 14) + ^ (data_bits >> 15) + ^ (data_bits >> 16) + ^ (data_bits >> 17) + ^ (data_bits >> 18) + ^ (data_bits >> 19) + ^ (data_bits >> 20) + ^ (data_bits >> 21) + ^ (data_bits >> 22) + ^ (data_bits >> 23) + ^ (data_bits >> 24) + ^ (data_bits >> 25)) + & 0x0001; + + 0x00 | (p1 as u8) << 0 | (p2 as u8) << 1 | (p3 as u8) << 2 | (p4 as u8) << 3 | (p5 as u8) << 5 +} + + +#[cfg(test)] +mod base_type_tests { + use super::calc_hemming; + + #[test] + fn hemming_code_generation() { + assert_eq!(calc_hemming(0x000), 0b00000); + assert_eq!(calc_hemming(0x001), 0b00011); + assert_eq!(calc_hemming(0x002), 0b00101); + assert_eq!(calc_hemming(0x003), 0b00110); + } +} From a57b50887e542e4ccb345cd58e681bbafa759817 Mon Sep 17 00:00:00 2001 From: ju6ge Date: Tue, 10 Mar 2026 00:30:28 +0100 Subject: [PATCH 02/19] add from and to u32 scaffolding for U26 types --- lib-bms-protocol/src/types.rs | 43 ++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/lib-bms-protocol/src/types.rs b/lib-bms-protocol/src/types.rs index b25775f..4d99e50 100644 --- a/lib-bms-protocol/src/types.rs +++ b/lib-bms-protocol/src/types.rs @@ -44,6 +44,12 @@ //! errors random errors in the transmission have a probabilty of 2**-6 = 0.015625 of matching the original messages CRC code. //! Meaning the chance of errors going undetected is <2%. +const U26_VALUE_MASK: u32 = 0xFFFFFFc0; +const U26_MAX_VALUE: u32 = 0x03FFFFFF; + +pub enum U26Error { + Overflow +} #[derive(Debug)] /// this type is used for storing data on memory devices the concern here is mostly protecting against memory corruption @@ -56,6 +62,24 @@ /// | 26 bits u26 value | 5 hemming bits | 1 unused extrabit | pub struct Stored_U26(u32); +impl From for u32 { + fn from(value: Stored_U26) -> Self { + value.0 & U26_VALUE_MASK >> 6 + } +} + +impl TryFrom for Stored_U26 { + type Error = U26Error; + + fn try_from(value: u32) -> Result { + if value > U26_MAX_VALUE { + Err(U26Error::Overflow) + } else { + todo!() + } + } +} + #[derive(Debug)] /// this type is used for transmitting data, the concern here is protection against transmission errors which most likely occur /// as burst errors effecting multiple bits, hemming codes would be a waist of space here so instead a 6 bit crc code is used @@ -64,6 +88,24 @@ pub struct Stored_U26(u32); /// | 26 bits u26 value | 6 bits crc value | pub struct Transit_U26(u32); +impl From for u32 { + fn from(value: Transit_U26) -> Self { + value.0 & U26_VALUE_MASK >> 6 + } +} + +impl TryFrom for Transit_U26 { + type Error = U26Error; + + fn try_from(value: u32) -> Result { + if value > U26_MAX_VALUE { + Err(U26Error::Overflow) + } else { + todo!() + } + } +} + // only calc hemming code for the first 24 data bits including the 5 hemming bits // pos | | p1 | p2 | p3 | p4 | p5 | // 00001 | p1 | o | | | | | @@ -182,7 +224,6 @@ fn calc_hemming(data_bits: u32) -> u8 { 0x00 | (p1 as u8) << 0 | (p2 as u8) << 1 | (p3 as u8) << 2 | (p4 as u8) << 3 | (p5 as u8) << 5 } - #[cfg(test)] mod base_type_tests { use super::calc_hemming; From 4e810cf704bdcd8e524ee3d3d8be136739f11ac3 Mon Sep 17 00:00:00 2001 From: ju6ge Date: Thu, 12 Mar 2026 21:10:01 +0100 Subject: [PATCH 03/19] impl Stored_U26 constructor --- lib-bms-protocol/src/types.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib-bms-protocol/src/types.rs b/lib-bms-protocol/src/types.rs index 4d99e50..17638c6 100644 --- a/lib-bms-protocol/src/types.rs +++ b/lib-bms-protocol/src/types.rs @@ -48,7 +48,7 @@ const U26_VALUE_MASK: u32 = 0xFFFFFFc0; const U26_MAX_VALUE: u32 = 0x03FFFFFF; pub enum U26Error { - Overflow + Overflow, } #[derive(Debug)] @@ -62,6 +62,14 @@ pub enum U26Error { /// | 26 bits u26 value | 5 hemming bits | 1 unused extrabit | pub struct Stored_U26(u32); +impl Stored_U26 { + pub fn new(value: u32) -> Self { + assert!(value <= U26_MAX_VALUE); + let hemming_code = calc_hemming(value); + Self((value << 6) | ((hemming_code & 0x3F) << 1) as u32) + } +} + impl From for u32 { fn from(value: Stored_U26) -> Self { value.0 & U26_VALUE_MASK >> 6 @@ -75,7 +83,7 @@ impl TryFrom for Stored_U26 { if value > U26_MAX_VALUE { Err(U26Error::Overflow) } else { - todo!() + Ok(Self::new(value)) } } } From 87bd3abaca65b148d2bc2a4e2b2be7d48748867e Mon Sep 17 00:00:00 2001 From: ju6ge Date: Sun, 22 Mar 2026 10:08:57 +0100 Subject: [PATCH 04/19] testing hemming error correction logic --- lib-bms-protocol/src/types.rs | 114 +++++++++++++++++++++++++++++++--- 1 file changed, 107 insertions(+), 7 deletions(-) diff --git a/lib-bms-protocol/src/types.rs b/lib-bms-protocol/src/types.rs index 17638c6..df944a9 100644 --- a/lib-bms-protocol/src/types.rs +++ b/lib-bms-protocol/src/types.rs @@ -163,7 +163,7 @@ fn calc_hemming(data_bits: u32) -> u8 { ^ (data_bits >> 21) ^ (data_bits >> 23) ^ (data_bits >> 25)) - & 0x0001; + & 0x1; let p2 = ((data_bits) ^ (data_bits >> 2) ^ (data_bits >> 3) @@ -179,7 +179,7 @@ fn calc_hemming(data_bits: u32) -> u8 { ^ (data_bits >> 21) ^ (data_bits >> 24) ^ (data_bits >> 25)) - & 0x0001; + & 0x1; let p3 = ((data_bits >> 1) ^ (data_bits >> 2) ^ (data_bits >> 3) @@ -195,7 +195,7 @@ fn calc_hemming(data_bits: u32) -> u8 { ^ (data_bits >> 23) ^ (data_bits >> 24) ^ (data_bits >> 25)) - & 0x0001; + & 0x1; let p4 = ((data_bits >> 4) ^ (data_bits >> 5) ^ (data_bits >> 6) @@ -211,7 +211,7 @@ fn calc_hemming(data_bits: u32) -> u8 { ^ (data_bits >> 23) ^ (data_bits >> 24) ^ (data_bits >> 25)) - & 0x001; + & 0x1; let p5 = ((data_bits >> 11) ^ (data_bits >> 12) ^ (data_bits >> 13) @@ -227,14 +227,56 @@ fn calc_hemming(data_bits: u32) -> u8 { ^ (data_bits >> 23) ^ (data_bits >> 24) ^ (data_bits >> 25)) - & 0x0001; + & 0x1; - 0x00 | (p1 as u8) << 0 | (p2 as u8) << 1 | (p3 as u8) << 2 | (p4 as u8) << 3 | (p5 as u8) << 5 + 0x00 | (p1 as u8) << 0 | (p2 as u8) << 1 | (p3 as u8) << 2 | (p4 as u8) << 3 | (p5 as u8) << 4 +} + +/// if the hemming code does not equal the expected stored hemming value it calculated +/// hemming code will indicate the position of the invalid bit, this type is used +/// to hold the logic to use this information for correcting the read data in case of +/// an error +pub struct HemmingCorrectionValue(u8); + +impl HemmingCorrectionValue { + pub fn new(invalid_hemming: u8) -> Self { + HemmingCorrectionValue(invalid_hemming) + } + + /// calculate xor bit patterns to apply to the data or the stored hemming code + /// returns tuple -> (xor with data, xor with hemming code) + fn calc_correction_data(&self) -> (u32, u8) { + if [1, 2, 4, 8, 16].contains(&self.0) { + (0x00000000, 0x1 << self.0.ilog2()) + } else { + if self.0 == 3 { + (0x1, 0x00) + } else if self.0 < 8 { + (0x1 << self.0 - 3 - 1, 0x00) + } else if self.0 < 16 { + (0x1 << self.0 - 4 - 1, 0x00) + } else { + (0x1 << self.0 - 5 - 1, 0x00) + } + } + } +} + +/// check if hemming code matches the expected value for the provided data +pub fn check_hemming(data: u32, hemming: u8) -> Result<(), HemmingCorrectionValue> { + let h = calc_hemming(data); + if h == hemming { + Ok(()) + } else { + Err(HemmingCorrectionValue::new(h^hemming)) + } } #[cfg(test)] mod base_type_tests { - use super::calc_hemming; + use std::println; + + use super::{calc_hemming, check_hemming, U26_MAX_VALUE}; #[test] fn hemming_code_generation() { @@ -242,5 +284,63 @@ mod base_type_tests { assert_eq!(calc_hemming(0x001), 0b00011); assert_eq!(calc_hemming(0x002), 0b00101); assert_eq!(calc_hemming(0x003), 0b00110); + assert_eq!(calc_hemming(0x011), 0b01010); + } + + #[test] + fn hemming_failure() { + assert!(check_hemming(0x011, 0b00011).is_err()); + } + + #[test] + fn hemming_correct_invalid_value() { + let invalid_data = 0x011; + let hemming = 0b00011; + let correction = check_hemming(invalid_data, hemming).unwrap_err(); + let (data_correction, hemming_correction) = correction.calc_correction_data(); + assert_eq!(hemming_correction, 0); + assert_eq!(data_correction, 0x10); + let valid_data = invalid_data ^ data_correction; + assert!(check_hemming(valid_data, hemming).is_ok()); + } + + #[test] + fn hemming_correct_invalid_hemming_code() { + let data = 0x001; + let invalid_hemming = 0b00001; + let correction = check_hemming(data, invalid_hemming).unwrap_err(); + let (data_correction, hemming_correction) = correction.calc_correction_data(); + assert_eq!(hemming_correction, 0b00010); + assert_eq!(data_correction, 0x00); + let valid_hemming = invalid_hemming ^ hemming_correction; + assert!(check_hemming(data, valid_hemming).is_ok()); + } + + #[test] + fn full_onebit_value_correction_sweep() { + for val in 1..U26_MAX_VALUE { + let hemming = calc_hemming(val); + for i in 0..26 { + let bit_error = 0x1 << i; + let invalid_value = val ^ bit_error; + //println!("0x{val:x}, 0x{bit_error:x}, 0x{invalid_value:x}, 0b{hemming:b}, 0b{:b}", calc_hemming(invalid_value)); + let correction = check_hemming(invalid_value, hemming).unwrap_err(); + let (dc, _) = correction.calc_correction_data(); + assert_eq!(invalid_value ^ dc, val); + } + } + } + + #[test] + fn full_hemming_code_error_sweep() { + let data = 0x123456; + let hemming = calc_hemming(data); + for i in 0..5 { + let bit_error = 0x1 << i; + let invalid_hemming = hemming ^ bit_error; + let correction = check_hemming(data, invalid_hemming).unwrap_err(); + let (_, hc) = correction.calc_correction_data(); + assert_eq!(invalid_hemming^hc, hemming); + } } } From f4381cffb8757d3ffba9f5710bcc6268c3d2601a Mon Sep 17 00:00:00 2001 From: ju6ge Date: Sun, 22 Mar 2026 15:54:53 +0100 Subject: [PATCH 05/19] test detection of dual bit errors with hemming --- lib-bms-protocol/src/types.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib-bms-protocol/src/types.rs b/lib-bms-protocol/src/types.rs index df944a9..c9c325e 100644 --- a/lib-bms-protocol/src/types.rs +++ b/lib-bms-protocol/src/types.rs @@ -343,4 +343,14 @@ mod base_type_tests { assert_eq!(invalid_hemming^hc, hemming); } } + + #[test] + fn hemming_dual_bit_error_detection() { + let data = 0x123456; + let hemming = calc_hemming(data); + let bit_error = 0x101; + let correction = check_hemming(data ^ bit_error, hemming).unwrap_err(); + let (dc, hc) = correction.calc_correction_data(); + assert!(check_hemming(data ^ dc, hemming ^ hc).is_err()) + } } From 1106b4a82ef47b395c11376724d74e509257bbfd Mon Sep 17 00:00:00 2001 From: ju6ge Date: Sun, 22 Mar 2026 17:35:24 +0100 Subject: [PATCH 06/19] implement to and from be bytes for stored type --- lib-bms-protocol/src/types.rs | 56 +++++++++++++++++++++++++++++------ 1 file changed, 47 insertions(+), 9 deletions(-) diff --git a/lib-bms-protocol/src/types.rs b/lib-bms-protocol/src/types.rs index c9c325e..48df79a 100644 --- a/lib-bms-protocol/src/types.rs +++ b/lib-bms-protocol/src/types.rs @@ -46,12 +46,16 @@ const U26_VALUE_MASK: u32 = 0xFFFFFFc0; const U26_MAX_VALUE: u32 = 0x03FFFFFF; +const U26_HEMMING_MASK: u32 = 0x0000003e; +#[derive(Debug, PartialEq)] pub enum U26Error { + InvalidCodeWord, + IncorrectibleError, Overflow, } -#[derive(Debug)] +#[derive(Debug, PartialEq)] /// this type is used for storing data on memory devices the concern here is mostly protecting against memory corruption /// du to faulty memory cells. Here one bit errors are the most probabl cause of errors so this types uses a build in /// hemming code for one mit error correction. @@ -68,6 +72,38 @@ impl Stored_U26 { let hemming_code = calc_hemming(value); Self((value << 6) | ((hemming_code & 0x3F) << 1) as u32) } + + pub fn to_be_bytes(&self) -> [u8; 4] { + self.0.to_be_bytes() + } + + pub fn from_be_bytes(bytes: [u8;4]) -> Result { + let raw_u32 = u32::from_be_bytes(bytes); + // the last bit is expetced to be 0 + if raw_u32 & 0x1 != 0x0 { + return Err(U26Error::InvalidCodeWord); + } + let value = (raw_u32 & U26_VALUE_MASK) >> 6; + let hemming = ((raw_u32 & U26_HEMMING_MASK) >> 1) as u8; + match check_hemming(value, hemming) { + Ok(()) => Ok( + Stored_U26(raw_u32) + ), + Err(correction_data) => { + let (dc, hc) = correction_data.calc_correction_data(); + if check_hemming(value ^ dc, hemming ^ hc).is_err() { + // this case will not occur in our case because + // or codeword length corresponds to 31 and thus a perfect hemming + // code, flipping the correct bit will always produce a valid + // codeword even though the data was not the orginal stored data, + // to secure against this a crc is required + Err(U26Error::IncorrectibleError) + } else { + Ok(Self::new(value ^ dc)) + } + }, + } + } } impl From for u32 { @@ -276,7 +312,9 @@ pub fn check_hemming(data: u32, hemming: u8) -> Result<(), HemmingCorrectionValu mod base_type_tests { use std::println; - use super::{calc_hemming, check_hemming, U26_MAX_VALUE}; + use crate::types::U26Error; + + use super::{calc_hemming, check_hemming, Stored_U26, U26_MAX_VALUE}; #[test] fn hemming_code_generation() { @@ -345,12 +383,12 @@ mod base_type_tests { } #[test] - fn hemming_dual_bit_error_detection() { - let data = 0x123456; - let hemming = calc_hemming(data); - let bit_error = 0x101; - let correction = check_hemming(data ^ bit_error, hemming).unwrap_err(); - let (dc, hc) = correction.calc_correction_data(); - assert!(check_hemming(data ^ dc, hemming ^ hc).is_err()) + fn stored_u26_to_from_bytes() { + let data: Stored_U26 = 0x123456.try_into().unwrap(); + let mut bytes = data.to_be_bytes(); + assert_eq!(Stored_U26::from_be_bytes(bytes).unwrap(), data); + // introduce 1 bit error + bytes[2] = bytes[2] ^ 0x01; + assert_eq!(Stored_U26::from_be_bytes(bytes).unwrap(), data); } } From a71ac0c43e21a92566b8d1d55d7ca981b428ae58 Mon Sep 17 00:00:00 2001 From: ju6ge Date: Tue, 17 Mar 2026 20:03:39 +0100 Subject: [PATCH 07/19] inital implementation of crc6 --- lib-bms-protocol/src/types.rs | 119 ++++++++++++++++++++++++++++++---- 1 file changed, 106 insertions(+), 13 deletions(-) diff --git a/lib-bms-protocol/src/types.rs b/lib-bms-protocol/src/types.rs index 48df79a..7b5996c 100644 --- a/lib-bms-protocol/src/types.rs +++ b/lib-bms-protocol/src/types.rs @@ -77,7 +77,7 @@ impl Stored_U26 { self.0.to_be_bytes() } - pub fn from_be_bytes(bytes: [u8;4]) -> Result { + pub fn from_be_bytes(bytes: [u8; 4]) -> Result { let raw_u32 = u32::from_be_bytes(bytes); // the last bit is expetced to be 0 if raw_u32 & 0x1 != 0x0 { @@ -86,9 +86,7 @@ impl Stored_U26 { let value = (raw_u32 & U26_VALUE_MASK) >> 6; let hemming = ((raw_u32 & U26_HEMMING_MASK) >> 1) as u8; match check_hemming(value, hemming) { - Ok(()) => Ok( - Stored_U26(raw_u32) - ), + Ok(()) => Ok(Stored_U26(raw_u32)), Err(correction_data) => { let (dc, hc) = correction_data.calc_correction_data(); if check_hemming(value ^ dc, hemming ^ hc).is_err() { @@ -101,7 +99,7 @@ impl Stored_U26 { } else { Ok(Self::new(value ^ dc)) } - }, + } } } } @@ -282,9 +280,9 @@ impl HemmingCorrectionValue { /// calculate xor bit patterns to apply to the data or the stored hemming code /// returns tuple -> (xor with data, xor with hemming code) fn calc_correction_data(&self) -> (u32, u8) { - if [1, 2, 4, 8, 16].contains(&self.0) { + if [1, 2, 4, 8, 16].contains(&self.0) { (0x00000000, 0x1 << self.0.ilog2()) - } else { + } else { if self.0 == 3 { (0x1, 0x00) } else if self.0 < 8 { @@ -304,18 +302,70 @@ pub fn check_hemming(data: u32, hemming: u8) -> Result<(), HemmingCorrectionValu if h == hemming { Ok(()) } else { - Err(HemmingCorrectionValue::new(h^hemming)) + Err(HemmingCorrectionValue::new(h ^ hemming)) } } +/// CRC-6 calculation using polynomial 0x21 (MSB first) +/// +/// This function calculates a 6-bit CRC for 26 bits of data. +/// The polynomial used is 0x21 in the crc world polynoms +/// are named after the bit pattern with implicit assumption +/// of the trailing 1, thus 0x21 become 0x43 referring to +/// the 0b1000011 pattern. (Don't ask why just accept it …) +/// +/// # Algorithm +/// +/// The CRC is calculated using the standard CRC algorithm with MSB first bit ordering, +/// the inital value is equal to 0x00 and no additional transformat at the end is performed. +/// +/// # Error Detection +/// This CRC provides a Hamming distance of 3, meaning it can detect: +/// - All single-bit errors +/// - All double-bit errors +/// - Random errors with a probability of 2^-6 = 0.015625 of going undetected +#[cfg(test)] +pub fn calc_crc6(data: u32) -> u8 { + let mut working_data: u32 = data << 6; + let mut polynom: u32 = 0x43 << 25; + let mut mask: u32 = 0x40 << 25; + while working_data >= 0x40 { + if working_data & mask != 0 { + working_data ^= polynom; + } else { + polynom >>= 1; + mask >>= 1; + } + } + (working_data & 0x3f) as u8 +} + +/// CRC-6 checking +/// +/// This function is the mirror piece to the crc6 calculation, it checks a crc value against +/// the data to determine if the crc is valid. +pub fn check_crc6(data: u32, crc: u8) -> bool { + let mut working_data: u32 = data << 6 | crc as u32; + let mut polynom: u32 = 0x43 << 25; + let mut mask: u32 = 0x40 << 25; + while working_data >= 0x40 { + if working_data & mask != 0 { + working_data ^= polynom; + } else { + polynom >>= 1; + mask >>= 1; + } + } + working_data == 0x00000000 +} + #[cfg(test)] mod base_type_tests { - use std::println; - + use super::{ + Stored_U26, Transit_U26, U26_MAX_VALUE, calc_crc6, calc_hemming, check_crc6, check_hemming, + }; use crate::types::U26Error; - use super::{calc_hemming, check_hemming, Stored_U26, U26_MAX_VALUE}; - #[test] fn hemming_code_generation() { assert_eq!(calc_hemming(0x000), 0b00000); @@ -378,7 +428,7 @@ mod base_type_tests { let invalid_hemming = hemming ^ bit_error; let correction = check_hemming(data, invalid_hemming).unwrap_err(); let (_, hc) = correction.calc_correction_data(); - assert_eq!(invalid_hemming^hc, hemming); + assert_eq!(invalid_hemming ^ hc, hemming); } } @@ -391,4 +441,47 @@ mod base_type_tests { bytes[2] = bytes[2] ^ 0x01; assert_eq!(Stored_U26::from_be_bytes(bytes).unwrap(), data); } + + #[test] + fn crc6_simple_data() { + assert_eq!(calc_crc6(0x0000000), 0x00); + assert_eq!(calc_crc6(0x0000001), 0x03); + assert_eq!(calc_crc6(0x0000043), 0x00); + assert_eq!(calc_crc6(0x3ffffff), 0x06); + } + + #[test] + fn crc6_consistency() { + // Test that same input always produces same output + let data = 0x123456; + let crc = calc_crc6(data); + assert!(check_crc6(data, crc)) + } + + #[test] + fn crc6_different_inputs_different_outputs() { + // Test that different inputs produce different CRCs (with high probability) + let data1 = 0x123456; + let data2 = 0x123457; + let crc1 = calc_crc6(data1); + let crc2 = calc_crc6(data2); + assert_ne!(crc1, crc2); + } + + #[test] + fn crc6_bit_error_detection() { + // Test that single bit errors are detected + let original_data = 0x123456; + let original_crc = calc_crc6(original_data); + + // Flip each bit and verify CRC changes + for bit_pos in 0..26 { + let mut corrupted_data = original_data; + let mask = 1 << (25 - bit_pos); // Flip specific bit + corrupted_data ^= mask; + + // With high probability, CRC should change + assert!(!check_crc6(corrupted_data, original_crc)); + } + } } From bcaf76de12e12f0cc6cecadc9c61715f86b85893 Mon Sep 17 00:00:00 2001 From: ju6ge Date: Thu, 19 Mar 2026 19:21:49 +0100 Subject: [PATCH 08/19] impl Transit_U26 constructor --- lib-bms-protocol/src/types.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib-bms-protocol/src/types.rs b/lib-bms-protocol/src/types.rs index 7b5996c..31ee68e 100644 --- a/lib-bms-protocol/src/types.rs +++ b/lib-bms-protocol/src/types.rs @@ -130,6 +130,14 @@ impl TryFrom for Stored_U26 { /// | 26 bits u26 value | 6 bits crc value | pub struct Transit_U26(u32); +impl Transit_U26 { + pub fn new(value: u32) -> Self { + assert!(value <= U26_MAX_VALUE); + let crc6 = calc_crc6(value); + Self((value << 6) | (crc6 & 0x3F) as u32) + } +} + impl From for u32 { fn from(value: Transit_U26) -> Self { value.0 & U26_VALUE_MASK >> 6 @@ -143,7 +151,7 @@ impl TryFrom for Transit_U26 { if value > U26_MAX_VALUE { Err(U26Error::Overflow) } else { - todo!() + Ok(Transit_U26::new(value)) } } } @@ -324,7 +332,6 @@ pub fn check_hemming(data: u32, hemming: u8) -> Result<(), HemmingCorrectionValu /// - All single-bit errors /// - All double-bit errors /// - Random errors with a probability of 2^-6 = 0.015625 of going undetected -#[cfg(test)] pub fn calc_crc6(data: u32) -> u8 { let mut working_data: u32 = data << 6; let mut polynom: u32 = 0x43 << 25; From 5ab61e77d64d82d0e43c9b0bb046eac0959e3378 Mon Sep 17 00:00:00 2001 From: ju6ge Date: Thu, 19 Mar 2026 20:37:39 +0100 Subject: [PATCH 09/19] Impl to_* and from_be_bytes function for transit type --- lib-bms-protocol/src/types.rs | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/lib-bms-protocol/src/types.rs b/lib-bms-protocol/src/types.rs index 31ee68e..79d558d 100644 --- a/lib-bms-protocol/src/types.rs +++ b/lib-bms-protocol/src/types.rs @@ -53,6 +53,7 @@ pub enum U26Error { InvalidCodeWord, IncorrectibleError, Overflow, + ChecksumError, } #[derive(Debug, PartialEq)] @@ -122,7 +123,7 @@ impl TryFrom for Stored_U26 { } } -#[derive(Debug)] +#[derive(Debug, PartialEq)] /// this type is used for transmitting data, the concern here is protection against transmission errors which most likely occur /// as burst errors effecting multiple bits, hemming codes would be a waist of space here so instead a 6 bit crc code is used /// @@ -136,6 +137,21 @@ impl Transit_U26 { let crc6 = calc_crc6(value); Self((value << 6) | (crc6 & 0x3F) as u32) } + + pub fn from_be_bytes(bytes: [u8; 4]) -> Result { + let raw_val = u32::from_be_bytes(bytes); + let crc = (raw_val & 0x3F) as u8; + let val = raw_val >> 6; + if check_crc6(val, crc) { + Ok(Self(raw_val)) + } else { + Err(U26Error::ChecksumError) + } + } + + pub fn to_be_bytes(&self) -> [u8; 4] { + self.0.to_be_bytes() + } } impl From for u32 { @@ -459,20 +475,21 @@ mod base_type_tests { #[test] fn crc6_consistency() { - // Test that same input always produces same output + // Test that calc crc6 and check crc6 agree on validity let data = 0x123456; let crc = calc_crc6(data); assert!(check_crc6(data, crc)) } #[test] - fn crc6_different_inputs_different_outputs() { - // Test that different inputs produce different CRCs (with high probability) - let data1 = 0x123456; - let data2 = 0x123457; - let crc1 = calc_crc6(data1); - let crc2 = calc_crc6(data2); - assert_ne!(crc1, crc2); + fn transit_u26_to_from_bytes() { + let data1 = Transit_U26::new(0x123456); + // check that to from bytes conversion produces the same value + let mut bytes = data1.to_be_bytes(); + assert_eq!(data1, Transit_U26::from_be_bytes(bytes).unwrap()); + // test that changed data leads to checksum error + bytes[1] = 0x00; + assert!(Transit_U26::from_be_bytes(bytes).unwrap_err() == U26Error::ChecksumError) } #[test] From 6b82ddcd740fd3848e6fcea03411d5f3b46c62b6 Mon Sep 17 00:00:00 2001 From: ju6ge Date: Wed, 25 Mar 2026 13:54:20 +0100 Subject: [PATCH 10/19] updated crc algorithm to a more efficient implementation --- lib-bms-protocol/src/types.rs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/lib-bms-protocol/src/types.rs b/lib-bms-protocol/src/types.rs index 79d558d..d3d0bc1 100644 --- a/lib-bms-protocol/src/types.rs +++ b/lib-bms-protocol/src/types.rs @@ -332,7 +332,7 @@ pub fn check_hemming(data: u32, hemming: u8) -> Result<(), HemmingCorrectionValu /// CRC-6 calculation using polynomial 0x21 (MSB first) /// -/// This function calculates a 6-bit CRC for 26 bits of data. +/// This function calculates a 6-bit CRC for a u32 value. /// The polynomial used is 0x21 in the crc world polynoms /// are named after the bit pattern with implicit assumption /// of the trailing 1, thus 0x21 become 0x43 referring to @@ -349,18 +349,15 @@ pub fn check_hemming(data: u32, hemming: u8) -> Result<(), HemmingCorrectionValu /// - All double-bit errors /// - Random errors with a probability of 2^-6 = 0.015625 of going undetected pub fn calc_crc6(data: u32) -> u8 { - let mut working_data: u32 = data << 6; - let mut polynom: u32 = 0x43 << 25; - let mut mask: u32 = 0x40 << 25; - while working_data >= 0x40 { - if working_data & mask != 0 { - working_data ^= polynom; + let mut crc: u8 = 0x00; + for pos in 0..32 { + if ((data >> (31-pos)) & 0x1) ^ ((crc >> 5) & 0x1) as u32 != 0 { + crc = ((crc << 1) ^ 0x03) & 0x3F; } else { - polynom >>= 1; - mask >>= 1; + crc = (crc << 1) & 0x3F; } } - (working_data & 0x3f) as u8 + crc } /// CRC-6 checking From 3641331da2bf2cf8df7c4fea4bd51f5fb9f9558f Mon Sep 17 00:00:00 2001 From: ju6ge Date: Wed, 25 Mar 2026 13:54:47 +0100 Subject: [PATCH 11/19] update crc6 check as well, seperate commit to check that test results are stable --- lib-bms-protocol/src/types.rs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/lib-bms-protocol/src/types.rs b/lib-bms-protocol/src/types.rs index d3d0bc1..49993b8 100644 --- a/lib-bms-protocol/src/types.rs +++ b/lib-bms-protocol/src/types.rs @@ -365,18 +365,15 @@ pub fn calc_crc6(data: u32) -> u8 { /// This function is the mirror piece to the crc6 calculation, it checks a crc value against /// the data to determine if the crc is valid. pub fn check_crc6(data: u32, crc: u8) -> bool { - let mut working_data: u32 = data << 6 | crc as u32; - let mut polynom: u32 = 0x43 << 25; - let mut mask: u32 = 0x40 << 25; - while working_data >= 0x40 { - if working_data & mask != 0 { - working_data ^= polynom; + let mut check_crc: u8 = 0x00; + for pos in 0..32 { + if ((data >> (31-pos)) & 0x1) ^ ((check_crc >> 5) & 0x1) as u32 != 0 { + check_crc = ((check_crc << 1) ^ 0x03) & 0x3F; } else { - polynom >>= 1; - mask >>= 1; + check_crc = (check_crc << 1) & 0x3F; } } - working_data == 0x00000000 + check_crc == crc } #[cfg(test)] From d69c468ebc996d5810b4dbbe16aa945bc2ea2219 Mon Sep 17 00:00:00 2001 From: ju6ge Date: Wed, 25 Mar 2026 14:54:24 +0100 Subject: [PATCH 12/19] add crc6 calculation to Stored_U26 type and require crc for from_be_bytes function --- lib-bms-protocol/src/types.rs | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/lib-bms-protocol/src/types.rs b/lib-bms-protocol/src/types.rs index 49993b8..e034e52 100644 --- a/lib-bms-protocol/src/types.rs +++ b/lib-bms-protocol/src/types.rs @@ -74,11 +74,15 @@ impl Stored_U26 { Self((value << 6) | ((hemming_code & 0x3F) << 1) as u32) } + pub fn calc_crc6(&self) -> u8 { + calc_crc6(self.0) + } + pub fn to_be_bytes(&self) -> [u8; 4] { self.0.to_be_bytes() } - pub fn from_be_bytes(bytes: [u8; 4]) -> Result { + pub fn from_be_bytes(bytes: [u8; 4], crc: u8) -> Result { let raw_u32 = u32::from_be_bytes(bytes); // the last bit is expetced to be 0 if raw_u32 & 0x1 != 0x0 { @@ -98,7 +102,12 @@ impl Stored_U26 { // to secure against this a crc is required Err(U26Error::IncorrectibleError) } else { - Ok(Self::new(value ^ dc)) + let value = Self::new(value ^ dc); + if value.calc_crc6() == crc { + Ok(value) + } else { + Err(U26Error::IncorrectibleError) + } } } } @@ -452,11 +461,15 @@ mod base_type_tests { #[test] fn stored_u26_to_from_bytes() { let data: Stored_U26 = 0x123456.try_into().unwrap(); + let crc = data.calc_crc6(); let mut bytes = data.to_be_bytes(); - assert_eq!(Stored_U26::from_be_bytes(bytes).unwrap(), data); + assert_eq!(Stored_U26::from_be_bytes(bytes, crc).unwrap(), data); // introduce 1 bit error bytes[2] = bytes[2] ^ 0x01; - assert_eq!(Stored_U26::from_be_bytes(bytes).unwrap(), data); + assert_eq!(Stored_U26::from_be_bytes(bytes, crc).unwrap(), data); + // introduce second bit error + bytes[0] = bytes[0] ^ 0x01; + assert_eq!(Stored_U26::from_be_bytes(bytes, crc).unwrap_err(), U26Error::IncorrectibleError); } #[test] From 0437d30d04032ca8c52227b77073b834329834bb Mon Sep 17 00:00:00 2001 From: ju6ge Date: Wed, 25 Mar 2026 14:31:18 +0100 Subject: [PATCH 13/19] Convert communication protocol to use Transit_U26 with built-in CRC calculation --- lib-bms-protocol/src/lib.rs | 45 +++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/lib-bms-protocol/src/lib.rs b/lib-bms-protocol/src/lib.rs index 3704cd9..006b1dd 100644 --- a/lib-bms-protocol/src/lib.rs +++ b/lib-bms-protocol/src/lib.rs @@ -8,6 +8,8 @@ mod types; use embedded_hal::i2c::{I2c, Operation, SevenBitAddress}; use thiserror::Error; +use crate::types::Transit_U26; + pub const BMS_I2C_ADDRESS: u8 = 0x55; pub trait BmsReadable { @@ -38,6 +40,12 @@ where } } +impl From for BmsProtocolError { + fn from(_error: crate::types::U26Error) -> Self { + Self::I2cCommunicationError + } +} + #[derive(Debug)] pub struct BmsSoftwareReset; @@ -73,7 +81,8 @@ impl BmsReadable for ProtocolVersion { Operation::Read(&mut version), ], )?; - Ok(ProtocolVersion(u32::from_be_bytes(version))) + let transit = Transit_U26::from_be_bytes(version)?; + Ok(ProtocolVersion(u32::from(transit))) } } @@ -96,7 +105,8 @@ impl BmsReadable for FirmwareVersion { Operation::Read(&mut version), ], )?; - Ok(FirmwareVersion(u32::from_be_bytes(version))) + let transit = Transit_U26::from_be_bytes(version)?; + Ok(FirmwareVersion(u32::from(transit))) } } @@ -127,9 +137,9 @@ impl BmsReadable for Config { ], )?; Ok(Config { - capacity_mah: u32::from_be_bytes(capacity_mah), - v_full_mv: u32::from_be_bytes(v_full_mv), - v_empty_mv: u32::from_be_bytes(v_empty_mv), + capacity_mah: u32::from(Transit_U26::from_be_bytes(capacity_mah)?), + v_full_mv: u32::from(Transit_U26::from_be_bytes(v_full_mv)?), + v_empty_mv: u32::from(Transit_U26::from_be_bytes(v_empty_mv)?), }) } } @@ -167,11 +177,11 @@ impl BmsReadable for BatteryState { ], )?; Ok(BatteryState { - lifetime_capacity_mah: u32::from_be_bytes(lifetime_capacity_mah), - remaining_capacity_mah: u32::from_be_bytes(remaining_capacity_mah), - current_mv: u32::from_be_bytes(current_mv), + lifetime_capacity_mah: u32::from(Transit_U26::from_be_bytes(lifetime_capacity_mah)?), + remaining_capacity_mah: u32::from(Transit_U26::from_be_bytes(remaining_capacity_mah)?), + current_mv: u32::from(Transit_U26::from_be_bytes(current_mv)?), temperature_celcius: i32::from_be_bytes(temperature_celcius), - health_percent: u32::from_be_bytes(health_percent), + health_percent: u32::from(Transit_U26::from_be_bytes(health_percent)?), }) } } @@ -194,9 +204,9 @@ impl BmsReadable for ChargeInfoWindowSec { Operation::Read(&mut charge_info_window_sec), ], )?; - Ok(ChargeInfoWindowSec(u32::from_be_bytes( + Ok(ChargeInfoWindowSec(u32::from(Transit_U26::from_be_bytes( charge_info_window_sec, - ))) + )?))) } } @@ -206,11 +216,12 @@ impl BmsWriteable for ChargeInfoWindowSec { I: I2c, { let cmd = BmsRegisterMap::ChargoInfoWindowTotalSecs; + let transit = Transit_U26::try_from(self.0)?; i2c_dev.transaction( BMS_I2C_ADDRESS, &mut [ Operation::Write(&(cmd as u32).to_be_bytes()), - Operation::Write(&self.0.to_be_bytes()), + Operation::Write(&transit.to_be_bytes()), ], )?; Ok(()) @@ -250,11 +261,11 @@ impl BmsReadable for ChargeInfo { ], )?; Ok(ChargeInfo { - total_charge_ma: u32::from_be_bytes(total_charge_ma), - total_discharge_ma: u32::from_be_bytes(total_discharge_ma), - max_charge_mw: u32::from_be_bytes(max_charge_mw), - max_discharge_mw: u32::from_be_bytes(max_discharge_mw), - avg_voltage_mv: u32::from_be_bytes(avg_voltage_mv), + total_charge_ma: u32::from(Transit_U26::from_be_bytes(total_charge_ma)?), + total_discharge_ma: u32::from(Transit_U26::from_be_bytes(total_discharge_ma)?), + max_charge_mw: u32::from(Transit_U26::from_be_bytes(max_charge_mw)?), + max_discharge_mw: u32::from(Transit_U26::from_be_bytes(max_discharge_mw)?), + avg_voltage_mv: u32::from(Transit_U26::from_be_bytes(avg_voltage_mv)?), }) } } From 83f54d595320c9cafbb99acc678e5eb88a106c93 Mon Sep 17 00:00:00 2001 From: ju6ge Date: Wed, 25 Mar 2026 14:35:19 +0100 Subject: [PATCH 14/19] Add specific ChecksumError variant to BmsProtocolError --- lib-bms-protocol/src/lib.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib-bms-protocol/src/lib.rs b/lib-bms-protocol/src/lib.rs index 006b1dd..8e13811 100644 --- a/lib-bms-protocol/src/lib.rs +++ b/lib-bms-protocol/src/lib.rs @@ -29,6 +29,8 @@ pub trait BmsWriteable { pub enum BmsProtocolError { #[error("i2c communication failed")] I2cCommunicationError, + #[error("checksum error in received data")] + ChecksumError, } impl From for BmsProtocolError @@ -41,8 +43,11 @@ where } impl From for BmsProtocolError { - fn from(_error: crate::types::U26Error) -> Self { - Self::I2cCommunicationError + fn from(error: crate::types::U26Error) -> Self { + match error { + crate::types::U26Error::ChecksumError => Self::ChecksumError, + _ => Self::I2cCommunicationError, + } } } From 2332b507b2f4f78d0cc555fa733125a751556fd6 Mon Sep 17 00:00:00 2001 From: ju6ge Date: Wed, 25 Mar 2026 14:37:31 +0100 Subject: [PATCH 15/19] Replace u32::from with .into() for idiomatic Rust --- lib-bms-protocol/src/lib.rs | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/lib-bms-protocol/src/lib.rs b/lib-bms-protocol/src/lib.rs index 8e13811..d696386 100644 --- a/lib-bms-protocol/src/lib.rs +++ b/lib-bms-protocol/src/lib.rs @@ -87,7 +87,7 @@ impl BmsReadable for ProtocolVersion { ], )?; let transit = Transit_U26::from_be_bytes(version)?; - Ok(ProtocolVersion(u32::from(transit))) + Ok(ProtocolVersion(transit.into())) } } @@ -111,7 +111,7 @@ impl BmsReadable for FirmwareVersion { ], )?; let transit = Transit_U26::from_be_bytes(version)?; - Ok(FirmwareVersion(u32::from(transit))) + Ok(FirmwareVersion(transit.into())) } } @@ -142,9 +142,9 @@ impl BmsReadable for Config { ], )?; Ok(Config { - capacity_mah: u32::from(Transit_U26::from_be_bytes(capacity_mah)?), - v_full_mv: u32::from(Transit_U26::from_be_bytes(v_full_mv)?), - v_empty_mv: u32::from(Transit_U26::from_be_bytes(v_empty_mv)?), + capacity_mah: Transit_U26::from_be_bytes(capacity_mah)?.into(), + v_full_mv: Transit_U26::from_be_bytes(v_full_mv)?.into(), + v_empty_mv: Transit_U26::from_be_bytes(v_empty_mv)?.into(), }) } } @@ -182,11 +182,11 @@ impl BmsReadable for BatteryState { ], )?; Ok(BatteryState { - lifetime_capacity_mah: u32::from(Transit_U26::from_be_bytes(lifetime_capacity_mah)?), - remaining_capacity_mah: u32::from(Transit_U26::from_be_bytes(remaining_capacity_mah)?), - current_mv: u32::from(Transit_U26::from_be_bytes(current_mv)?), + lifetime_capacity_mah: Transit_U26::from_be_bytes(lifetime_capacity_mah)?.into(), + remaining_capacity_mah: Transit_U26::from_be_bytes(remaining_capacity_mah)?.into(), + current_mv: Transit_U26::from_be_bytes(current_mv)?.into(), temperature_celcius: i32::from_be_bytes(temperature_celcius), - health_percent: u32::from(Transit_U26::from_be_bytes(health_percent)?), + health_percent: Transit_U26::from_be_bytes(health_percent)?.into(), }) } } @@ -209,9 +209,9 @@ impl BmsReadable for ChargeInfoWindowSec { Operation::Read(&mut charge_info_window_sec), ], )?; - Ok(ChargeInfoWindowSec(u32::from(Transit_U26::from_be_bytes( - charge_info_window_sec, - )?))) + Ok(ChargeInfoWindowSec( + Transit_U26::from_be_bytes(charge_info_window_sec)?.into(), + )) } } @@ -266,11 +266,11 @@ impl BmsReadable for ChargeInfo { ], )?; Ok(ChargeInfo { - total_charge_ma: u32::from(Transit_U26::from_be_bytes(total_charge_ma)?), - total_discharge_ma: u32::from(Transit_U26::from_be_bytes(total_discharge_ma)?), - max_charge_mw: u32::from(Transit_U26::from_be_bytes(max_charge_mw)?), - max_discharge_mw: u32::from(Transit_U26::from_be_bytes(max_discharge_mw)?), - avg_voltage_mv: u32::from(Transit_U26::from_be_bytes(avg_voltage_mv)?), + total_charge_ma: Transit_U26::from_be_bytes(total_charge_ma)?.into(), + total_discharge_ma: Transit_U26::from_be_bytes(total_discharge_ma)?.into(), + max_charge_mw: Transit_U26::from_be_bytes(max_charge_mw)?.into(), + max_discharge_mw: Transit_U26::from_be_bytes(max_discharge_mw)?.into(), + avg_voltage_mv: Transit_U26::from_be_bytes(avg_voltage_mv)?.into(), }) } } From bf2ad52e0f91b39f87b478ad6e15dd9078890c5d Mon Sep 17 00:00:00 2001 From: ju6ge Date: Wed, 25 Mar 2026 15:39:42 +0100 Subject: [PATCH 16/19] Fix Transit_I26 implementation and complete protocol conversion --- lib-bms-protocol/src/lib.rs | 3 +- lib-bms-protocol/src/types.rs | 113 ++++++++++++++++++++++++++++++++-- 2 files changed, 109 insertions(+), 7 deletions(-) diff --git a/lib-bms-protocol/src/lib.rs b/lib-bms-protocol/src/lib.rs index d696386..a091787 100644 --- a/lib-bms-protocol/src/lib.rs +++ b/lib-bms-protocol/src/lib.rs @@ -185,7 +185,8 @@ impl BmsReadable for BatteryState { lifetime_capacity_mah: Transit_U26::from_be_bytes(lifetime_capacity_mah)?.into(), remaining_capacity_mah: Transit_U26::from_be_bytes(remaining_capacity_mah)?.into(), current_mv: Transit_U26::from_be_bytes(current_mv)?.into(), - temperature_celcius: i32::from_be_bytes(temperature_celcius), + temperature_celcius: crate::types::Transit_I26::from_be_bytes(temperature_celcius)? + .into(), health_percent: Transit_U26::from_be_bytes(health_percent)?.into(), }) } diff --git a/lib-bms-protocol/src/types.rs b/lib-bms-protocol/src/types.rs index e034e52..1012c46 100644 --- a/lib-bms-protocol/src/types.rs +++ b/lib-bms-protocol/src/types.rs @@ -165,7 +165,7 @@ impl Transit_U26 { impl From for u32 { fn from(value: Transit_U26) -> Self { - value.0 & U26_VALUE_MASK >> 6 + (value.0 & U26_VALUE_MASK) >> 6 } } @@ -181,6 +181,58 @@ impl TryFrom for Transit_U26 { } } +#[derive(Debug, PartialEq)] +/// this type is used for transmitting signed 26-bit integer data +/// It wraps the Transit_U26 type to provide i32 support +/// The i32 value is converted to u32 by casting, preserving the bit pattern +/// This ensures proper sign extension when converting between signed and unsigned +/// Valid range: -33554432 to 33554431 (26-bit signed) +pub struct Transit_I26(Transit_U26); + +impl Transit_I26 { + pub fn new(value: i32) -> Result { + // Check if value is within 26-bit signed range + if value < -0x02000000 || value > 0x01FFFFFF { + return Err(U26Error::Overflow); + } + // Extract only the lower 26 bits + let u26_val = (value as u32) & 0x03FFFFFF; + let transit_u26 = Transit_U26::try_from(u26_val)?; + Ok(Self(transit_u26)) + } + + pub fn from_be_bytes(bytes: [u8; 4]) -> Result { + let transit_u26 = Transit_U26::from_be_bytes(bytes)?; + Ok(Self(transit_u26)) + } + + pub fn to_be_bytes(&self) -> [u8; 4] { + self.0.to_be_bytes() + } +} + +impl From for i32 { + fn from(value: Transit_I26) -> Self { + // Convert u32 back to i32 by casting, preserving the exact bit pattern + let u32_val: u32 = value.0.into(); + // Interpret as signed 26-bit integer + if u32_val & 0x02000000 != 0 { + // Sign-extend to 32 bits + (u32_val as i32) | 0xFC000000u32 as i32 + } else { + u32_val as i32 + } + } +} + +impl TryFrom for Transit_I26 { + type Error = U26Error; + + fn try_from(value: i32) -> Result { + Self::new(value) + } +} + // only calc hemming code for the first 24 data bits including the 5 hemming bits // pos | | p1 | p2 | p3 | p4 | p5 | // 00001 | p1 | o | | | | | @@ -358,9 +410,9 @@ pub fn check_hemming(data: u32, hemming: u8) -> Result<(), HemmingCorrectionValu /// - All double-bit errors /// - Random errors with a probability of 2^-6 = 0.015625 of going undetected pub fn calc_crc6(data: u32) -> u8 { - let mut crc: u8 = 0x00; + let mut crc: u8 = 0x00; for pos in 0..32 { - if ((data >> (31-pos)) & 0x1) ^ ((crc >> 5) & 0x1) as u32 != 0 { + if ((data >> (31 - pos)) & 0x1) ^ ((crc >> 5) & 0x1) as u32 != 0 { crc = ((crc << 1) ^ 0x03) & 0x3F; } else { crc = (crc << 1) & 0x3F; @@ -374,9 +426,9 @@ pub fn calc_crc6(data: u32) -> u8 { /// This function is the mirror piece to the crc6 calculation, it checks a crc value against /// the data to determine if the crc is valid. pub fn check_crc6(data: u32, crc: u8) -> bool { - let mut check_crc: u8 = 0x00; + let mut check_crc: u8 = 0x00; for pos in 0..32 { - if ((data >> (31-pos)) & 0x1) ^ ((check_crc >> 5) & 0x1) as u32 != 0 { + if ((data >> (31 - pos)) & 0x1) ^ ((check_crc >> 5) & 0x1) as u32 != 0 { check_crc = ((check_crc << 1) ^ 0x03) & 0x3F; } else { check_crc = (check_crc << 1) & 0x3F; @@ -388,7 +440,8 @@ pub fn check_crc6(data: u32, crc: u8) -> bool { #[cfg(test)] mod base_type_tests { use super::{ - Stored_U26, Transit_U26, U26_MAX_VALUE, calc_crc6, calc_hemming, check_crc6, check_hemming, + calc_crc6, calc_hemming, check_crc6, check_hemming, Stored_U26, Transit_I26, Transit_U26, + U26_MAX_VALUE, }; use crate::types::U26Error; @@ -515,4 +568,52 @@ mod base_type_tests { assert!(!check_crc6(corrupted_data, original_crc)); } } + + #[test] + fn transit_i26_positive_values() { + let data = Transit_I26::new(12345).unwrap(); + let bytes = data.to_be_bytes(); + let decoded = Transit_I26::from_be_bytes(bytes).unwrap(); + assert_eq!(i32::from(decoded), 12345); + } + + #[test] + fn transit_i26_negative_values() { + let data = Transit_I26::new(-12345).unwrap(); + let bytes = data.to_be_bytes(); + let decoded = Transit_I26::from_be_bytes(bytes).unwrap(); + assert_eq!(i32::from(decoded), -12345); + } + + #[test] + fn transit_i26_zero() { + let data = Transit_I26::new(0).unwrap(); + let bytes = data.to_be_bytes(); + let decoded = Transit_I26::from_be_bytes(bytes).unwrap(); + assert_eq!(i32::from(decoded), 0); + } + + #[test] + fn transit_i26_max_positive() { + let data = Transit_I26::new(0x01FFFFFF).unwrap(); + let bytes = data.to_be_bytes(); + let decoded = Transit_I26::from_be_bytes(bytes).unwrap(); + assert_eq!(i32::from(decoded), 0x01FFFFFF as i32); + } + + #[test] + fn transit_i26_max_negative() { + let data = Transit_I26::new(-0x01FFFFFF).unwrap(); + let bytes = data.to_be_bytes(); + let decoded = Transit_I26::from_be_bytes(bytes).unwrap(); + assert_eq!(i32::from(decoded), -0x01FFFFFF as i32); + } + + #[test] + fn transit_i26_checksum_error() { + let data = Transit_I26::new(12345).unwrap(); + let mut bytes = data.to_be_bytes(); + bytes[1] ^= 0x01; // Corrupt one byte + assert!(Transit_I26::from_be_bytes(bytes).is_err()); + } } From 1c0ad62b7998691049dc954dbb586c1631623458 Mon Sep 17 00:00:00 2001 From: ju6ge Date: Sun, 12 Apr 2026 22:15:08 +0200 Subject: [PATCH 17/19] add chrono dependency --- Cargo.lock | 10 ++++++++++ lib-bms-protocol/Cargo.toml | 1 + 2 files changed, 11 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 388af15..5850b5e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -121,6 +121,15 @@ dependencies = [ "vcell", ] +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "num-traits", +] + [[package]] name = "critical-section" version = "1.2.0" @@ -536,6 +545,7 @@ dependencies = [ name = "lib-bms-protocol" version = "0.1.0" dependencies = [ + "chrono", "embedded-hal 1.0.0", "thiserror", ] diff --git a/lib-bms-protocol/Cargo.toml b/lib-bms-protocol/Cargo.toml index b374162..3b2f071 100644 --- a/lib-bms-protocol/Cargo.toml +++ b/lib-bms-protocol/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] +chrono = { version = "0.4.44", default-features = false } embedded-hal = "1.0.0" thiserror = { version = "2.0.17", default-features = false } From b3df9fedad32c0abd429e5dd06662cd65bd94218 Mon Sep 17 00:00:00 2001 From: ju6ge Date: Sun, 12 Apr 2026 22:17:57 +0200 Subject: [PATCH 18/19] basetypes i26 und u26 enable clone and disable warnings --- lib-bms-protocol/src/types.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib-bms-protocol/src/types.rs b/lib-bms-protocol/src/types.rs index 1012c46..b25aeca 100644 --- a/lib-bms-protocol/src/types.rs +++ b/lib-bms-protocol/src/types.rs @@ -56,7 +56,8 @@ pub enum U26Error { ChecksumError, } -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone)] +#[allow(non_camel_case_types)] /// this type is used for storing data on memory devices the concern here is mostly protecting against memory corruption /// du to faulty memory cells. Here one bit errors are the most probabl cause of errors so this types uses a build in /// hemming code for one mit error correction. @@ -132,7 +133,8 @@ impl TryFrom for Stored_U26 { } } -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone)] +#[allow(non_camel_case_types)] /// this type is used for transmitting data, the concern here is protection against transmission errors which most likely occur /// as burst errors effecting multiple bits, hemming codes would be a waist of space here so instead a 6 bit crc code is used /// @@ -181,7 +183,8 @@ impl TryFrom for Transit_U26 { } } -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone)] +#[allow(non_camel_case_types)] /// this type is used for transmitting signed 26-bit integer data /// It wraps the Transit_U26 type to provide i32 support /// The i32 value is converted to u32 by casting, preserving the bit pattern From 1dba2e85f248d5a596e1c882102afde97dde0a07 Mon Sep 17 00:00:00 2001 From: ju6ge Date: Sun, 12 Apr 2026 22:18:57 +0200 Subject: [PATCH 19/19] Add Store/Transit_DateTimeUtc wrapper type for 52-bit timestamp --- lib-bms-protocol/src/lib.rs | 3 + lib-bms-protocol/src/timestamp.rs | 373 ++++++++++++++++++++++++++++++ lib-bms-protocol/src/types.rs | 7 +- 3 files changed, 381 insertions(+), 2 deletions(-) create mode 100644 lib-bms-protocol/src/timestamp.rs diff --git a/lib-bms-protocol/src/lib.rs b/lib-bms-protocol/src/lib.rs index a091787..b9c77a5 100644 --- a/lib-bms-protocol/src/lib.rs +++ b/lib-bms-protocol/src/lib.rs @@ -3,7 +3,10 @@ #[cfg(test)] extern crate std; +extern crate chrono; + mod types; +mod timestamp; use embedded_hal::i2c::{I2c, Operation, SevenBitAddress}; use thiserror::Error; diff --git a/lib-bms-protocol/src/timestamp.rs b/lib-bms-protocol/src/timestamp.rs new file mode 100644 index 0000000..34d01ed --- /dev/null +++ b/lib-bms-protocol/src/timestamp.rs @@ -0,0 +1,373 @@ +use chrono::{DateTime, Utc}; + +use crate::types::{Stored_U26, Transit_U26, U26Error}; + +#[derive(Debug, PartialEq)] +/// Error type for DateTimeUtc wrapper types +pub enum DateTimeUtcError { + Overflow, + InvalidTimestamp, + ConversionError, +} + +impl From for DateTimeUtcError { + fn from(error: U26Error) -> Self { + match error { + U26Error::Overflow => DateTimeUtcError::Overflow, + U26Error::InvalidCodeWord => DateTimeUtcError::InvalidTimestamp, + U26Error::IncorrectibleError => DateTimeUtcError::InvalidTimestamp, + U26Error::ChecksumError => DateTimeUtcError::InvalidTimestamp, + } + } +} + + +#[derive(Debug, PartialEq)] +#[allow(non_camel_case_types)] +/// Wrapper type for storing DateTime with error correction +/// Uses two Stored_U26 values internally to store 52 bits of milliseconds since Unix epoch +pub struct Store_DateTimeUtc(Stored_U26, Stored_U26); + +impl Store_DateTimeUtc { + /// Maximum timestamp value that can be stored (in milliseconds) + const MAX_TIMESTAMP_MS: u64 = (1u64 << 52) - 1; + + /// Create a new Store_DateTimeUtc from a DateTime + pub fn new(dt: DateTime) -> Result { + let timestamp_ms = dt.timestamp_millis(); + if timestamp_ms < 0 || timestamp_ms > Self::MAX_TIMESTAMP_MS as i64 { + return Err(DateTimeUtcError::Overflow); + } + let timestamp_u64 = timestamp_ms as u64; + let low_26 = (timestamp_u64 & 0x03FFFFFF) as u32; + let high_26 = ((timestamp_u64 >> 26) & 0x03FFFFFF) as u32; + let stored_low = Stored_U26::try_from(low_26)?; + let stored_high = Stored_U26::try_from(high_26)?; + Ok(Self(stored_low, stored_high)) + } + + /// Get the DateTime value + pub fn into_datetime(self) -> Result, DateTimeUtcError> { + let low_26: u32 = self.0.into(); + let high_26: u32 = self.1.into(); + let timestamp_u64 = ((high_26 as u64) << 26) | (low_26 as u64); + DateTime::from_timestamp_millis(timestamp_u64 as i64) + .ok_or(DateTimeUtcError::ConversionError) + } + + /// Get the DateTime value (borrowed) + pub fn to_datetime(&self) -> Result, DateTimeUtcError> { + let low_26: u32 = self.0.clone().into(); + let high_26: u32 = self.1.clone().into(); + let timestamp_u64 = ((high_26 as u64) << 26) | (low_26 as u64); + DateTime::from_timestamp_millis(timestamp_u64 as i64) + .ok_or(DateTimeUtcError::ConversionError) + } + + /// Serialize to big-endian bytes with CRC values + /// Format: [high_bytes[0], high_bytes[1], high_bytes[2], high_bytes[3], high_crc, low_bytes[0], low_bytes[1], low_bytes[2], low_bytes[3], low_crc] + pub fn to_be_bytes(&self) -> [u8; 10] { + let mut bytes = [0u8; 10]; + let high_bytes = self.1.to_be_bytes(); + let high_crc = self.1.calc_crc6(); + let low_bytes = self.0.to_be_bytes(); + let low_crc = self.0.calc_crc6(); + + bytes[0] = high_bytes[0]; + bytes[1] = high_bytes[1]; + bytes[2] = high_bytes[2]; + bytes[3] = high_bytes[3]; + bytes[4] = high_crc; + bytes[5] = low_bytes[0]; + bytes[6] = low_bytes[1]; + bytes[7] = low_bytes[2]; + bytes[8] = low_bytes[3]; + bytes[9] = low_crc; + + bytes + } + + /// Deserialize from big-endian bytes with CRC check + /// Format: [high_bytes[0], high_bytes[1], high_bytes[2], high_bytes[3], high_crc, low_bytes[0], low_bytes[1], low_bytes[2], low_bytes[3], low_crc] + pub fn from_be_bytes(bytes: [u8; 10]) -> Result { + let high_bytes = [bytes[0], bytes[1], bytes[2], bytes[3]]; + let high_crc = bytes[4]; + let low_bytes = [bytes[5], bytes[6], bytes[7], bytes[8]]; + let low_crc = bytes[9]; + + let stored_high = Stored_U26::from_be_bytes(high_bytes, high_crc)?; + let stored_low = Stored_U26::from_be_bytes(low_bytes, low_crc)?; + Ok(Self(stored_low, stored_high)) + } +} + +impl From for DateTime { + fn from(value: Store_DateTimeUtc) -> Self { + value.into_datetime().unwrap() + } +} + +impl TryFrom> for Store_DateTimeUtc { + type Error = DateTimeUtcError; + + fn try_from(value: DateTime) -> Result { + Self::new(value) + } +} + +impl From for u64 { + fn from(value: Store_DateTimeUtc) -> Self { + let low_26: u32 = value.0.into(); + let high_26: u32 = value.1.into(); + ((high_26 as u64) << 26) | (low_26 as u64) + } +} + +#[derive(Debug, PartialEq)] +#[allow(non_camel_case_types)] +/// Wrapper type for transmitting DateTime with CRC error detection +/// Uses two Transit_U26 values internally to store 52 bits of milliseconds since Unix epoch +pub struct Transit_DateTimeUtc(Transit_U26, Transit_U26); + +impl Transit_DateTimeUtc { + /// Maximum timestamp value that can be transmitted (in milliseconds) + const MAX_TIMESTAMP_MS: u64 = (1u64 << 52) - 1; + + /// Create a new Transit_DateTimeUtc from a DateTime + pub fn new(dt: DateTime) -> Result { + let timestamp_ms = dt.timestamp_millis(); + if timestamp_ms < 0 || timestamp_ms > Self::MAX_TIMESTAMP_MS as i64 { + return Err(DateTimeUtcError::Overflow); + } + let timestamp_u64 = timestamp_ms as u64; + let low_26 = (timestamp_u64 & 0x03FFFFFF) as u32; + let high_26 = ((timestamp_u64 >> 26) & 0x03FFFFFF) as u32; + let transit_low = Transit_U26::try_from(low_26)?; + let transit_high = Transit_U26::try_from(high_26)?; + Ok(Self(transit_low, transit_high)) + } + + /// Get the DateTime value + pub fn into_datetime(self) -> Result, DateTimeUtcError> { + let low_26: u32 = self.0.into(); + let high_26: u32 = self.1.into(); + let timestamp_u64 = ((high_26 as u64) << 26) | (low_26 as u64); + DateTime::from_timestamp_millis(timestamp_u64 as i64) + .ok_or(DateTimeUtcError::ConversionError) + } + + /// Get the DateTime value (borrowed) + pub fn to_datetime(&self) -> Result, DateTimeUtcError> { + let low_26: u32 = self.0.clone().into(); + let high_26: u32 = self.1.clone().into(); + let timestamp_u64 = ((high_26 as u64) << 26) | (low_26 as u64); + DateTime::from_timestamp_millis(timestamp_u64 as i64) + .ok_or(DateTimeUtcError::ConversionError) + } + + /// Serialize to big-endian bytes + /// Format: [high_bytes[0], high_bytes[1], high_bytes[2], high_bytes[3], low_bytes[0], low_bytes[1], low_bytes[2], low_bytes[3]] + pub fn to_be_bytes(&self) -> [u8; 8] { + let mut bytes = [0u8; 8]; + let high_bytes = self.1.to_be_bytes(); + let low_bytes = self.0.to_be_bytes(); + + bytes[0] = high_bytes[0]; + bytes[1] = high_bytes[1]; + bytes[2] = high_bytes[2]; + bytes[3] = high_bytes[3]; + bytes[4] = low_bytes[0]; + bytes[5] = low_bytes[1]; + bytes[6] = low_bytes[2]; + bytes[7] = low_bytes[3]; + + bytes + } + + /// Deserialize from big-endian bytes with CRC check + /// Format: [high_bytes[0], high_bytes[1], high_bytes[2], high_bytes[3], low_bytes[0], low_bytes[1], low_bytes[2], low_bytes[3]] + pub fn from_be_bytes(bytes: [u8; 8]) -> Result { + let high_bytes = [bytes[0], bytes[1], bytes[2], bytes[3]]; + let low_bytes = [bytes[4], bytes[5], bytes[6], bytes[7]]; + + let transit_high = Transit_U26::from_be_bytes(high_bytes)?; + let transit_low = Transit_U26::from_be_bytes(low_bytes)?; + Ok(Self(transit_low, transit_high)) + } +} + +impl From for DateTime { + fn from(value: Transit_DateTimeUtc) -> Self { + value.into_datetime().unwrap() + } +} + +impl TryFrom> for Transit_DateTimeUtc { + type Error = DateTimeUtcError; + + fn try_from(value: DateTime) -> Result { + Self::new(value) + } +} + +impl From for u64 { + fn from(value: Transit_DateTimeUtc) -> Self { + let low_26: u32 = value.0.into(); + let high_26: u32 = value.1.into(); + ((high_26 as u64) << 26) | (low_26 as u64) + } +} + + +#[cfg(test)] +mod timestamp_test { + use super::*; + use chrono::{DateTime, TimeZone, Utc}; + + #[test] + fn store_datetimeutc_basic_operations() { + // Test with Unix epoch + let epoch = Utc.timestamp_millis_opt(0).unwrap(); + let stored = Store_DateTimeUtc::new(epoch).unwrap(); + let retrieved: DateTime = stored.into(); + assert_eq!(retrieved, epoch); + + // Test with a specific date (within 52-bit range) + let dt = Utc.with_ymd_and_hms(2020, 1, 15, 12, 30, 45).unwrap(); + let stored = Store_DateTimeUtc::new(dt).unwrap(); + let retrieved: DateTime = stored.into(); + assert_eq!(retrieved, dt); + } + + #[test] + fn store_datetimeutc_serialization() { + let dt = Utc.with_ymd_and_hms(2020, 5, 20, 8, 15, 30).unwrap(); + let stored = Store_DateTimeUtc::new(dt).unwrap(); + let bytes = stored.to_be_bytes(); + + // Deserialize and verify + let deserialized = Store_DateTimeUtc::from_be_bytes(bytes).unwrap(); + let retrieved: DateTime = deserialized.into(); + assert_eq!(retrieved, dt); + } + + #[test] + fn store_datetimeutc_error_correction() { + let dt = Utc.with_ymd_and_hms(2020, 3, 10, 14, 25, 10).unwrap(); + let stored = Store_DateTimeUtc::new(dt).unwrap(); + let mut bytes = stored.to_be_bytes(); + + // Introduce a single bit error in low bytes that can be corrected + bytes[7] ^= 0x01; + let deserialized = Store_DateTimeUtc::from_be_bytes(bytes).unwrap(); + let retrieved: DateTime = deserialized.into(); + assert_eq!(retrieved, dt); + } + + #[test] + fn store_datetimeutc_overflow() { + // Test with timestamp beyond max value + let max_timestamp = Store_DateTimeUtc::MAX_TIMESTAMP_MS; + let dt = Utc.timestamp_millis_opt(max_timestamp as i64 + 1).unwrap(); + let result = Store_DateTimeUtc::new(dt); + assert!(matches!(result, Err(DateTimeUtcError::Overflow))); + } + + #[test] + fn store_datetimeutc_borrowed_method() { + let dt = Utc.with_ymd_and_hms(2020, 7, 22, 18, 45, 20).unwrap(); + let stored = Store_DateTimeUtc::new(dt).unwrap(); + + // Test the borrowed to_datetime method + let retrieved = stored.to_datetime().unwrap(); + assert_eq!(retrieved, dt); + } + + #[test] + fn store_datetimeutc_try_from() { + let dt = Utc.with_ymd_and_hms(2020, 9, 15, 10, 10, 10).unwrap(); + let stored: Store_DateTimeUtc = dt.try_into().unwrap(); + let retrieved: DateTime = stored.into(); + assert_eq!(retrieved, dt); + } + + #[test] + fn store_datetimeutc_crc_error_detection() { + let dt = Utc.with_ymd_and_hms(2020, 12, 25, 0, 0, 0).unwrap(); + let stored = Store_DateTimeUtc::new(dt).unwrap(); + let mut bytes = stored.to_be_bytes(); + + // Introduce multiple bit errors in low bytes that can't be corrected + bytes[5] ^= 0x01; + bytes[6] ^= 0x01; + let result = Store_DateTimeUtc::from_be_bytes(bytes); + assert!(result.is_err()); + } + + #[test] + fn transit_datetimeutc_basic_operations() { + // Test with Unix epoch + let epoch = Utc.timestamp_millis_opt(0).unwrap(); + let transit = Transit_DateTimeUtc::new(epoch).unwrap(); + let retrieved: DateTime = transit.into(); + assert_eq!(retrieved, epoch); + + // Test with a specific date (within 52-bit range) + let dt = Utc.with_ymd_and_hms(2020, 1, 15, 12, 30, 45).unwrap(); + let transit = Transit_DateTimeUtc::new(dt).unwrap(); + let retrieved: DateTime = transit.into(); + assert_eq!(retrieved, dt); + } + + #[test] + fn transit_datetimeutc_serialization() { + let dt = Utc.with_ymd_and_hms(2020, 5, 20, 8, 15, 30).unwrap(); + let transit = Transit_DateTimeUtc::new(dt).unwrap(); + let bytes = transit.to_be_bytes(); + + // Deserialize and verify + let deserialized = Transit_DateTimeUtc::from_be_bytes(bytes).unwrap(); + let retrieved: DateTime = deserialized.into(); + assert_eq!(retrieved, dt); + } + + #[test] + fn transit_datetimeutc_crc_error_detection() { + let dt = Utc.with_ymd_and_hms(2020, 12, 25, 0, 0, 0).unwrap(); + let transit = Transit_DateTimeUtc::new(dt).unwrap(); + let mut bytes = transit.to_be_bytes(); + + // Introduce bit errors in low bytes that will cause CRC mismatch + bytes[4] ^= 0x01; + bytes[5] ^= 0x01; + let result = Transit_DateTimeUtc::from_be_bytes(bytes); + assert!(result.is_err()); + } + + #[test] + fn transit_datetimeutc_overflow() { + // Test with timestamp beyond max value + let max_timestamp = Transit_DateTimeUtc::MAX_TIMESTAMP_MS; + let dt = Utc.timestamp_millis_opt(max_timestamp as i64 + 1).unwrap(); + let result = Transit_DateTimeUtc::new(dt); + assert!(matches!(result, Err(DateTimeUtcError::Overflow))); + } + + #[test] + fn transit_datetimeutc_borrowed_method() { + let dt = Utc.with_ymd_and_hms(2020, 7, 22, 18, 45, 20).unwrap(); + let transit = Transit_DateTimeUtc::new(dt).unwrap(); + + // Test the borrowed to_datetime method + let retrieved = transit.to_datetime().unwrap(); + assert_eq!(retrieved, dt); + } + + #[test] + fn transit_datetimeutc_try_from() { + let dt = Utc.with_ymd_and_hms(2020, 9, 15, 10, 10, 10).unwrap(); + let transit: Transit_DateTimeUtc = dt.try_into().unwrap(); + let retrieved: DateTime = transit.into(); + assert_eq!(retrieved, dt); + } +} diff --git a/lib-bms-protocol/src/types.rs b/lib-bms-protocol/src/types.rs index b25aeca..8f6ab68 100644 --- a/lib-bms-protocol/src/types.rs +++ b/lib-bms-protocol/src/types.rs @@ -117,7 +117,7 @@ impl Stored_U26 { impl From for u32 { fn from(value: Stored_U26) -> Self { - value.0 & U26_VALUE_MASK >> 6 + (value.0 & U26_VALUE_MASK) >> 6 } } @@ -525,7 +525,10 @@ mod base_type_tests { assert_eq!(Stored_U26::from_be_bytes(bytes, crc).unwrap(), data); // introduce second bit error bytes[0] = bytes[0] ^ 0x01; - assert_eq!(Stored_U26::from_be_bytes(bytes, crc).unwrap_err(), U26Error::IncorrectibleError); + assert_eq!( + Stored_U26::from_be_bytes(bytes, crc).unwrap_err(), + U26Error::IncorrectibleError + ); } #[test]