From 0437d30d04032ca8c52227b77073b834329834bb Mon Sep 17 00:00:00 2001 From: ju6ge Date: Wed, 25 Mar 2026 14:31:18 +0100 Subject: [PATCH 1/4] 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 2/4] 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 3/4] 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 4/4] 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()); + } }