Merge branch 'feature/timestamp-store-transit-u52' into feature/base-type-error-correcting

This commit is contained in:
2026-04-12 22:29:59 +02:00
5 changed files with 398 additions and 5 deletions
Generated
+10
View File
@@ -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",
]
+1
View File
@@ -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 }
+3
View File
@@ -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;
+373
View File
@@ -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<U26Error> 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<Utc> 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<Utc>
pub fn new(dt: DateTime<Utc>) -> Result<Self, DateTimeUtcError> {
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<Utc> value
pub fn into_datetime(self) -> Result<DateTime<Utc>, 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<Utc> value (borrowed)
pub fn to_datetime(&self) -> Result<DateTime<Utc>, 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<Self, DateTimeUtcError> {
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<Store_DateTimeUtc> for DateTime<Utc> {
fn from(value: Store_DateTimeUtc) -> Self {
value.into_datetime().unwrap()
}
}
impl TryFrom<DateTime<Utc>> for Store_DateTimeUtc {
type Error = DateTimeUtcError;
fn try_from(value: DateTime<Utc>) -> Result<Self, Self::Error> {
Self::new(value)
}
}
impl From<Store_DateTimeUtc> 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<Utc> 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<Utc>
pub fn new(dt: DateTime<Utc>) -> Result<Self, DateTimeUtcError> {
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<Utc> value
pub fn into_datetime(self) -> Result<DateTime<Utc>, 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<Utc> value (borrowed)
pub fn to_datetime(&self) -> Result<DateTime<Utc>, 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<Self, DateTimeUtcError> {
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<Transit_DateTimeUtc> for DateTime<Utc> {
fn from(value: Transit_DateTimeUtc) -> Self {
value.into_datetime().unwrap()
}
}
impl TryFrom<DateTime<Utc>> for Transit_DateTimeUtc {
type Error = DateTimeUtcError;
fn try_from(value: DateTime<Utc>) -> Result<Self, Self::Error> {
Self::new(value)
}
}
impl From<Transit_DateTimeUtc> 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<Utc> = 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<Utc> = 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<Utc> = 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<Utc> = 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<Utc> = 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<Utc> = 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<Utc> = 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<Utc> = 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<Utc> = transit.into();
assert_eq!(retrieved, dt);
}
}
+11 -5
View File
@@ -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.
@@ -116,7 +117,7 @@ impl Stored_U26 {
impl From<Stored_U26> for u32 {
fn from(value: Stored_U26) -> Self {
value.0 & U26_VALUE_MASK >> 6
(value.0 & U26_VALUE_MASK) >> 6
}
}
@@ -132,7 +133,8 @@ impl TryFrom<u32> 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<u32> 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
@@ -522,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]