Merge branch 'feature/base-type-error-correcting'
This commit is contained in:
Generated
+10
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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 }
|
||||
|
||||
|
||||
+44
-19
@@ -1,8 +1,18 @@
|
||||
#![no_std]
|
||||
|
||||
#[cfg(test)]
|
||||
extern crate std;
|
||||
|
||||
extern crate chrono;
|
||||
|
||||
mod types;
|
||||
mod timestamp;
|
||||
|
||||
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 {
|
||||
@@ -22,6 +32,8 @@ pub trait BmsWriteable {
|
||||
pub enum BmsProtocolError {
|
||||
#[error("i2c communication failed")]
|
||||
I2cCommunicationError,
|
||||
#[error("checksum error in received data")]
|
||||
ChecksumError,
|
||||
}
|
||||
|
||||
impl<E> From<E> for BmsProtocolError
|
||||
@@ -33,6 +45,15 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::types::U26Error> for BmsProtocolError {
|
||||
fn from(error: crate::types::U26Error) -> Self {
|
||||
match error {
|
||||
crate::types::U26Error::ChecksumError => Self::ChecksumError,
|
||||
_ => Self::I2cCommunicationError,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct BmsSoftwareReset;
|
||||
|
||||
@@ -68,7 +89,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(transit.into()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +113,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(transit.into()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,9 +145,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: 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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -162,11 +185,12 @@ 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),
|
||||
temperature_celcius: i32::from_be_bytes(temperature_celcius),
|
||||
health_percent: u32::from_be_bytes(health_percent),
|
||||
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: crate::types::Transit_I26::from_be_bytes(temperature_celcius)?
|
||||
.into(),
|
||||
health_percent: Transit_U26::from_be_bytes(health_percent)?.into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -189,9 +213,9 @@ impl BmsReadable for ChargeInfoWindowSec {
|
||||
Operation::Read(&mut charge_info_window_sec),
|
||||
],
|
||||
)?;
|
||||
Ok(ChargeInfoWindowSec(u32::from_be_bytes(
|
||||
charge_info_window_sec,
|
||||
)))
|
||||
Ok(ChargeInfoWindowSec(
|
||||
Transit_U26::from_be_bytes(charge_info_window_sec)?.into(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,11 +225,12 @@ impl BmsWriteable for ChargeInfoWindowSec {
|
||||
I: I2c<SevenBitAddress>,
|
||||
{
|
||||
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(())
|
||||
@@ -245,11 +270,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: 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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,625 @@
|
||||
//! # 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%.
|
||||
|
||||
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,
|
||||
ChecksumError,
|
||||
}
|
||||
|
||||
#[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.
|
||||
///
|
||||
/// 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);
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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], crc: u8) -> Result<Self, U26Error> {
|
||||
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 {
|
||||
let value = Self::new(value ^ dc);
|
||||
if value.calc_crc6() == crc {
|
||||
Ok(value)
|
||||
} else {
|
||||
Err(U26Error::IncorrectibleError)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Stored_U26> for u32 {
|
||||
fn from(value: Stored_U26) -> Self {
|
||||
(value.0 & U26_VALUE_MASK) >> 6
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<u32> for Stored_U26 {
|
||||
type Error = U26Error;
|
||||
|
||||
fn try_from(value: u32) -> Result<Self, Self::Error> {
|
||||
if value > U26_MAX_VALUE {
|
||||
Err(U26Error::Overflow)
|
||||
} else {
|
||||
Ok(Self::new(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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
|
||||
///
|
||||
/// this type is stored in u32 in the following form:
|
||||
/// | 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)
|
||||
}
|
||||
|
||||
pub fn from_be_bytes(bytes: [u8; 4]) -> Result<Self, U26Error> {
|
||||
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<Transit_U26> for u32 {
|
||||
fn from(value: Transit_U26) -> Self {
|
||||
(value.0 & U26_VALUE_MASK) >> 6
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<u32> for Transit_U26 {
|
||||
type Error = U26Error;
|
||||
|
||||
fn try_from(value: u32) -> Result<Self, Self::Error> {
|
||||
if value > U26_MAX_VALUE {
|
||||
Err(U26Error::Overflow)
|
||||
} else {
|
||||
Ok(Transit_U26::new(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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
|
||||
/// 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<Self, U26Error> {
|
||||
// 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<Self, U26Error> {
|
||||
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<Transit_I26> 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<i32> for Transit_I26 {
|
||||
type Error = U26Error;
|
||||
|
||||
fn try_from(value: i32) -> Result<Self, Self::Error> {
|
||||
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 | | | | |
|
||||
// 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))
|
||||
& 0x1;
|
||||
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))
|
||||
& 0x1;
|
||||
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))
|
||||
& 0x1;
|
||||
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))
|
||||
& 0x1;
|
||||
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))
|
||||
& 0x1;
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
/// CRC-6 calculation using polynomial 0x21 (MSB first)
|
||||
///
|
||||
/// 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
|
||||
/// 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
|
||||
pub fn calc_crc6(data: u32) -> u8 {
|
||||
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 {
|
||||
crc = (crc << 1) & 0x3F;
|
||||
}
|
||||
}
|
||||
crc
|
||||
}
|
||||
|
||||
/// 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 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 {
|
||||
check_crc = (check_crc << 1) & 0x3F;
|
||||
}
|
||||
}
|
||||
check_crc == crc
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod base_type_tests {
|
||||
use super::{
|
||||
calc_crc6, calc_hemming, check_crc6, check_hemming, Stored_U26, Transit_I26, Transit_U26,
|
||||
U26_MAX_VALUE,
|
||||
};
|
||||
use crate::types::U26Error;
|
||||
|
||||
#[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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
#[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, crc).unwrap(), data);
|
||||
// introduce 1 bit error
|
||||
bytes[2] = bytes[2] ^ 0x01;
|
||||
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]
|
||||
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 calc crc6 and check crc6 agree on validity
|
||||
let data = 0x123456;
|
||||
let crc = calc_crc6(data);
|
||||
assert!(check_crc6(data, crc))
|
||||
}
|
||||
|
||||
#[test]
|
||||
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]
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
#[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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user