diff --git a/lib-bms-protocol/src/lib.rs b/lib-bms-protocol/src/lib.rs index 3704cd9..a091787 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 { @@ -27,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 @@ -38,6 +42,15 @@ where } } +impl From for BmsProtocolError { + fn from(error: crate::types::U26Error) -> Self { + match error { + crate::types::U26Error::ChecksumError => Self::ChecksumError, + _ => Self::I2cCommunicationError, + } + } +} + #[derive(Debug)] pub struct BmsSoftwareReset; @@ -73,7 +86,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())) } } @@ -96,7 +110,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())) } } @@ -127,9 +142,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(), }) } } @@ -167,11 +182,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(), }) } } @@ -194,9 +210,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(), + )) } } @@ -206,11 +222,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 +267,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(), }) } } 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()); + } }