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]