From 8f7619b9f1200597e4a2d4e5558b124236358dd8 Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Sun, 25 Jan 2026 21:12:39 +0800 Subject: [PATCH 01/14] smart_battery: Add basic SBS protocol support Add a new --smartbattery command to read basic Smart Battery System (SBS) information via I2C passthrough to the EC. This includes: - Battery mode, serial number, manufacture date - Temperature, voltage, individual cell voltages - Cycle count, device name, manufacturer name Reference: TI Smart Battery System specification Signed-off-by: Daniel Schaefer --- framework_lib/src/commandline/clap_std.rs | 5 + framework_lib/src/commandline/mod.rs | 9 ++ framework_lib/src/commandline/uefi.rs | 1 + framework_lib/src/lib.rs | 2 + framework_lib/src/smart_battery.rs | 116 ++++++++++++++++++ .../completions/bash/framework_tool | 6 +- .../completions/fish/framework_tool.fish | 2 + .../completions/zsh/_framework_tool | 2 + 8 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 framework_lib/src/smart_battery.rs diff --git a/framework_lib/src/commandline/clap_std.rs b/framework_lib/src/commandline/clap_std.rs index 6c8ffba0..2b1f932e 100644 --- a/framework_lib/src/commandline/clap_std.rs +++ b/framework_lib/src/commandline/clap_std.rs @@ -53,6 +53,10 @@ struct ClapCli { #[arg(long)] power: bool, + /// Show detailed smart battery information + #[arg(long)] + smartbattery: bool, + /// Print thermal information (Temperatures and Fan speed) #[arg(long)] thermal: bool, @@ -495,6 +499,7 @@ pub fn parse(args: &[String]) -> Cli { device: args.device, compare_version: args.compare_version, power: args.power, + smartbattery: args.smartbattery, thermal: args.thermal, sensors: args.sensors, fansetduty, diff --git a/framework_lib/src/commandline/mod.rs b/framework_lib/src/commandline/mod.rs index f2b0c693..9f9d3b44 100644 --- a/framework_lib/src/commandline/mod.rs +++ b/framework_lib/src/commandline/mod.rs @@ -56,6 +56,8 @@ use crate::nvme; use crate::os_specific; use crate::parade_retimer; use crate::power; +#[cfg(not(feature = "uefi"))] +use crate::smart_battery::SmartBattery; use crate::smbios; use crate::smbios::ConfigDigit0; use crate::smbios::{get_smbios, is_framework}; @@ -186,6 +188,7 @@ pub struct Cli { pub device: Option, pub compare_version: Option, pub power: bool, + pub smartbattery: bool, pub thermal: bool, pub sensors: bool, pub fansetduty: Option<(Option, u32)>, @@ -1593,6 +1596,12 @@ pub fn run_with_args(args: &Cli, _allupdate: bool) -> i32 { print_board_ids(&ec); } else if args.power { return power::get_and_print_power_info(&ec); + } else if args.smartbattery { + #[cfg(not(feature = "uefi"))] + { + let bat = SmartBattery::new(); + print_err(bat.dump_data(&ec)); + } } else if args.thermal { power::print_thermal(&ec); } else if args.sensors { diff --git a/framework_lib/src/commandline/uefi.rs b/framework_lib/src/commandline/uefi.rs index 0b2701c2..d0f7559b 100644 --- a/framework_lib/src/commandline/uefi.rs +++ b/framework_lib/src/commandline/uefi.rs @@ -40,6 +40,7 @@ pub fn parse(args: &[String]) -> Cli { device: None, compare_version: None, power: false, + smartbattery: false, thermal: false, sensors: false, fansetduty: None, diff --git a/framework_lib/src/lib.rs b/framework_lib/src/lib.rs index 5da49205..64d6cb5f 100644 --- a/framework_lib/src/lib.rs +++ b/framework_lib/src/lib.rs @@ -61,6 +61,8 @@ pub mod fw_uefi; mod os_specific; pub mod parade_retimer; pub mod power; +#[cfg(not(feature = "uefi"))] +pub mod smart_battery; pub mod smbios; mod util; diff --git a/framework_lib/src/smart_battery.rs b/framework_lib/src/smart_battery.rs new file mode 100644 index 00000000..70979b20 --- /dev/null +++ b/framework_lib/src/smart_battery.rs @@ -0,0 +1,116 @@ +// Smart Battery System (SBS) protocol support +// Reference: https://www.ti.com/lit/ug/sluua43a/sluua43a.pdf +// Based on driver/battery/smart.c and include/battery_smart.h from EC codebase + +use alloc::string::String; + +use crate::chromium_ec::i2c_passthrough::*; +use crate::chromium_ec::{CrosEc, EcResult}; + +#[repr(u16)] +enum SmartBatReg { + Mode = 0x03, + Temp = 0x08, + Voltage = 0x09, + CycleCount = 0x17, + ManufactureDate = 0x1B, + SerialNum = 0x1C, + ManufacturerName = 0x20, + DeviceName = 0x21, + CellVoltage1 = 0x3C, + CellVoltage2 = 0x3D, + CellVoltage3 = 0x3E, + CellVoltage4 = 0x3F, +} + +pub struct SmartBattery { + i2c_port: u8, + i2c_addr: u16, +} + +impl Default for SmartBattery { + fn default() -> Self { + Self::new() + } +} + +impl SmartBattery { + pub fn new() -> Self { + SmartBattery { + // Same on all our Nuvoton ECs + i2c_port: 3, + // 0x0B 7-bit, 0x16 8-bit address + // Same for all our batteries, they use the same IC + i2c_addr: 0x16, + } + } + + fn read_i16(&self, ec: &CrosEc, addr: u16) -> EcResult { + let i2c_response = i2c_read(ec, self.i2c_port, self.i2c_addr >> 1, addr, 0x02)?; + i2c_response.is_successful()?; + Ok(u16::from_le_bytes([ + i2c_response.data[0], + i2c_response.data[1], + ])) + } + + fn read_string(&self, ec: &CrosEc, addr: u16) -> EcResult { + // SMBus strings are length-prefixed + let i2c_response = i2c_read(ec, self.i2c_port, self.i2c_addr >> 1, addr, 0x20)?; + i2c_response.is_successful()?; + // First byte is the returned string length + let str_bytes = &i2c_response.data[1..=(i2c_response.data[0] as usize)]; + Ok(String::from_utf8_lossy(str_bytes).to_string()) + } + + /// Print basic battery information (sealed data, no unseal required) + pub fn dump_data(&self, ec: &CrosEc) -> EcResult<()> { + println!( + "Mode: 0x{:04X}", + self.read_i16(ec, SmartBatReg::Mode as u16)? + ); + println!( + "Serial Num: {:04X}", + self.read_i16(ec, SmartBatReg::SerialNum as u16)? + ); + + // ManufactureDate format: Year*512 + Month*32 + Day (year offset from 1980) + let mfg_date = self.read_i16(ec, SmartBatReg::ManufactureDate as u16)?; + let day = mfg_date & 0x1F; + let month = (mfg_date >> 5) & 0x0F; + let year = (mfg_date >> 9) + 1980; + println!("Manuf Date: {:04}-{:02}-{:02}", year, month, day); + + // Temperature is in 0.1K units, convert to Celsius + let temp = self.read_i16(ec, SmartBatReg::Temp as u16)?; + let temp_c = (temp as f32 / 10.0) - 273.15; + println!("Temperature: {:.1}C", temp_c); + + let voltage = self.read_i16(ec, SmartBatReg::Voltage as u16)?; + println!("Voltage: {}.{:03}V", voltage / 1000, voltage % 1000); + + let cell1_v = self.read_i16(ec, SmartBatReg::CellVoltage1 as u16)?; + println!(" Cell 1: {}.{:03}V", cell1_v / 1000, cell1_v % 1000); + let cell2_v = self.read_i16(ec, SmartBatReg::CellVoltage2 as u16)?; + println!(" Cell 2: {}.{:03}V", cell2_v / 1000, cell2_v % 1000); + let cell3_v = self.read_i16(ec, SmartBatReg::CellVoltage3 as u16)?; + println!(" Cell 3: {}.{:03}V", cell3_v / 1000, cell3_v % 1000); + let cell4_v = self.read_i16(ec, SmartBatReg::CellVoltage4 as u16)?; + println!(" Cell 4: {}.{:03}V", cell4_v / 1000, cell4_v % 1000); + + println!( + "Cycle Count: {}", + self.read_i16(ec, SmartBatReg::CycleCount as u16)? + ); + println!( + "Device Name: {}", + self.read_string(ec, SmartBatReg::DeviceName as u16)? + ); + println!( + "Manuf Name: {}", + self.read_string(ec, SmartBatReg::ManufacturerName as u16)? + ); + + Ok(()) + } +} diff --git a/framework_tool/completions/bash/framework_tool b/framework_tool/completions/bash/framework_tool index 45cbd9bc..e542c17e 100755 --- a/framework_tool/completions/bash/framework_tool +++ b/framework_tool/completions/bash/framework_tool @@ -23,7 +23,7 @@ _framework_tool() { case "${cmd}" in framework_tool) - opts="-v -q -t -f -h --flash-gpu-descriptor --verbose --quiet --versions --version --features --esrt --device --compare-version --power --thermal --sensors --fansetduty --fansetrpm --autofanctrl --pdports --pdports-chromebook --info --meinfo --pd-info --pd-reset --pd-disable --pd-enable --dp-hdmi-info --dp-hdmi-update --audio-card-info --privacy --pd-bin --ec-bin --capsule --dump --h2o-capsule --dump-ec-flash --flash-full-ec --flash-ec --flash-ro-ec --flash-rw-ec --intrusion --inputdeck --inputdeck-mode --expansion-bay --charge-limit --charge-current-limit --charge-rate-limit --get-gpio --fp-led-level --fp-brightness --kblight --remap-key --rgbkbd --ps2-enable --tablet-mode --touchscreen-enable --haptic-intensity --click-force --stylus-battery --console --reboot-ec --ec-hib-delay --uptimeinfo --s0ix-counter --hash --driver --pd-addrs --pd-ports --test --test-retimer --boardid --force --dry-run --flash-gpu-descriptor-file --dump-gpu-descriptor-file --validate-gpu-descriptor-file --nvidia --host-command --generate-completions --help" + opts="-v -q -t -f -h --flash-gpu-descriptor --verbose --quiet --versions --version --features --esrt --device --compare-version --power --smartbattery --smartbattery-auth --thermal --sensors --fansetduty --fansetrpm --autofanctrl --pdports --pdports-chromebook --info --meinfo --pd-info --pd-reset --pd-disable --pd-enable --dp-hdmi-info --dp-hdmi-update --audio-card-info --privacy --pd-bin --ec-bin --capsule --dump --h2o-capsule --dump-ec-flash --flash-full-ec --flash-ec --flash-ro-ec --flash-rw-ec --intrusion --inputdeck --inputdeck-mode --expansion-bay --charge-limit --charge-current-limit --charge-rate-limit --get-gpio --fp-led-level --fp-brightness --kblight --remap-key --rgbkbd --ps2-enable --tablet-mode --touchscreen-enable --haptic-intensity --click-force --stylus-battery --console --reboot-ec --ec-hib-delay --uptimeinfo --s0ix-counter --hash --driver --pd-addrs --pd-ports --test --test-retimer --boardid --force --dry-run --flash-gpu-descriptor-file --dump-gpu-descriptor-file --validate-gpu-descriptor-file --nvidia --host-command --generate-completions --help" if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -41,6 +41,10 @@ _framework_tool() { COMPREPLY=($(compgen -f "${cur}")) return 0 ;; + --smartbattery) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; --fansetduty) COMPREPLY=($(compgen -f "${cur}")) return 0 diff --git a/framework_tool/completions/fish/framework_tool.fish b/framework_tool/completions/fish/framework_tool.fish index 465f1cde..815f2b4e 100644 --- a/framework_tool/completions/fish/framework_tool.fish +++ b/framework_tool/completions/fish/framework_tool.fish @@ -8,6 +8,7 @@ rtm23\t'' ac-left\t'' ac-right\t''" complete -c framework_tool -l compare-version -r +complete -c framework_tool -l smartbattery -d 'Show detailed smart battery information, or load from dump file' -r -F complete -c framework_tool -l fansetduty -d 'Set fan duty cycle (0-100%)' -r complete -c framework_tool -l fansetrpm -d 'Set fan RPM (limited by EC fan table max RPM)' -r complete -c framework_tool -l autofanctrl -d 'Turn on automatic fan speed control' -r @@ -88,6 +89,7 @@ complete -c framework_tool -l version -d 'Show tool version information (Add -vv complete -c framework_tool -l features -d 'Show features support by the firmware' complete -c framework_tool -l esrt -d 'Display the UEFI ESRT table' complete -c framework_tool -l power -d 'Show current power status of battery and AC (Add -vv for more details)' +complete -c framework_tool -l smartbattery-auth -d 'Authenticate smart battery (requires unseal and auth keys)' complete -c framework_tool -l thermal -d 'Print thermal information (Temperatures and Fan speed)' complete -c framework_tool -l sensors -d 'Print sensor information (ALS, G-Sensor)' complete -c framework_tool -l pdports -d 'Show USB-C PD port state' diff --git a/framework_tool/completions/zsh/_framework_tool b/framework_tool/completions/zsh/_framework_tool index 324c6286..edc7b7eb 100644 --- a/framework_tool/completions/zsh/_framework_tool +++ b/framework_tool/completions/zsh/_framework_tool @@ -18,6 +18,7 @@ _framework_tool() { '--flash-gpu-descriptor=[]: :_default: :_default' \ '--device=[]:DEVICE:(bios ec pd0 pd1 rtm01 rtm23 ac-left ac-right)' \ '--compare-version=[]:COMPARE_VERSION:_default' \ +'--smartbattery=[Show detailed smart battery information, or load from dump file]::FILE:_files' \ '*--fansetduty=[Set fan duty cycle (0-100%)]::FANSETDUTY:_default' \ '*--fansetrpm=[Set fan RPM (limited by EC fan table max RPM)]::FANSETRPM:_default' \ '--autofanctrl=[Turn on automatic fan speed control]::AUTOFANCTRL:_default' \ @@ -72,6 +73,7 @@ _framework_tool() { '--features[Show features support by the firmware]' \ '--esrt[Display the UEFI ESRT table]' \ '--power[Show current power status of battery and AC (Add -vv for more details)]' \ +'--smartbattery-auth[Authenticate smart battery (requires unseal and auth keys)]' \ '--thermal[Print thermal information (Temperatures and Fan speed)]' \ '--sensors[Print sensor information (ALS, G-Sensor)]' \ '--pdports[Show USB-C PD port state]' \ From 4bdb620e5d8936ffe53603d7d04f3412ffee8cda Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Sun, 25 Jan 2026 21:22:40 +0800 Subject: [PATCH 02/14] smart_battery: Add ManufacturerAccess and LifeTime decoding Add support for reading TI BQ40z50 ManufacturerAccess registers: - Unseal/seal commands for protected data access - OperationStatus, SafetyAlert/Status, PFAlert/Status registers - LifeTimeDataBlocks 1-5 with detailed field decoding - State of Health (remaining capacity in mAh/Wh) Features: - Hidden input for unseal key (no terminal echo) - Multi-line operation status display with flag decoding - Detailed lifetime statistics (voltages, temps, currents, events) Requires nix "term" feature for Unix terminal control. Signed-off-by: Daniel Schaefer --- framework_lib/Cargo.toml | 4 +- framework_lib/src/smart_battery.rs | 723 ++++++++++++++++++++++++++++- 2 files changed, 720 insertions(+), 7 deletions(-) diff --git a/framework_lib/Cargo.toml b/framework_lib/Cargo.toml index 9a6a63c7..32a00a83 100644 --- a/framework_lib/Cargo.toml +++ b/framework_lib/Cargo.toml @@ -57,7 +57,9 @@ nvml-wrapper = { version = "0.11.0", optional = true } [target.'cfg(unix)'.dependencies] libc = "0.2.155" -nix = { version = "0.30", features = ["ioctl", "user"] } +nix = { version = "0.30", features = ["ioctl", "user", "term"] } +redox_hwio = { git = "https://github.com/FrameworkComputer/rust-hwio", branch = "freebsd" } +smbios-lib = { git = "https://github.com/FrameworkComputer/smbios-lib.git", branch = "no-std" } env_logger = "0.11" clap = { version = "4.5", features = ["derive", "cargo"] } clap-num = { version = "1.2.0" } diff --git a/framework_lib/src/smart_battery.rs b/framework_lib/src/smart_battery.rs index 70979b20..566f2cd7 100644 --- a/framework_lib/src/smart_battery.rs +++ b/framework_lib/src/smart_battery.rs @@ -3,9 +3,12 @@ // Based on driver/battery/smart.c and include/battery_smart.h from EC codebase use alloc::string::String; +use alloc::vec::Vec; + +use std::io::{self, Write}; use crate::chromium_ec::i2c_passthrough::*; -use crate::chromium_ec::{CrosEc, EcResult}; +use crate::chromium_ec::{CrosEc, EcError, EcResult}; #[repr(u16)] enum SmartBatReg { @@ -23,6 +26,377 @@ enum SmartBatReg { CellVoltage4 = 0x3F, } +/// ManufacturerAccess block registers +/// Needs unseal for access +/// On EC Console can read these with battmfgacc command if CONFIG_CMD_BATT_MFG_ACCESS +#[repr(u16)] +enum ManufReg { + SafetyAlert = 0x50, + SafetyStatus = 0x51, + PFAlert = 0x52, + PFStatus = 0x53, + OperationStatus = 0x54, + LifeTimeDataBlock1 = 0x60, + LifeTimeDataBlock2 = 0x61, + LifeTimeDataBlock3 = 0x62, + LifeTimeDataBlock4 = 0x63, + LifeTimeDataBlock5 = 0x64, + Soh = 0x77, +} + +/// Decode OperationStatus register bits (BQ40z50) +/// Based on TI bq40z50 device definitions +fn decode_operation_status(value: u32) -> Vec<&'static str> { + let mut flags = Vec::new(); + if value & (1 << 0) != 0 { + flags.push("PRES (Cell Voltages OK)"); + } + if value & (1 << 1) != 0 { + flags.push("DSG (Discharge FET On)"); + } + if value & (1 << 2) != 0 { + flags.push("CHG (Charge FET On)"); + } + if value & (1 << 3) != 0 { + flags.push("PCHG (Precharge FET On)"); + } + if value & (1 << 5) != 0 { + flags.push("FUSE (Fuse Active)"); + } + if value & (1 << 7) != 0 { + flags.push("BTP_INT (Battery Trip Point)"); + } + // Bits 8-9: Security mode (00=Reserved, 01=FullAccess, 10=Unsealed, 11=Sealed) + let sec_mode = (value >> 8) & 0x03; + match sec_mode { + 1 => flags.push("SEC=FullAccess"), + 2 => flags.push("SEC=Unsealed"), + 3 => flags.push("SEC=Sealed"), + _ => {} + } + if value & (1 << 10) != 0 { + flags.push("SDV (Shutdown Low Voltage)"); + } + if value & (1 << 11) != 0 { + flags.push("SS (Safety Status)"); + } + if value & (1 << 12) != 0 { + flags.push("PF (Permanent Failure)"); + } + if value & (1 << 13) != 0 { + flags.push("XDSG (Discharge Disabled)"); + } + if value & (1 << 14) != 0 { + flags.push("XCHG (Charge Disabled)"); + } + if value & (1 << 15) != 0 { + flags.push("SLEEP (Sleep Mode)"); + } + if value & (1 << 16) != 0 { + flags.push("SDM (Shutdown Command)"); + } + if value & (1 << 17) != 0 { + flags.push("LED (LED Display)"); + } + if value & (1 << 18) != 0 { + flags.push("AUTH (Authentication)"); + } + if value & (1 << 19) != 0 { + flags.push("AUTOCALM (Auto CC Offset)"); + } + if value & (1 << 20) != 0 { + flags.push("CAL (Calibration)"); + } + if value & (1 << 21) != 0 { + flags.push("CAL_OFFSET (Cal Offset)"); + } + if value & (1 << 22) != 0 { + flags.push("XL (400kHz Mode)"); + } + if value & (1 << 23) != 0 { + flags.push("SLEEPM (Sleep Cmd Active)"); + } + if value & (1 << 24) != 0 { + flags.push("INIT (Initialization)"); + } + if value & (1 << 25) != 0 { + flags.push("SMBLCAL (SMBus Cal)"); + } + if value & (1 << 26) != 0 { + flags.push("SLPAD (Sleep via Adapter)"); + } + if value & (1 << 27) != 0 { + flags.push("SLPCC (Sleep via CC)"); + } + if value & (1 << 28) != 0 { + flags.push("CB (Cell Balancing)"); + } + if value & (1 << 29) != 0 { + flags.push("EMSHUT (Emergency Shutdown)"); + } + flags +} + +/// Decode SafetyAlert/SafetyStatus register bits (BQ40z50) +/// Based on TI BQ40z50 Technical Reference Manual +fn decode_safety_status(value: u32) -> Vec<&'static str> { + let mut flags = Vec::new(); + if value & (1 << 0) != 0 { + flags.push("CUV (Cell Under-Voltage)"); + } + if value & (1 << 1) != 0 { + flags.push("COV (Cell Over-Voltage)"); + } + if value & (1 << 2) != 0 { + flags.push("OCC1 (Over-Current Charge Tier1)"); + } + if value & (1 << 3) != 0 { + flags.push("OCC2 (Over-Current Charge Tier2)"); + } + if value & (1 << 4) != 0 { + flags.push("OCD1 (Over-Current Discharge Tier1)"); + } + if value & (1 << 5) != 0 { + flags.push("OCD2 (Over-Current Discharge Tier2)"); + } + if value & (1 << 7) != 0 { + flags.push("OCDL (Over-Current Discharge Latch)"); + } + if value & (1 << 9) != 0 { + flags.push("SCCL (Short-Circuit Charge Latch)"); + } + if value & (1 << 11) != 0 { + flags.push("SCDL (Short-Circuit Discharge Latch)"); + } + if value & (1 << 12) != 0 { + flags.push("OTC (Over-Temp Charge)"); + } + if value & (1 << 13) != 0 { + flags.push("OTD (Over-Temp Discharge)"); + } + if value & (1 << 14) != 0 { + flags.push("CUVC (Cell Under-Voltage Compensated)"); + } + if value & (1 << 16) != 0 { + flags.push("OTF (Over-Temp FET)"); + } + if value & (1 << 18) != 0 { + flags.push("PTO (Precharge Timeout)"); + } + if value & (1 << 19) != 0 { + flags.push("PTOS (Precharge Timeout Suspend)"); + } + if value & (1 << 20) != 0 { + flags.push("CTO (Charge Timeout)"); + } + if value & (1 << 21) != 0 { + flags.push("CTOS (Charge Timeout Suspend)"); + } + if value & (1 << 22) != 0 { + flags.push("OC (Overcharge)"); + } + if value & (1 << 23) != 0 { + flags.push("CHGC (Overcharge Current)"); + } + if value & (1 << 24) != 0 { + flags.push("CHGV (Overcharge Voltage)"); + } + if value & (1 << 25) != 0 { + flags.push("PCHGC (Over Precharge Current)"); + } + if value & (1 << 26) != 0 { + flags.push("UTC (Under-Temp Charge)"); + } + if value & (1 << 27) != 0 { + flags.push("UTD (Under-Temp Discharge)"); + } + flags +} + +/// Decode PFAlert/PFStatus register bits (BQ40z50) +/// Based on TI BQ40z50 Technical Reference Manual +fn decode_pf_status(value: u32) -> Vec<&'static str> { + let mut flags = Vec::new(); + if value & (1 << 0) != 0 { + flags.push("SUV (Safety Cell Under-Voltage)"); + } + if value & (1 << 1) != 0 { + flags.push("SOV (Safety Cell Over-Voltage)"); + } + if value & (1 << 2) != 0 { + flags.push("SOCC (Safety Over-Current Charge)"); + } + if value & (1 << 3) != 0 { + flags.push("SOCD (Safety Over-Current Discharge)"); + } + if value & (1 << 4) != 0 { + flags.push("SOT (Safety Over-Temp Cell)"); + } + if value & (1 << 5) != 0 { + flags.push("SOTF (Safety Over-Temp FET)"); + } + if value & (1 << 6) != 0 { + flags.push("VIMR (Voltage Imbalance at Rest)"); + } + if value & (1 << 7) != 0 { + flags.push("VIMA (Voltage Imbalance Active)"); + } + if value & (1 << 8) != 0 { + flags.push("QIM (QMax Imbalance)"); + } + if value & (1 << 9) != 0 { + flags.push("CB (Cell Balancing Failure)"); + } + if value & (1 << 10) != 0 { + flags.push("IMP (Impedance Failure)"); + } + if value & (1 << 11) != 0 { + flags.push("CD (Coulomb Counter Failure)"); + } + if value & (1 << 12) != 0 { + flags.push("FUSE (Chemical Fuse Failure)"); + } + if value & (1 << 13) != 0 { + flags.push("AFER (AFE Register Failure)"); + } + if value & (1 << 14) != 0 { + flags.push("AFEC (AFE Communication Failure)"); + } + if value & (1 << 15) != 0 { + flags.push("2LVL (Second Level Safety)"); + } + if value & (1 << 16) != 0 { + flags.push("PTC (Open PTC Failure)"); + } + if value & (1 << 17) != 0 { + flags.push("CFETF (Charge FET Failure)"); + } + if value & (1 << 18) != 0 { + flags.push("DFETF (Discharge FET Failure)"); + } + if value & (1 << 19) != 0 { + flags.push("TS1 (Open Thermistor TS1)"); + } + if value & (1 << 20) != 0 { + flags.push("TS2 (Open Thermistor TS2)"); + } + if value & (1 << 21) != 0 { + flags.push("TS3 (Open Thermistor TS3)"); + } + if value & (1 << 22) != 0 { + flags.push("TS4 (Open Thermistor TS4)"); + } + if value & (1 << 23) != 0 { + flags.push("DFW (Data Flash Wearout)"); + } + if value & (1 << 24) != 0 { + flags.push("HWMX (HW Max Cell Voltage)"); + } + flags +} + +/// Print status flags with hex value (for Safety/PF registers) +fn print_status_flags(label: &str, value: u32, flags: Vec<&str>) { + if value == 0 { + println!("{}: (OK)", label); + } else { + println!("{}: 0x{:08X}", label, value); + for flag in flags { + println!(" - {}", flag); + } + } +} + +/// Print operation status flags on multiple lines +fn print_operation_status(value: u32) { + let flags = decode_operation_status(value); + println!("Operation Status: 0x{:08X}", value); + for flag in flags { + println!(" - {}", flag); + } +} + +/// Reads a line from stdin without echoing (for sensitive input like keys) +#[cfg(unix)] +fn read_password() -> io::Result { + use nix::sys::termios::{self, LocalFlags, SetArg}; + use std::io::BufRead; + use std::os::fd::AsFd; + + let stdin = io::stdin(); + let fd = stdin.as_fd(); + + // Save original terminal settings + let original = termios::tcgetattr(fd)?; + + // Disable echo + let mut noecho = original.clone(); + noecho.local_flags.remove(LocalFlags::ECHO); + termios::tcsetattr(fd, SetArg::TCSANOW, &noecho)?; + + // Read the line + let mut input = String::new(); + let result = stdin.lock().read_line(&mut input); + + // Restore original settings + termios::tcsetattr(fd, SetArg::TCSANOW, &original)?; + + // Print newline since echo was disabled + println!(); + + result?; + Ok(input) +} + +/// Reads a line from stdin without echoing (for sensitive input like keys) +#[cfg(windows)] +fn read_password() -> io::Result { + use std::io::BufRead; + use windows::Win32::System::Console::{ + GetConsoleMode, GetStdHandle, SetConsoleMode, CONSOLE_MODE, ENABLE_ECHO_INPUT, + STD_INPUT_HANDLE, + }; + + unsafe { + let handle = GetStdHandle(STD_INPUT_HANDLE).map_err(|e| { + io::Error::new( + io::ErrorKind::Other, + format!("Failed to get stdin handle: {}", e), + ) + })?; + + let mut mode = CONSOLE_MODE::default(); + GetConsoleMode(handle, &mut mode).map_err(|e| { + io::Error::new( + io::ErrorKind::Other, + format!("Failed to get console mode: {}", e), + ) + })?; + + // Disable echo + let noecho = CONSOLE_MODE(mode.0 & !ENABLE_ECHO_INPUT.0); + SetConsoleMode(handle, noecho).map_err(|e| { + io::Error::new( + io::ErrorKind::Other, + format!("Failed to set console mode: {}", e), + ) + })?; + + // Read the line + let mut input = String::new(); + let result = io::stdin().lock().read_line(&mut input); + + // Restore original mode + let _ = SetConsoleMode(handle, mode); + + // Print newline since echo was disabled + println!(); + + result?; + Ok(input) + } +} + pub struct SmartBattery { i2c_port: u8, i2c_addr: u16, @@ -45,6 +419,29 @@ impl SmartBattery { } } + fn unseal(&self, ec: &CrosEc, key1: u16, key2: u16) -> EcResult<()> { + i2c_write_block( + ec, + self.i2c_port, + self.i2c_addr >> 1, + 0x00, + &key1.to_le_bytes(), + )?; + i2c_write_block( + ec, + self.i2c_port, + self.i2c_addr >> 1, + 0x00, + &key2.to_le_bytes(), + )?; + Ok(()) + } + + fn seal(&self, ec: &CrosEc) -> EcResult<()> { + i2c_write_block(ec, self.i2c_port, self.i2c_addr >> 1, 0x00, &[0x30, 0x00])?; + Ok(()) + } + fn read_i16(&self, ec: &CrosEc, addr: u16) -> EcResult { let i2c_response = i2c_read(ec, self.i2c_port, self.i2c_addr >> 1, addr, 0x02)?; i2c_response.is_successful()?; @@ -54,6 +451,27 @@ impl SmartBattery { ])) } + /// Read a 32-bit value from a ManufacturerAccess block command (SMBus block format with length prefix) + fn read_i32(&self, ec: &CrosEc, addr: u16) -> EcResult { + // ManufacturerAccess block commands return data in SMBus block format: + // Byte 0: Length, Bytes 1-4: Data + let i2c_response = i2c_read(ec, self.i2c_port, self.i2c_addr >> 1, addr, 0x05)?; + i2c_response.is_successful()?; + let len = i2c_response.data[0]; + if len != 4 { + return Err(EcError::DeviceError(format!( + "Expected 4 bytes but got {} from register 0x{:02X}", + len, addr + ))); + } + Ok(u32::from_le_bytes([ + i2c_response.data[1], + i2c_response.data[2], + i2c_response.data[3], + i2c_response.data[4], + ])) + } + fn read_string(&self, ec: &CrosEc, addr: u16) -> EcResult { // SMBus strings are length-prefixed let i2c_response = i2c_read(ec, self.i2c_port, self.i2c_addr >> 1, addr, 0x20)?; @@ -63,16 +481,37 @@ impl SmartBattery { Ok(String::from_utf8_lossy(str_bytes).to_string()) } - /// Print basic battery information (sealed data, no unseal required) + /// Read a block of bytes with expected length + fn read_bytes(&self, ec: &CrosEc, addr: u16, len: u16) -> EcResult> { + let i2c_response = i2c_read(ec, self.i2c_port, self.i2c_addr >> 1, addr, len + 1)?; + i2c_response.is_successful()?; + let actual_len = i2c_response.data[0]; + if actual_len != len as u8 { + return Err(EcError::DeviceError(format!( + "Expected {} bytes but got {} from register 0x{:02X}", + len, actual_len, addr + ))); + } + Ok(i2c_response.data[1..].to_vec()) + } + + /// Read a block of bytes, returning whatever length the device provides + fn read_block(&self, ec: &CrosEc, addr: u16, max_len: u16) -> EcResult> { + let i2c_response = i2c_read(ec, self.i2c_port, self.i2c_addr >> 1, addr, max_len + 1)?; + i2c_response.is_successful()?; + let actual_len = i2c_response.data[0] as usize; + Ok(i2c_response.data[1..=actual_len].to_vec()) + } + + /// Print battery information interactively (prompts for unseal key) pub fn dump_data(&self, ec: &CrosEc) -> EcResult<()> { + // Basic registers (no unseal required) println!( "Mode: 0x{:04X}", self.read_i16(ec, SmartBatReg::Mode as u16)? ); - println!( - "Serial Num: {:04X}", - self.read_i16(ec, SmartBatReg::SerialNum as u16)? - ); + let serial_num = self.read_i16(ec, SmartBatReg::SerialNum as u16)?; + println!("Serial Num: {:04X}", serial_num); // ManufactureDate format: Year*512 + Month*32 + Day (year offset from 1980) let mfg_date = self.read_i16(ec, SmartBatReg::ManufactureDate as u16)?; @@ -111,6 +550,278 @@ impl SmartBattery { self.read_string(ec, SmartBatReg::ManufacturerName as u16)? ); + // Prompt for unseal key to access ManufacturerAccess data + print!("Enter unseal key in hex (e.g. 04143672), or press enter to skip: "); + io::stdout() + .flush() + .map_err(|e| EcError::DeviceError(format!("Failed to flush stdout: {}", e)))?; + let input_text = read_password() + .map_err(|e| EcError::DeviceError(format!("Failed to read key: {}", e)))?; + let input_text = input_text.trim(); + + if !input_text.is_empty() { + let key: u32 = u32::from_str_radix(input_text, 16) + .map_err(|e| EcError::DeviceError(format!("Invalid key: {}", e)))?; + self.unseal(ec, (key >> 16) as u16, key as u16)?; + + let soh = self.read_bytes(ec, ManufReg::Soh as u16, 4)?; + println!( + "StateOfHealth: {}mAh, {}.{:02}Wh", + u16::from_le_bytes([soh[0], soh[1]]), + u16::from_le_bytes([soh[2], soh[3]]) / 100, + u16::from_le_bytes([soh[2], soh[3]]) % 100, + ); + + let operation_status = self.read_i32(ec, ManufReg::OperationStatus as u16)?; + print_operation_status(operation_status); + + let safety_alert = self.read_i32(ec, ManufReg::SafetyAlert as u16)?; + print_status_flags( + "Safety Alert", + safety_alert, + decode_safety_status(safety_alert), + ); + + let safety_status = self.read_i32(ec, ManufReg::SafetyStatus as u16)?; + print_status_flags( + "Safety Status", + safety_status, + decode_safety_status(safety_status), + ); + + let pf_alert = self.read_i32(ec, ManufReg::PFAlert as u16)?; + print_status_flags("PF Alert", pf_alert, decode_pf_status(pf_alert)); + + let pf_status = self.read_i32(ec, ManufReg::PFStatus as u16)?; + print_status_flags("PF Status", pf_status, decode_pf_status(pf_status)); + + // LifeTime Data Blocks + let lt1 = self.read_bytes(ec, ManufReg::LifeTimeDataBlock1 as u16, 32)?; + println!("LifeTime1:"); + println!( + " Cell 1 Max Voltage: {}mV", + u16::from_le_bytes([lt1[0], lt1[1]]) + ); + println!( + " Min Voltage: {}mV", + u16::from_le_bytes([lt1[8], lt1[9]]) + ); + println!( + " Cell 2 Max Voltage: {}mV", + u16::from_le_bytes([lt1[2], lt1[3]]) + ); + println!( + " Min Voltage: {}mV", + u16::from_le_bytes([lt1[10], lt1[11]]) + ); + println!( + " Cell 3 Max Voltage: {}mV", + u16::from_le_bytes([lt1[4], lt1[5]]) + ); + println!( + " Min Voltage: {}mV", + u16::from_le_bytes([lt1[12], lt1[13]]) + ); + println!( + " Cell 4 Max Voltage: {}mV", + u16::from_le_bytes([lt1[6], lt1[7]]) + ); + println!( + " Min Voltage: {}mV", + u16::from_le_bytes([lt1[14], lt1[15]]) + ); + println!( + " Max Delta Cell Voltage: {}mV", + u16::from_le_bytes([lt1[16], lt1[17]]) + ); + println!( + " Max Charge Current: {:.2}A", + u16::from_le_bytes([lt1[18], lt1[19]]) as f32 / 1000.0 + ); + println!( + " Max Discharge Current: {:.2}A", + i16::from_le_bytes([lt1[20], lt1[21]]).unsigned_abs() as f32 / 1000.0 + ); + println!( + " Max Avg Dsg Current: {:.2}A", + i16::from_le_bytes([lt1[22], lt1[23]]).unsigned_abs() as f32 / 1000.0 + ); + println!( + " Max Avg Dsg Power: {:.1}W", + u16::from_le_bytes([lt1[24], lt1[25]]) as f32 / 1000.0 + ); + println!(" Max Temp Cell: {}C", lt1[26]); + println!(" Min Temp Cell: {}C", lt1[27]); + println!(" Max Delta Cell Temp: {}C", lt1[28]); + println!(" Max Temp Int Sensor: {}C", lt1[29]); + println!(" Min Temp Int Sensor: {}C", lt1[30]); + println!(" Max Temp FET: {}C", lt1[31]); + + let lt2 = self.read_bytes(ec, ManufReg::LifeTimeDataBlock2 as u16, 20)?; + println!("LifeTime2:"); + println!(" No. of Shutdowns: {}", lt2[0]); + println!(" No. of Partial Resets: {}", lt2[1]); + println!(" No. of Full Resets: {}", lt2[2]); + println!(" No. of WDT Resets: {}", lt2[3]); + let cb1 = u32::from_le_bytes([lt2[4], lt2[5], lt2[6], lt2[7]]); + let cb2 = u32::from_le_bytes([lt2[8], lt2[9], lt2[10], lt2[11]]); + let cb3 = u32::from_le_bytes([lt2[12], lt2[13], lt2[14], lt2[15]]); + let cb4 = u32::from_le_bytes([lt2[16], lt2[17], lt2[18], lt2[19]]); + println!( + " CB Time Cell 1: {}s ({:.1}h)", + cb1, + cb1 as f64 / 3600.0 + ); + println!( + " CB Time Cell 2: {}s ({:.1}h)", + cb2, + cb2 as f64 / 3600.0 + ); + println!( + " CB Time Cell 3: {}s ({:.1}h)", + cb3, + cb3 as f64 / 3600.0 + ); + println!( + " CB Time Cell 4: {}s ({:.1}h)", + cb4, + cb4 as f64 / 3600.0 + ); + + let lt3 = self.read_block(ec, ManufReg::LifeTimeDataBlock3 as u16, 16)?; + if !lt3.is_empty() { + println!("LifeTime3:"); + if lt3.len() >= 4 { + println!( + " Total FW Runtime: {}h", + u16::from_le_bytes([lt3[0], lt3[1]]) + ); + println!( + " Time in Under Temp: {}h", + u16::from_le_bytes([lt3[2], lt3[3]]) + ); + } + if lt3.len() >= 16 { + println!( + " Time in Low Temp: {}h", + u16::from_le_bytes([lt3[4], lt3[5]]) + ); + println!( + " Time in Std Temp Low: {}h", + u16::from_le_bytes([lt3[6], lt3[7]]) + ); + println!( + " Time in Std Temp: {}h", + u16::from_le_bytes([lt3[8], lt3[9]]) + ); + println!( + " Time in Std Temp High: {}h", + u16::from_le_bytes([lt3[10], lt3[11]]) + ); + println!( + " Time in High Temp: {}h", + u16::from_le_bytes([lt3[12], lt3[13]]) + ); + println!( + " Time in Over Temp: {}h", + u16::from_le_bytes([lt3[14], lt3[15]]) + ); + } + } + + let lt4 = self.read_block(ec, ManufReg::LifeTimeDataBlock4 as u16, 32)?; + if lt4.len() >= 32 { + println!("LifeTime4:"); + println!( + " Cell Over-Voltage: {} (last @ cycle {})", + u16::from_le_bytes([lt4[0], lt4[1]]), + u16::from_le_bytes([lt4[2], lt4[3]]) + ); + println!( + " Cell Under-Voltage: {} (last @ cycle {})", + u16::from_le_bytes([lt4[4], lt4[5]]), + u16::from_le_bytes([lt4[6], lt4[7]]) + ); + println!( + " Over-Current Discharge 1: {} (last @ cycle {})", + u16::from_le_bytes([lt4[8], lt4[9]]), + u16::from_le_bytes([lt4[10], lt4[11]]) + ); + println!( + " Over-Current Discharge 2: {} (last @ cycle {})", + u16::from_le_bytes([lt4[12], lt4[13]]), + u16::from_le_bytes([lt4[14], lt4[15]]) + ); + println!( + " Over-Current Charge 1: {} (last @ cycle {})", + u16::from_le_bytes([lt4[16], lt4[17]]), + u16::from_le_bytes([lt4[18], lt4[19]]) + ); + println!( + " Over-Current Charge 2: {} (last @ cycle {})", + u16::from_le_bytes([lt4[20], lt4[21]]), + u16::from_le_bytes([lt4[22], lt4[23]]) + ); + println!( + " Open Load Detection: {} (last @ cycle {})", + u16::from_le_bytes([lt4[24], lt4[25]]), + u16::from_le_bytes([lt4[26], lt4[27]]) + ); + println!( + " Short-Circuit Discharge: {} (last @ cycle {})", + u16::from_le_bytes([lt4[28], lt4[29]]), + u16::from_le_bytes([lt4[30], lt4[31]]) + ); + } + + let lt5 = self.read_block(ec, ManufReg::LifeTimeDataBlock5 as u16, 32)?; + if lt5.len() >= 32 { + println!("LifeTime5:"); + println!( + " Short-Circuit Charge: {} (last @ cycle {})", + u16::from_le_bytes([lt5[0], lt5[1]]), + u16::from_le_bytes([lt5[2], lt5[3]]) + ); + println!( + " Over-Temp Charge: {} (last @ cycle {})", + u16::from_le_bytes([lt5[4], lt5[5]]), + u16::from_le_bytes([lt5[6], lt5[7]]) + ); + println!( + " Over-Temp Discharge: {} (last @ cycle {})", + u16::from_le_bytes([lt5[8], lt5[9]]), + u16::from_le_bytes([lt5[10], lt5[11]]) + ); + println!( + " Over-Temp FET: {} (last @ cycle {})", + u16::from_le_bytes([lt5[12], lt5[13]]), + u16::from_le_bytes([lt5[14], lt5[15]]) + ); + println!( + " Valid Charge Terminations:{} (last @ cycle {})", + u16::from_le_bytes([lt5[16], lt5[17]]), + u16::from_le_bytes([lt5[18], lt5[19]]) + ); + println!( + " QMax Updates: {} (last @ cycle {})", + u16::from_le_bytes([lt5[20], lt5[21]]), + u16::from_le_bytes([lt5[22], lt5[23]]) + ); + println!( + " Resistance Updates: {} (last @ cycle {})", + u16::from_le_bytes([lt5[24], lt5[25]]), + u16::from_le_bytes([lt5[26], lt5[27]]) + ); + println!( + " Resistance Update Fails: {} (last @ cycle {})", + u16::from_le_bytes([lt5[28], lt5[29]]), + u16::from_le_bytes([lt5[30], lt5[31]]) + ); + } + + self.seal(ec)?; + } + Ok(()) } } From da6da84bff27922e08bdf2dd94717545b1672b5d Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Sun, 25 Jan 2026 21:26:25 +0800 Subject: [PATCH 03/14] smart_battery: Add data dump/load and unit tests Add BatteryData struct for collecting and serializing battery data: - write_to_file/read_from_file for simple text-based dump format - collect_data() method to gather all data into struct - display_battery_data() function for formatted output - Refactor dump_data() to use collect_data + display_battery_data - dump_to_file() method for saving data to file Add unit tests for: - Parsing battery dump files - Lifetime data validation - Hex encode/decode roundtrip - Manufacture date decoding - Temperature unit conversion Signed-off-by: Daniel Schaefer --- framework_lib/src/smart_battery.rs | 588 ++++++++++++++++++----- framework_lib/test_bins/battery_00E3.txt | 26 + 2 files changed, 494 insertions(+), 120 deletions(-) create mode 100644 framework_lib/test_bins/battery_00E3.txt diff --git a/framework_lib/src/smart_battery.rs b/framework_lib/src/smart_battery.rs index 566f2cd7..063bf403 100644 --- a/framework_lib/src/smart_battery.rs +++ b/framework_lib/src/smart_battery.rs @@ -5,11 +5,151 @@ use alloc::string::String; use alloc::vec::Vec; -use std::io::{self, Write}; +use std::fs::File; +use std::io::{self, BufRead, BufReader, Write}; +use std::path::Path; use crate::chromium_ec::i2c_passthrough::*; use crate::chromium_ec::{CrosEc, EcError, EcResult}; +/// Raw battery data that can be dumped to a file and loaded later +#[derive(Default)] +pub struct BatteryData { + pub mode: u16, + pub serial_num: u16, + pub manufacture_date: u16, + pub temperature: u16, + pub voltage: u16, + pub cell_voltage1: u16, + pub cell_voltage2: u16, + pub cell_voltage3: u16, + pub cell_voltage4: u16, + pub cycle_count: u16, + pub device_name: String, + pub manufacturer_name: String, + // Unsealed data (may be empty if not unsealed) + pub state_of_health: Vec, + pub operation_status: u32, + pub safety_alert: u32, + pub safety_status: u32, + pub pf_alert: u32, + pub pf_status: u32, + pub lifetime1: Vec, + pub lifetime2: Vec, + pub lifetime3: Vec, + pub lifetime4: Vec, + pub lifetime5: Vec, +} + +impl BatteryData { + /// Write raw data to a file in a simple text format + pub fn write_to_file(&self, path: &Path) -> io::Result<()> { + let mut file = File::create(path)?; + writeln!(file, "# Smart Battery Raw Data Dump")?; + writeln!(file, "# Format: key=hex_value or key=string")?; + writeln!(file)?; + writeln!(file, "mode={:04X}", self.mode)?; + writeln!(file, "serial_num={:04X}", self.serial_num)?; + writeln!(file, "manufacture_date={:04X}", self.manufacture_date)?; + writeln!(file, "temperature={:04X}", self.temperature)?; + writeln!(file, "voltage={:04X}", self.voltage)?; + writeln!(file, "cell_voltage1={:04X}", self.cell_voltage1)?; + writeln!(file, "cell_voltage2={:04X}", self.cell_voltage2)?; + writeln!(file, "cell_voltage3={:04X}", self.cell_voltage3)?; + writeln!(file, "cell_voltage4={:04X}", self.cell_voltage4)?; + writeln!(file, "cycle_count={:04X}", self.cycle_count)?; + writeln!(file, "device_name={}", self.device_name)?; + writeln!(file, "manufacturer_name={}", self.manufacturer_name)?; + writeln!( + file, + "state_of_health={}", + hex_encode(&self.state_of_health) + )?; + writeln!(file, "operation_status={:08X}", self.operation_status)?; + writeln!(file, "safety_alert={:08X}", self.safety_alert)?; + writeln!(file, "safety_status={:08X}", self.safety_status)?; + writeln!(file, "pf_alert={:08X}", self.pf_alert)?; + writeln!(file, "pf_status={:08X}", self.pf_status)?; + writeln!(file, "lifetime1={}", hex_encode(&self.lifetime1))?; + writeln!(file, "lifetime2={}", hex_encode(&self.lifetime2))?; + writeln!(file, "lifetime3={}", hex_encode(&self.lifetime3))?; + writeln!(file, "lifetime4={}", hex_encode(&self.lifetime4))?; + writeln!(file, "lifetime5={}", hex_encode(&self.lifetime5))?; + Ok(()) + } + + /// Read raw data from a dump file + pub fn read_from_file(path: &Path) -> io::Result { + let file = File::open(path)?; + let reader = BufReader::new(file); + let mut data = BatteryData::default(); + + for line in reader.lines() { + let line = line?; + let line = line.trim(); + if line.is_empty() || line.starts_with('#') { + continue; + } + if let Some((key, value)) = line.split_once('=') { + match key { + "mode" => data.mode = u16::from_str_radix(value, 16).unwrap_or(0), + "serial_num" => data.serial_num = u16::from_str_radix(value, 16).unwrap_or(0), + "manufacture_date" => { + data.manufacture_date = u16::from_str_radix(value, 16).unwrap_or(0) + } + "temperature" => data.temperature = u16::from_str_radix(value, 16).unwrap_or(0), + "voltage" => data.voltage = u16::from_str_radix(value, 16).unwrap_or(0), + "cell_voltage1" => { + data.cell_voltage1 = u16::from_str_radix(value, 16).unwrap_or(0) + } + "cell_voltage2" => { + data.cell_voltage2 = u16::from_str_radix(value, 16).unwrap_or(0) + } + "cell_voltage3" => { + data.cell_voltage3 = u16::from_str_radix(value, 16).unwrap_or(0) + } + "cell_voltage4" => { + data.cell_voltage4 = u16::from_str_radix(value, 16).unwrap_or(0) + } + "cycle_count" => data.cycle_count = u16::from_str_radix(value, 16).unwrap_or(0), + "device_name" => data.device_name = value.to_string(), + "manufacturer_name" => data.manufacturer_name = value.to_string(), + "state_of_health" => data.state_of_health = hex_decode(value), + "operation_status" => { + data.operation_status = u32::from_str_radix(value, 16).unwrap_or(0) + } + "safety_alert" => { + data.safety_alert = u32::from_str_radix(value, 16).unwrap_or(0) + } + "safety_status" => { + data.safety_status = u32::from_str_radix(value, 16).unwrap_or(0) + } + "pf_alert" => data.pf_alert = u32::from_str_radix(value, 16).unwrap_or(0), + "pf_status" => data.pf_status = u32::from_str_radix(value, 16).unwrap_or(0), + "lifetime1" => data.lifetime1 = hex_decode(value), + "lifetime2" => data.lifetime2 = hex_decode(value), + "lifetime3" => data.lifetime3 = hex_decode(value), + "lifetime4" => data.lifetime4 = hex_decode(value), + "lifetime5" => data.lifetime5 = hex_decode(value), + _ => {} // Ignore unknown keys for forward compatibility + } + } + } + Ok(data) + } +} + +fn hex_encode(data: &[u8]) -> String { + data.iter().map(|b| format!("{:02X}", b)).collect() +} + +fn hex_decode(s: &str) -> Vec { + (0..s.len()) + .step_by(2) + .filter_map(|i| u8::from_str_radix(&s[i..i + 2], 16).ok()) + .collect() +} + #[repr(u16)] enum SmartBatReg { Mode = 0x03, @@ -505,52 +645,74 @@ impl SmartBattery { /// Print battery information interactively (prompts for unseal key) pub fn dump_data(&self, ec: &CrosEc) -> EcResult<()> { - // Basic registers (no unseal required) - println!( - "Mode: 0x{:04X}", - self.read_i16(ec, SmartBatReg::Mode as u16)? - ); - let serial_num = self.read_i16(ec, SmartBatReg::SerialNum as u16)?; - println!("Serial Num: {:04X}", serial_num); + // Prompt for unseal key to access ManufacturerAccess data + print!("Enter unseal key in hex (e.g. 04143672), or press enter to skip: "); + io::stdout() + .flush() + .map_err(|e| EcError::DeviceError(format!("Failed to flush stdout: {}", e)))?; + let input_text = read_password() + .map_err(|e| EcError::DeviceError(format!("Failed to read key: {}", e)))?; + let input_text = input_text.trim(); - // ManufactureDate format: Year*512 + Month*32 + Day (year offset from 1980) - let mfg_date = self.read_i16(ec, SmartBatReg::ManufactureDate as u16)?; - let day = mfg_date & 0x1F; - let month = (mfg_date >> 5) & 0x0F; - let year = (mfg_date >> 9) + 1980; - println!("Manuf Date: {:04}-{:02}-{:02}", year, month, day); + let unseal_key = if input_text.is_empty() { + None + } else { + Some( + u32::from_str_radix(input_text, 16) + .map_err(|e| EcError::DeviceError(format!("Invalid key: {}", e)))?, + ) + }; - // Temperature is in 0.1K units, convert to Celsius - let temp = self.read_i16(ec, SmartBatReg::Temp as u16)?; - let temp_c = (temp as f32 / 10.0) - 273.15; - println!("Temperature: {:.1}C", temp_c); + let data = self.collect_data(ec, unseal_key)?; + display_battery_data(&data); - let voltage = self.read_i16(ec, SmartBatReg::Voltage as u16)?; - println!("Voltage: {}.{:03}V", voltage / 1000, voltage % 1000); + Ok(()) + } - let cell1_v = self.read_i16(ec, SmartBatReg::CellVoltage1 as u16)?; - println!(" Cell 1: {}.{:03}V", cell1_v / 1000, cell1_v % 1000); - let cell2_v = self.read_i16(ec, SmartBatReg::CellVoltage2 as u16)?; - println!(" Cell 2: {}.{:03}V", cell2_v / 1000, cell2_v % 1000); - let cell3_v = self.read_i16(ec, SmartBatReg::CellVoltage3 as u16)?; - println!(" Cell 3: {}.{:03}V", cell3_v / 1000, cell3_v % 1000); - let cell4_v = self.read_i16(ec, SmartBatReg::CellVoltage4 as u16)?; - println!(" Cell 4: {}.{:03}V", cell4_v / 1000, cell4_v % 1000); + /// Collect raw battery data into a struct (for dumping or analysis) + #[allow(clippy::field_reassign_with_default)] + pub fn collect_data(&self, ec: &CrosEc, unseal_key: Option) -> EcResult { + let mut data = BatteryData::default(); - println!( - "Cycle Count: {}", - self.read_i16(ec, SmartBatReg::CycleCount as u16)? - ); - println!( - "Device Name: {}", - self.read_string(ec, SmartBatReg::DeviceName as u16)? - ); - println!( - "Manuf Name: {}", - self.read_string(ec, SmartBatReg::ManufacturerName as u16)? - ); + // Basic registers (no unseal required) + data.mode = self.read_i16(ec, SmartBatReg::Mode as u16)?; + data.serial_num = self.read_i16(ec, SmartBatReg::SerialNum as u16)?; + data.manufacture_date = self.read_i16(ec, SmartBatReg::ManufactureDate as u16)?; + data.temperature = self.read_i16(ec, SmartBatReg::Temp as u16)?; + data.voltage = self.read_i16(ec, SmartBatReg::Voltage as u16)?; + data.cell_voltage1 = self.read_i16(ec, SmartBatReg::CellVoltage1 as u16)?; + data.cell_voltage2 = self.read_i16(ec, SmartBatReg::CellVoltage2 as u16)?; + data.cell_voltage3 = self.read_i16(ec, SmartBatReg::CellVoltage3 as u16)?; + data.cell_voltage4 = self.read_i16(ec, SmartBatReg::CellVoltage4 as u16)?; + data.cycle_count = self.read_i16(ec, SmartBatReg::CycleCount as u16)?; + data.device_name = self.read_string(ec, SmartBatReg::DeviceName as u16)?; + data.manufacturer_name = self.read_string(ec, SmartBatReg::ManufacturerName as u16)?; - // Prompt for unseal key to access ManufacturerAccess data + // Unsealed data + if let Some(key) = unseal_key { + self.unseal(ec, (key >> 16) as u16, key as u16)?; + + data.state_of_health = self.read_bytes(ec, ManufReg::Soh as u16, 4)?; + data.operation_status = self.read_i32(ec, ManufReg::OperationStatus as u16)?; + data.safety_alert = self.read_i32(ec, ManufReg::SafetyAlert as u16)?; + data.safety_status = self.read_i32(ec, ManufReg::SafetyStatus as u16)?; + data.pf_alert = self.read_i32(ec, ManufReg::PFAlert as u16)?; + data.pf_status = self.read_i32(ec, ManufReg::PFStatus as u16)?; + data.lifetime1 = self.read_bytes(ec, ManufReg::LifeTimeDataBlock1 as u16, 32)?; + data.lifetime2 = self.read_bytes(ec, ManufReg::LifeTimeDataBlock2 as u16, 20)?; + data.lifetime3 = self.read_block(ec, ManufReg::LifeTimeDataBlock3 as u16, 16)?; + data.lifetime4 = self.read_block(ec, ManufReg::LifeTimeDataBlock4 as u16, 32)?; + data.lifetime5 = self.read_block(ec, ManufReg::LifeTimeDataBlock5 as u16, 32)?; + + self.seal(ec)?; + } + + Ok(data) + } + + /// Dump battery data to a file + pub fn dump_to_file(&self, ec: &CrosEc, path: &Path) -> EcResult<()> { + // Prompt for unseal key print!("Enter unseal key in hex (e.g. 04143672), or press enter to skip: "); io::stdout() .flush() @@ -559,44 +721,95 @@ impl SmartBattery { .map_err(|e| EcError::DeviceError(format!("Failed to read key: {}", e)))?; let input_text = input_text.trim(); - if !input_text.is_empty() { - let key: u32 = u32::from_str_radix(input_text, 16) - .map_err(|e| EcError::DeviceError(format!("Invalid key: {}", e)))?; - self.unseal(ec, (key >> 16) as u16, key as u16)?; + let unseal_key = if input_text.is_empty() { + None + } else { + Some( + u32::from_str_radix(input_text, 16) + .map_err(|e| EcError::DeviceError(format!("Invalid key: {}", e)))?, + ) + }; - let soh = self.read_bytes(ec, ManufReg::Soh as u16, 4)?; - println!( - "StateOfHealth: {}mAh, {}.{:02}Wh", - u16::from_le_bytes([soh[0], soh[1]]), - u16::from_le_bytes([soh[2], soh[3]]) / 100, - u16::from_le_bytes([soh[2], soh[3]]) % 100, - ); + let data = self.collect_data(ec, unseal_key)?; + data.write_to_file(path) + .map_err(|e| EcError::DeviceError(format!("Failed to write file: {}", e)))?; - let operation_status = self.read_i32(ec, ManufReg::OperationStatus as u16)?; - print_operation_status(operation_status); + println!("Battery data saved to {}", path.display()); + Ok(()) + } +} - let safety_alert = self.read_i32(ec, ManufReg::SafetyAlert as u16)?; - print_status_flags( - "Safety Alert", - safety_alert, - decode_safety_status(safety_alert), - ); +/// Display decoded battery data from a BatteryData struct +pub fn display_battery_data(data: &BatteryData) { + println!("Mode: 0x{:04X}", data.mode); + println!("Serial Num: {:04X}", data.serial_num); - let safety_status = self.read_i32(ec, ManufReg::SafetyStatus as u16)?; - print_status_flags( - "Safety Status", - safety_status, - decode_safety_status(safety_status), - ); + let day = data.manufacture_date & 0x1F; + let month = (data.manufacture_date >> 5) & 0x0F; + let year = (data.manufacture_date >> 9) + 1980; + println!("Manuf Date: {:04}-{:02}-{:02}", year, month, day); - let pf_alert = self.read_i32(ec, ManufReg::PFAlert as u16)?; - print_status_flags("PF Alert", pf_alert, decode_pf_status(pf_alert)); + let temp_c = (data.temperature as f32 / 10.0) - 273.15; + println!("Temperature: {:.1}C", temp_c); - let pf_status = self.read_i32(ec, ManufReg::PFStatus as u16)?; - print_status_flags("PF Status", pf_status, decode_pf_status(pf_status)); + println!( + "Voltage: {}.{:03}V", + data.voltage / 1000, + data.voltage % 1000 + ); + println!( + " Cell 1: {}.{:03}V", + data.cell_voltage1 / 1000, + data.cell_voltage1 % 1000 + ); + println!( + " Cell 2: {}.{:03}V", + data.cell_voltage2 / 1000, + data.cell_voltage2 % 1000 + ); + println!( + " Cell 3: {}.{:03}V", + data.cell_voltage3 / 1000, + data.cell_voltage3 % 1000 + ); + println!( + " Cell 4: {}.{:03}V", + data.cell_voltage4 / 1000, + data.cell_voltage4 % 1000 + ); + println!("Cycle Count: {}", data.cycle_count); + println!("Device Name: {}", data.device_name); + println!("Manuf Name: {}", data.manufacturer_name); - // LifeTime Data Blocks - let lt1 = self.read_bytes(ec, ManufReg::LifeTimeDataBlock1 as u16, 32)?; + // Unsealed data + if !data.state_of_health.is_empty() { + let soh = &data.state_of_health; + println!( + "StateOfHealth: {}mAh, {}.{:02}Wh", + u16::from_le_bytes([soh[0], soh[1]]), + u16::from_le_bytes([soh[2], soh[3]]) / 100, + u16::from_le_bytes([soh[2], soh[3]]) % 100, + ); + print_operation_status(data.operation_status); + print_status_flags( + "Safety Alert", + data.safety_alert, + decode_safety_status(data.safety_alert), + ); + print_status_flags( + "Safety Status", + data.safety_status, + decode_safety_status(data.safety_status), + ); + print_status_flags("PF Alert", data.pf_alert, decode_pf_status(data.pf_alert)); + print_status_flags( + "PF Status", + data.pf_status, + decode_pf_status(data.pf_status), + ); + + if data.lifetime1.len() >= 32 { + let lt1 = &data.lifetime1; println!("LifeTime1:"); println!( " Cell 1 Max Voltage: {}mV", @@ -656,8 +869,10 @@ impl SmartBattery { println!(" Max Temp Int Sensor: {}C", lt1[29]); println!(" Min Temp Int Sensor: {}C", lt1[30]); println!(" Max Temp FET: {}C", lt1[31]); + } - let lt2 = self.read_bytes(ec, ManufReg::LifeTimeDataBlock2 as u16, 20)?; + if data.lifetime2.len() >= 20 { + let lt2 = &data.lifetime2; println!("LifeTime2:"); println!(" No. of Shutdowns: {}", lt2[0]); println!(" No. of Partial Resets: {}", lt2[1]); @@ -687,51 +902,52 @@ impl SmartBattery { cb4, cb4 as f64 / 3600.0 ); + } - let lt3 = self.read_block(ec, ManufReg::LifeTimeDataBlock3 as u16, 16)?; - if !lt3.is_empty() { - println!("LifeTime3:"); - if lt3.len() >= 4 { - println!( - " Total FW Runtime: {}h", - u16::from_le_bytes([lt3[0], lt3[1]]) - ); - println!( - " Time in Under Temp: {}h", - u16::from_le_bytes([lt3[2], lt3[3]]) - ); - } - if lt3.len() >= 16 { - println!( - " Time in Low Temp: {}h", - u16::from_le_bytes([lt3[4], lt3[5]]) - ); - println!( - " Time in Std Temp Low: {}h", - u16::from_le_bytes([lt3[6], lt3[7]]) - ); - println!( - " Time in Std Temp: {}h", - u16::from_le_bytes([lt3[8], lt3[9]]) - ); - println!( - " Time in Std Temp High: {}h", - u16::from_le_bytes([lt3[10], lt3[11]]) - ); - println!( - " Time in High Temp: {}h", - u16::from_le_bytes([lt3[12], lt3[13]]) - ); - println!( - " Time in Over Temp: {}h", - u16::from_le_bytes([lt3[14], lt3[15]]) - ); - } + if !data.lifetime3.is_empty() { + println!("LifeTime3:"); + if data.lifetime3.len() >= 4 { + println!( + " Total FW Runtime: {}h", + u16::from_le_bytes([data.lifetime3[0], data.lifetime3[1]]) + ); + println!( + " Time in Under Temp: {}h", + u16::from_le_bytes([data.lifetime3[2], data.lifetime3[3]]) + ); + } + if data.lifetime3.len() >= 16 { + println!( + " Time in Low Temp: {}h", + u16::from_le_bytes([data.lifetime3[4], data.lifetime3[5]]) + ); + println!( + " Time in Std Temp Low: {}h", + u16::from_le_bytes([data.lifetime3[6], data.lifetime3[7]]) + ); + println!( + " Time in Std Temp: {}h", + u16::from_le_bytes([data.lifetime3[8], data.lifetime3[9]]) + ); + println!( + " Time in Std Temp High: {}h", + u16::from_le_bytes([data.lifetime3[10], data.lifetime3[11]]) + ); + println!( + " Time in High Temp: {}h", + u16::from_le_bytes([data.lifetime3[12], data.lifetime3[13]]) + ); + println!( + " Time in Over Temp: {}h", + u16::from_le_bytes([data.lifetime3[14], data.lifetime3[15]]) + ); } + } - let lt4 = self.read_block(ec, ManufReg::LifeTimeDataBlock4 as u16, 32)?; - if lt4.len() >= 32 { - println!("LifeTime4:"); + if !data.lifetime4.is_empty() { + println!("LifeTime4:"); + if data.lifetime4.len() >= 32 { + let lt4 = &data.lifetime4; println!( " Cell Over-Voltage: {} (last @ cycle {})", u16::from_le_bytes([lt4[0], lt4[1]]), @@ -773,10 +989,12 @@ impl SmartBattery { u16::from_le_bytes([lt4[30], lt4[31]]) ); } + } - let lt5 = self.read_block(ec, ManufReg::LifeTimeDataBlock5 as u16, 32)?; - if lt5.len() >= 32 { - println!("LifeTime5:"); + if !data.lifetime5.is_empty() { + println!("LifeTime5:"); + if data.lifetime5.len() >= 32 { + let lt5 = &data.lifetime5; println!( " Short-Circuit Charge: {} (last @ cycle {})", u16::from_le_bytes([lt5[0], lt5[1]]), @@ -818,10 +1036,140 @@ impl SmartBattery { u16::from_le_bytes([lt5[30], lt5[31]]) ); } + } + } +} - self.seal(ec)?; +#[cfg(test)] +mod tests { + use super::*; + use std::path::PathBuf; + + fn test_bins_path(filename: &str) -> PathBuf { + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + path.push("test_bins"); + path.push(filename); + path + } + + #[test] + fn test_parse_battery_dump() { + let path = test_bins_path("battery_00E3.txt"); + if !path.exists() { + // Skip test if dump file doesn't exist yet + eprintln!("Skipping test: {:?} not found", path); + return; } - Ok(()) + let data = BatteryData::read_from_file(&path).expect("Failed to read dump file"); + + // Basic info + assert_eq!(data.mode, 0x6001); + assert_eq!(data.serial_num, 0x00E3); + assert!(!data.device_name.is_empty()); + assert!(!data.manufacturer_name.is_empty()); + + // Verify temperature is reasonable (0-100C range in 0.1K units) + let temp_c = (data.temperature as f32 / 10.0) - 273.15; + assert!( + temp_c > 0.0 && temp_c < 100.0, + "Temperature {} out of range", + temp_c + ); + + // Verify voltage is reasonable (10-20V for 4-cell pack) + let voltage_v = data.voltage as f32 / 1000.0; + assert!( + voltage_v > 10.0 && voltage_v < 25.0, + "Voltage {} out of range", + voltage_v + ); + + // Verify cell voltages add up approximately to total + let cell_sum = + data.cell_voltage1 + data.cell_voltage2 + data.cell_voltage3 + data.cell_voltage4; + let diff = (cell_sum as i32 - data.voltage as i32).abs(); + assert!( + diff < 100, + "Cell voltages don't add up: {} vs {}", + cell_sum, + data.voltage + ); + } + + #[test] + fn test_parse_lifetime_data() { + let path = test_bins_path("battery_00E3.txt"); + if !path.exists() { + eprintln!("Skipping test: {:?} not found", path); + return; + } + + let data = BatteryData::read_from_file(&path).expect("Failed to read dump file"); + + // Check lifetime1 has expected length + if !data.lifetime1.is_empty() { + assert_eq!(data.lifetime1.len(), 32, "LifeTime1 should be 32 bytes"); + + // Check cell max voltages are reasonable (3.0-4.5V) + for i in 0..4 { + let max_v = u16::from_le_bytes([data.lifetime1[i * 2], data.lifetime1[i * 2 + 1]]); + assert!( + max_v > 3000 && max_v < 4600, + "Cell {} max voltage {} out of range", + i + 1, + max_v + ); + } + + // Check temperatures are reasonable + let max_temp = data.lifetime1[26]; + let min_temp = data.lifetime1[27]; + assert!(max_temp < 100, "Max temp {} too high", max_temp); + assert!( + min_temp < max_temp || min_temp == 0, + "Min temp {} > max temp {}", + min_temp, + max_temp + ); + } + + // Check lifetime2 has expected length + if !data.lifetime2.is_empty() { + assert_eq!(data.lifetime2.len(), 20, "LifeTime2 should be 20 bytes"); + } + } + + #[test] + fn test_hex_encode_decode_roundtrip() { + let original = vec![0x00, 0x11, 0x22, 0x33, 0xAA, 0xBB, 0xCC, 0xFF]; + let encoded = hex_encode(&original); + let decoded = hex_decode(&encoded); + assert_eq!(original, decoded); + } + + #[test] + fn test_manufacture_date_decode() { + // Test date decoding: Year*512 + Month*32 + Day + // 2024-10-06 = (2024-1980)*512 + 10*32 + 6 = 44*512 + 320 + 6 = 22528 + 326 = 22854 + let mfg_date: u16 = 22854; + let day = mfg_date & 0x1F; + let month = (mfg_date >> 5) & 0x0F; + let year = (mfg_date >> 9) + 1980; + assert_eq!(day, 6); + assert_eq!(month, 10); + assert_eq!(year, 2024); + } + + #[test] + fn test_temperature_conversion() { + // 35.5C in 0.1K units = (35.5 + 273.15) * 10 = 3086.5 ≈ 3087 + let temp_raw: u16 = 3087; + let temp_c = (temp_raw as f32 / 10.0) - 273.15; + assert!( + (temp_c - 35.55).abs() < 0.1, + "Temperature conversion failed: {}", + temp_c + ); } } diff --git a/framework_lib/test_bins/battery_00E3.txt b/framework_lib/test_bins/battery_00E3.txt new file mode 100644 index 00000000..418de8ae --- /dev/null +++ b/framework_lib/test_bins/battery_00E3.txt @@ -0,0 +1,26 @@ +# Smart Battery Raw Data Dump +# Format: key=hex_value or key=string + +mode=6001 +serial_num=00E3 +manufacture_date=5946 +temperature=0BE4 +voltage=4590 +cell_voltage1=1163 +cell_voltage2=1164 +cell_voltage3=1164 +cell_voltage4=1164 +cycle_count=0051 +device_name=FRANDBAT01 +manufacturer_name=NVT +state_of_health=A3153921 +operation_status=10000287 +safety_alert=00000000 +safety_status=00000000 +pf_alert=00000000 +pf_status=00000000 +lifetime1=7111731170117311E00A5B0A6B0AC60A61017915A7DFE5EA25E13B0F003E0C42 +lifetime2=0500050096950100CF670200AF740200B7940200 +lifetime3=27273C01 +lifetime4=0000000006004800000000000000000000000000000000000000000000000000 +lifetime5=000000000000000000000000000000005000510013005100860451007A005100 From 854991d198af81246a8ba3317f6179eb84ddd33c Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Sun, 25 Jan 2026 21:29:16 +0800 Subject: [PATCH 04/14] smart_battery: Add battery health analysis Add analyze_health() function to evaluate battery condition: - Safety and permanent failure status check - Cell voltage balance analysis (current and historical) - Temperature extreme detection - Safety event counting (over-voltage, under-voltage, over-current) - Gauging health metrics (charge termination rate, resistance updates) Prints a summary with status (HEALTHY/GOOD/NEEDS ATTENTION), any issues or warnings, and key statistics including cycle count, remaining capacity, and cell balancing times. Signed-off-by: Daniel Schaefer --- framework_lib/src/smart_battery.rs | 168 +++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) diff --git a/framework_lib/src/smart_battery.rs b/framework_lib/src/smart_battery.rs index 063bf403..31ebfff1 100644 --- a/framework_lib/src/smart_battery.rs +++ b/framework_lib/src/smart_battery.rs @@ -1038,6 +1038,174 @@ pub fn display_battery_data(data: &BatteryData) { } } } + + // Print health analysis at the end + analyze_health(data); +} + +/// Analyze battery health and print a summary +pub fn analyze_health(data: &BatteryData) { + println!("\n=== Battery Health Analysis ===\n"); + + let mut issues: Vec<&str> = Vec::new(); + let mut warnings: Vec = Vec::new(); + + // Check Safety/PF status + if data.safety_status != 0 { + issues.push("Safety status flags active"); + } + if data.pf_status != 0 { + issues.push("Permanent failure flags active"); + } + + // Check cell voltage balance (current) + let cells = [ + data.cell_voltage1, + data.cell_voltage2, + data.cell_voltage3, + data.cell_voltage4, + ]; + let max_cell = cells.iter().max().unwrap_or(&0); + let min_cell = cells.iter().min().unwrap_or(&0); + let cell_delta = max_cell - min_cell; + if cell_delta > 100 { + warnings.push(format!( + "Cell imbalance: {}mV difference (>100mV)", + cell_delta + )); + } + + // Analyze lifetime data if available + if data.lifetime1.len() >= 32 { + let lt1 = &data.lifetime1; + let max_delta = u16::from_le_bytes([lt1[16], lt1[17]]); + if max_delta > 200 { + warnings.push(format!( + "Historical cell imbalance: {}mV max delta recorded", + max_delta + )); + } + + // Check temperature extremes + let max_temp = lt1[26]; + let min_temp = lt1[27]; + if max_temp > 55 { + warnings.push(format!("High temperature recorded: {}C max", max_temp)); + } + if min_temp < 5 { + warnings.push(format!("Low temperature recorded: {}C min", min_temp)); + } + } + + // Analyze safety events + if data.lifetime4.len() >= 32 { + let lt4 = &data.lifetime4; + let cov_events = u16::from_le_bytes([lt4[0], lt4[1]]); + let cuv_events = u16::from_le_bytes([lt4[4], lt4[5]]); + let ocd1_events = u16::from_le_bytes([lt4[8], lt4[9]]); + let ocd2_events = u16::from_le_bytes([lt4[12], lt4[13]]); + let occ1_events = u16::from_le_bytes([lt4[16], lt4[17]]); + let scd_events = u16::from_le_bytes([lt4[28], lt4[29]]); + + if cov_events > 0 { + warnings.push(format!("{} cell over-voltage events", cov_events)); + } + if cuv_events > 5 { + warnings.push(format!( + "{} cell under-voltage events (deep discharge)", + cuv_events + )); + } + if ocd1_events > 0 || ocd2_events > 0 { + warnings.push(format!( + "{} over-current discharge events", + ocd1_events + ocd2_events + )); + } + if occ1_events > 0 { + warnings.push(format!("{} over-current charge events", occ1_events)); + } + if scd_events > 0 { + issues.push("Short-circuit events detected"); + } + } + + // Analyze gauging health + if data.lifetime5.len() >= 32 { + let lt5 = &data.lifetime5; + let valid_terminations = u16::from_le_bytes([lt5[16], lt5[17]]); + let ra_updates = u16::from_le_bytes([lt5[24], lt5[25]]); + let ra_fails = u16::from_le_bytes([lt5[28], lt5[29]]); + + // Check charge termination ratio + if data.cycle_count > 10 && valid_terminations < (data.cycle_count * 8 / 10) { + warnings.push(format!( + "Low charge termination rate: {} terminations over {} cycles", + valid_terminations, data.cycle_count + )); + } + + // Check resistance update failures + if ra_updates > 0 { + let fail_rate = (ra_fails as f32 / ra_updates as f32) * 100.0; + if fail_rate > 20.0 { + warnings.push(format!( + "High resistance update fail rate: {:.1}%", + fail_rate + )); + } + } + } + + // Print results + if issues.is_empty() && warnings.is_empty() { + println!("Status: HEALTHY"); + println!(" No issues detected. Battery is operating normally."); + } else if issues.is_empty() { + println!("Status: GOOD (with notes)"); + for warning in &warnings { + println!(" Note: {}", warning); + } + } else { + println!("Status: NEEDS ATTENTION"); + for issue in &issues { + println!(" Issue: {}", issue); + } + for warning in &warnings { + println!(" Note: {}", warning); + } + } + + // Print summary stats + println!("\nSummary:"); + println!(" Cycle count: {}", data.cycle_count); + if !data.state_of_health.is_empty() { + let soh_mah = u16::from_le_bytes([data.state_of_health[0], data.state_of_health[1]]); + let soh_mwh = u16::from_le_bytes([data.state_of_health[2], data.state_of_health[3]]); + println!( + " Remaining capacity: {}mAh ({}.{:02}Wh)", + soh_mah, + soh_mwh / 100, + soh_mwh % 100 + ); + } + println!(" Current cell balance: {}mV spread", cell_delta); + if data.lifetime2.len() >= 20 { + let lt2 = &data.lifetime2; + let cb_times: Vec = [ + u32::from_le_bytes([lt2[4], lt2[5], lt2[6], lt2[7]]), + u32::from_le_bytes([lt2[8], lt2[9], lt2[10], lt2[11]]), + u32::from_le_bytes([lt2[12], lt2[13], lt2[14], lt2[15]]), + u32::from_le_bytes([lt2[16], lt2[17], lt2[18], lt2[19]]), + ] + .iter() + .map(|&t| t as f64 / 3600.0) + .collect(); + println!( + " Cell balancing time: {:.1}h / {:.1}h / {:.1}h / {:.1}h", + cb_times[0], cb_times[1], cb_times[2], cb_times[3] + ); + } } #[cfg(test)] From 866755ea912ddf81327205cc359c7ae60bddecff Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Sun, 25 Jan 2026 21:38:11 +0800 Subject: [PATCH 05/14] smart_battery: Allow loading battery dump from file The --smartbattery flag now accepts an optional file path to load previously dumped battery data instead of reading from the actual battery. This enables offline analysis and debugging. Signed-off-by: Daniel Schaefer --- framework_lib/src/commandline/clap_std.rs | 11 +++++++---- framework_lib/src/commandline/mod.rs | 23 +++++++++++++++++++---- framework_lib/src/commandline/uefi.rs | 2 +- 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/framework_lib/src/commandline/clap_std.rs b/framework_lib/src/commandline/clap_std.rs index 2b1f932e..2a02a5a7 100644 --- a/framework_lib/src/commandline/clap_std.rs +++ b/framework_lib/src/commandline/clap_std.rs @@ -2,6 +2,7 @@ //! This way we can use it in the regular OS commandline tool on Linux and Windows, //! as well as on the UEFI shell tool. use std::io; +use std::path::PathBuf; use clap::builder::TypedValueParser; use clap::error::ErrorKind; @@ -53,9 +54,9 @@ struct ClapCli { #[arg(long)] power: bool, - /// Show detailed smart battery information - #[arg(long)] - smartbattery: bool, + /// Show detailed smart battery information, or load from dump file + #[arg(long, value_name = "FILE")] + smartbattery: Option>, /// Print thermal information (Temperatures and Fan speed) #[arg(long)] @@ -499,7 +500,9 @@ pub fn parse(args: &[String]) -> Cli { device: args.device, compare_version: args.compare_version, power: args.power, - smartbattery: args.smartbattery, + smartbattery: args + .smartbattery + .map(|opt| opt.map(|x| x.into_os_string().into_string().unwrap())), thermal: args.thermal, sensors: args.sensors, fansetduty, diff --git a/framework_lib/src/commandline/mod.rs b/framework_lib/src/commandline/mod.rs index 9f9d3b44..fc3ce930 100644 --- a/framework_lib/src/commandline/mod.rs +++ b/framework_lib/src/commandline/mod.rs @@ -188,7 +188,7 @@ pub struct Cli { pub device: Option, pub compare_version: Option, pub power: bool, - pub smartbattery: bool, + pub smartbattery: Option>, pub thermal: bool, pub sensors: bool, pub fansetduty: Option<(Option, u32)>, @@ -1596,11 +1596,26 @@ pub fn run_with_args(args: &Cli, _allupdate: bool) -> i32 { print_board_ids(&ec); } else if args.power { return power::get_and_print_power_info(&ec); - } else if args.smartbattery { + } else if let Some(smartbattery_arg) = &args.smartbattery { #[cfg(not(feature = "uefi"))] { - let bat = SmartBattery::new(); - print_err(bat.dump_data(&ec)); + use crate::smart_battery::{display_battery_data, BatteryData}; + use std::path::Path; + + match smartbattery_arg { + Some(file_path) => { + // Load from file + match BatteryData::read_from_file(Path::new(file_path)) { + Ok(data) => display_battery_data(&data), + Err(e) => eprintln!("Failed to read battery dump: {}", e), + } + } + None => { + // Read from actual battery + let bat = SmartBattery::new(); + print_err(bat.dump_data(&ec)); + } + } } } else if args.thermal { power::print_thermal(&ec); diff --git a/framework_lib/src/commandline/uefi.rs b/framework_lib/src/commandline/uefi.rs index d0f7559b..65ab5303 100644 --- a/framework_lib/src/commandline/uefi.rs +++ b/framework_lib/src/commandline/uefi.rs @@ -40,7 +40,7 @@ pub fn parse(args: &[String]) -> Cli { device: None, compare_version: None, power: false, - smartbattery: false, + smartbattery: None, thermal: false, sensors: false, fansetduty: None, From 4217bc50741786a104f4c7ba00fc19a83f58065a Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Sun, 25 Jan 2026 21:32:10 +0800 Subject: [PATCH 06/14] smart_battery: Add SHA-1 HMAC battery authentication Add battery authentication using TI's nested SHA-1 HMAC method: - calculate_ti_hmac(): Implements SHA1(Key || SHA1(Key || Challenge)) - authenticate_battery(): Challenge-response authentication flow - interactive_authenticate(): CLI interface with unseal/auth key prompts The authentication verifies battery authenticity by: 1. Sending a random 20-byte challenge to the battery 2. Waiting 250ms for the gauge to compute the response 3. Computing expected response locally using the auth key 4. Comparing responses to verify genuineness Add --smartbattery-auth CLI flag for interactive authentication. Requires sha1 and rand crate dependencies. Signed-off-by: Daniel Schaefer --- Cargo.lock | 792 ++++++++-------------- framework_lib/Cargo.toml | 6 +- framework_lib/src/commandline/clap_std.rs | 5 + framework_lib/src/commandline/mod.rs | 7 + framework_lib/src/commandline/uefi.rs | 1 + framework_lib/src/smart_battery.rs | 162 +++++ 6 files changed, 456 insertions(+), 517 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 39bdbf9a..bda59813 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -22,9 +22,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -37,15 +37,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -72,9 +72,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "bit_field" @@ -84,9 +84,15 @@ checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" [[package]] name = "bitflags" -version = "2.10.0" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" [[package]] name = "block-buffer" @@ -99,9 +105,9 @@ dependencies = [ [[package]] name = "built" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" +checksum = "5c0e531d93d39c34eef561e929e8a7f86d77a5af08aac4f6d6e39976c51858e9" dependencies = [ "chrono", "git2", @@ -109,15 +115,15 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.1" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "cc" -version = "1.2.53" +version = "1.2.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932" +checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" dependencies = [ "find-msvc-tools", "jobserver", @@ -139,9 +145,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.43" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "iana-time-zone", "num-traits", @@ -151,9 +157,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.54" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -180,9 +186,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.54" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -192,18 +198,18 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.65" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "430b4dc2b5e3861848de79627b2bedc9f3342c7da5173a14eaa5d0f8dc18ae5d" +checksum = "97bf4965940c2382204c0ded6dd3dd48c0c4e872f1e76fb1bf94f45991a2cb6a" dependencies = [ "clap", ] [[package]] name = "clap_derive" -version = "4.5.49" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck", "proc-macro2", @@ -213,15 +219,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.7" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "core-foundation-sys" @@ -284,41 +290,62 @@ dependencies = [ ] [[package]] -name = "digest" -version = "0.10.7" +name = "defmt" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +checksum = "a6e524506490a1953d237cb87b1cfc1e46f88c18f10a22dfe0f507dc6bfc7f7f" dependencies = [ - "block-buffer", - "crypto-common", + "bitflags 1.3.2", + "defmt-macros", ] [[package]] -name = "displaydoc" -version = "0.2.5" +name = "defmt-macros" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "f0a27770e9c8f719a79d8b638281f4d828f77d8fd61e0bd94451b9b85e576a0b" dependencies = [ + "defmt-parser", + "proc-macro-error2", "proc-macro2", "quote", "syn", ] [[package]] -name = "dmidecode" +name = "defmt-parser" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ecc81f210f04aaadfeb5aa067b7d9095ea2af9c0e140ef56b8a3f3381ec4db" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.18", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dmidecode" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a898086945b8e1c1e12f0996773ae65442d84f8dcafd5ab88a4c80b1da3d3008" dependencies = [ - "bitflags", + "bitflags 2.13.0", "uuid", ] [[package]] name = "embed-resource" -version = "3.0.6" +version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55a075fc573c64510038d7ee9abc7990635863992f83ebc52c8b433b8411a02e" +checksum = "c31a88c8d26de40ed18fe748c547845aa39de1db3afd958f8cb91579f3644bcb" dependencies = [ "cc", "memchr", @@ -330,9 +357,9 @@ dependencies = [ [[package]] name = "env_filter" -version = "0.1.4" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" +checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217" dependencies = [ "log", "regex", @@ -340,9 +367,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.8" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6" dependencies = [ "anstream", "anstyle", @@ -359,9 +386,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "find-msvc-tools" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "fnv" @@ -369,15 +396,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - [[package]] name = "framework_lib" version = "0.6.4" @@ -401,8 +419,10 @@ dependencies = [ "num-traits", "nvml-wrapper", "plain", + "rand", "regex", "rusb", + "sha1", "sha2", "spin 0.10.0", "uefi", @@ -436,9 +456,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -451,9 +471,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -461,15 +481,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -478,15 +498,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", @@ -495,21 +515,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -519,7 +539,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -547,15 +566,14 @@ dependencies = [ [[package]] name = "git2" -version = "0.20.3" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e2b37e2f62729cdada11f0e6b3b6fe383c69c29fc619e391223e12856af308c" +checksum = "ddddbf932745a6be37109b6112d3ee09696106f848449069d3a57bba937ab82e" dependencies = [ - "bitflags", + "bitflags 2.13.0", "libc", "libgit2-sys", "log", - "url", ] [[package]] @@ -569,9 +587,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.1" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "heck" @@ -581,22 +599,22 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hidapi" -version = "2.6.4" +version = "2.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "565dd4c730b8f8b2c0fb36df6be12e5470ae10895ddcc4e9dcfbfb495de202b0" +checksum = "c78dadfc12f865bc3fcac3897e64533b930737ceb9ef245c8277de98d0b010e9" dependencies = [ "cc", "cfg-if", "libc", "pkg-config", - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -616,119 +634,17 @@ dependencies = [ "cc", ] -[[package]] -name = "icu_collections" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" -dependencies = [ - "displaydoc", - "potential_utf", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" - -[[package]] -name = "icu_properties" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" - -[[package]] -name = "icu_provider" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - [[package]] name = "ident_case" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", "hashbrown", @@ -742,10 +658,11 @@ checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "jiff" -version = "0.2.18" +version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67e8da4c49d6d9909fe03361f9b620f58898859f5c7aded68351e85e71ecf50" +checksum = "ccfe6121cbe750cf81efa362d85c0bde7ea298ec43092d3a193baca59cdbd634" dependencies = [ + "defmt", "jiff-static", "log", "portable-atomic", @@ -755,9 +672,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.18" +version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c84ee7f197eca9a86c6fd6cb771e55eb991632f15f2bc3ca6ec838929e6e78" +checksum = "e165e897f662d428f3cd3828a919dbe067c2d42bb1031eede74ef9d27ecdedd2" dependencies = [ "proc-macro2", "quote", @@ -776,11 +693,12 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.85" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ - "once_cell", + "cfg-if", + "futures-util", "wasm-bindgen", ] @@ -795,15 +713,15 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.180" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libgit2-sys" -version = "0.18.3+1.9.2" +version = "0.18.5+1.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9b3acc4b91781bb0b3386669d325163746af5f6e4f73e6d2d630e09a35f3487" +checksum = "005d6ae6eac1912906073e069f7db60b1fa98e052a68227824afe3e3a1c59ca2" dependencies = [ "cc", "libc", @@ -835,9 +753,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.23" +version = "1.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7" +checksum = "85bc9657773828b90eeb625adff10eeac83cc21bbfd8e23a03eaa8a33c9e28d9" dependencies = [ "cc", "libc", @@ -845,12 +763,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "litemap" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" - [[package]] name = "lock_api" version = "0.4.14" @@ -862,15 +774,15 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" [[package]] name = "nix" @@ -878,7 +790,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags", + "bitflags 2.13.0", "cfg-if", "cfg_aliases", "libc", @@ -968,7 +880,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d5c6c0ef9702176a570f06ad94f3198bc29c524c8b498f1b9346e1b1bdcbb3a" dependencies = [ - "bitflags", + "bitflags 2.13.0", "libloading", "nvml-wrapper-sys", "static_assertions", @@ -978,18 +890,18 @@ dependencies = [ [[package]] name = "nvml-wrapper-sys" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd23dbe2eb8d8335d2bce0299e0a07d6a63c089243d626ca75b770a962ff49e6" +checksum = "6b4d594420fcda43b1c2c4bd44d48974aa3c7a9ab2cbf10dc18e35265767bf0b" dependencies = [ "libloading", ] [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -997,29 +909,17 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - [[package]] name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "plain" @@ -1029,33 +929,55 @@ checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" [[package]] name = "portable-atomic" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.4" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" dependencies = [ "portable-atomic", ] [[package]] -name = "potential_utf" -version = "0.1.4" +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" dependencies = [ - "zerovec", + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn", ] [[package]] name = "proc-macro2" -version = "1.0.105" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -1082,9 +1004,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.43" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" dependencies = [ "proc-macro2", ] @@ -1095,11 +1017,40 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom", +] + [[package]] name = "regex" -version = "1.12.2" +version = "1.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" dependencies = [ "aho-corasick", "memchr", @@ -1109,9 +1060,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -1120,9 +1071,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "rusb" @@ -1157,9 +1108,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" @@ -1193,13 +1144,24 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.4" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ "serde_core", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha2" version = "0.10.9" @@ -1213,21 +1175,15 @@ dependencies = [ [[package]] name = "shlex" -version = "1.3.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "spin" @@ -1244,12 +1200,6 @@ dependencies = [ "lock_api", ] -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - [[package]] name = "static_assertions" version = "1.1.0" @@ -1270,26 +1220,15 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.114" +version = "2.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "thiserror" version = "1.0.69" @@ -1301,11 +1240,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.17", + "thiserror-impl 2.0.18", ] [[package]] @@ -1321,30 +1260,20 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", "syn", ] -[[package]] -name = "tinystr" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" -dependencies = [ - "displaydoc", - "zerovec", -] - [[package]] name = "toml" -version = "0.9.11+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" dependencies = [ "indexmap", "serde_core", @@ -1357,33 +1286,33 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.5+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ "serde_core", ] [[package]] name = "toml_parser" -version = "1.0.6+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ "winnow", ] [[package]] name = "toml_writer" -version = "1.0.6+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "ucs2" @@ -1400,7 +1329,7 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71fe9058b73ee2b6559524af9e33199c13b2485ddbf3ad1181b68051cdc50c17" dependencies = [ - "bitflags", + "bitflags 2.13.0", "cfg-if", "log", "ptr_meta", @@ -1427,7 +1356,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f64fe59e11af447d12fd60a403c74106eb104309f34b4c6dbce6e927d97da9d" dependencies = [ - "bitflags", + "bitflags 2.13.0", "uguid", ] @@ -1439,27 +1368,9 @@ checksum = "0c8352f8c05e47892e7eaf13b34abd76a7f4aeaf817b716e88789381927f199c" [[package]] name = "unicode-ident" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" - -[[package]] -name = "url" -version = "2.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "utf8_iter" -version = "1.0.4" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "utf8parse" @@ -1469,9 +1380,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.22.0" +version = "1.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" [[package]] name = "vcpkg" @@ -1507,18 +1418,18 @@ dependencies = [ [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.108" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if", "once_cell", @@ -1529,9 +1440,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.108" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1539,9 +1450,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.108" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ "bumpalo", "proc-macro2", @@ -1552,9 +1463,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.108" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] @@ -1682,22 +1593,13 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - [[package]] name = "windows-sys" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -1709,35 +1611,20 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", "windows_i686_gnullvm", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] @@ -1758,36 +1645,18 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -1800,48 +1669,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -1850,9 +1695,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.7.14" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" [[package]] name = "winreg" @@ -1866,9 +1711,9 @@ dependencies = [ [[package]] name = "winresource" -version = "0.1.29" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17cdfa8da4b111045a5e47c7c839e6c5e11c942de1309bc624393ed5d87f89c6" +checksum = "0986a8b1d586b7d3e4fe3d9ea39fb451ae22869dcea4aa109d287a374d866087" dependencies = [ "toml", "version_check", @@ -1876,9 +1721,9 @@ dependencies = [ [[package]] name = "wit-bindgen" -version = "0.51.0" +version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] name = "wmi" @@ -1890,7 +1735,7 @@ dependencies = [ "futures", "log", "serde", - "thiserror 2.0.17", + "thiserror 2.0.18", "windows", "windows-core", ] @@ -1907,103 +1752,20 @@ dependencies = [ "syn", ] -[[package]] -name = "writeable" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" - -[[package]] -name = "yoke" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - [[package]] name = "zerocopy" -version = "0.8.47" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zerotrie" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.2" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" dependencies = [ "proc-macro2", "quote", diff --git a/framework_lib/Cargo.toml b/framework_lib/Cargo.toml index 32a00a83..c3e10209 100644 --- a/framework_lib/Cargo.toml +++ b/framework_lib/Cargo.toml @@ -26,6 +26,7 @@ built = { version = "0.8", features = ["chrono", "git2"] } [dependencies] lazy_static = "1.4.0" dmidecode = { version = "1", default-features = false } +sha1 = { version = "0.10.6", default-features = false } sha2 = { version = "0.10.8", default-features = false, features = [ "force-soft" ] } regex = { version = "1.11.1", default-features = false } num = { version = "0.4", default-features = false } @@ -45,6 +46,7 @@ uefi-raw = "0.13" plain = "0.2.3" [target.'cfg(windows)'.dependencies] +rand = { version = "0.9", default-features = false, features = ["std", "std_rng", "thread_rng"] } wmi = "~0.17.3" env_logger = "0.11" clap = { version = "4.5", features = ["derive", "cargo"] } @@ -56,10 +58,9 @@ winreg = "0.55.0" nvml-wrapper = { version = "0.11.0", optional = true } [target.'cfg(unix)'.dependencies] +rand = { version = "0.9", default-features = false, features = ["std", "std_rng", "thread_rng"] } libc = "0.2.155" nix = { version = "0.30", features = ["ioctl", "user", "term"] } -redox_hwio = { git = "https://github.com/FrameworkComputer/rust-hwio", branch = "freebsd" } -smbios-lib = { git = "https://github.com/FrameworkComputer/smbios-lib.git", branch = "no-std" } env_logger = "0.11" clap = { version = "4.5", features = ["derive", "cargo"] } clap-num = { version = "1.2.0" } @@ -83,6 +84,7 @@ features = [ "Win32_Devices_Properties", "Win32_Storage_EnhancedStorage", "Win32_System_Threading", + "Win32_System_Console", "Win32_UI_Shell_PropertiesSystem" ] diff --git a/framework_lib/src/commandline/clap_std.rs b/framework_lib/src/commandline/clap_std.rs index 2a02a5a7..76ac18f6 100644 --- a/framework_lib/src/commandline/clap_std.rs +++ b/framework_lib/src/commandline/clap_std.rs @@ -58,6 +58,10 @@ struct ClapCli { #[arg(long, value_name = "FILE")] smartbattery: Option>, + /// Authenticate smart battery (requires unseal and auth keys) + #[arg(long)] + smartbattery_auth: bool, + /// Print thermal information (Temperatures and Fan speed) #[arg(long)] thermal: bool, @@ -503,6 +507,7 @@ pub fn parse(args: &[String]) -> Cli { smartbattery: args .smartbattery .map(|opt| opt.map(|x| x.into_os_string().into_string().unwrap())), + smartbattery_auth: args.smartbattery_auth, thermal: args.thermal, sensors: args.sensors, fansetduty, diff --git a/framework_lib/src/commandline/mod.rs b/framework_lib/src/commandline/mod.rs index fc3ce930..a56d917b 100644 --- a/framework_lib/src/commandline/mod.rs +++ b/framework_lib/src/commandline/mod.rs @@ -189,6 +189,7 @@ pub struct Cli { pub compare_version: Option, pub power: bool, pub smartbattery: Option>, + pub smartbattery_auth: bool, pub thermal: bool, pub sensors: bool, pub fansetduty: Option<(Option, u32)>, @@ -1617,6 +1618,12 @@ pub fn run_with_args(args: &Cli, _allupdate: bool) -> i32 { } } } + } else if args.smartbattery_auth { + #[cfg(not(feature = "uefi"))] + { + let bat = SmartBattery::new(); + print_err(bat.interactive_authenticate(&ec)); + } } else if args.thermal { power::print_thermal(&ec); } else if args.sensors { diff --git a/framework_lib/src/commandline/uefi.rs b/framework_lib/src/commandline/uefi.rs index 65ab5303..b3bd2274 100644 --- a/framework_lib/src/commandline/uefi.rs +++ b/framework_lib/src/commandline/uefi.rs @@ -41,6 +41,7 @@ pub fn parse(args: &[String]) -> Cli { compare_version: None, power: false, smartbattery: None, + smartbattery_auth: false, thermal: false, sensors: false, fansetduty: None, diff --git a/framework_lib/src/smart_battery.rs b/framework_lib/src/smart_battery.rs index 31ebfff1..66c191e0 100644 --- a/framework_lib/src/smart_battery.rs +++ b/framework_lib/src/smart_battery.rs @@ -5,9 +5,13 @@ use alloc::string::String; use alloc::vec::Vec; +use rand::random; +use sha1::{Digest, Sha1}; use std::fs::File; use std::io::{self, BufRead, BufReader, Write}; use std::path::Path; +use std::thread; +use std::time::Duration; use crate::chromium_ec::i2c_passthrough::*; use crate::chromium_ec::{CrosEc, EcError, EcResult}; @@ -160,6 +164,7 @@ enum SmartBatReg { SerialNum = 0x1C, ManufacturerName = 0x20, DeviceName = 0x21, + Authenticate = 0x2F, CellVoltage1 = 0x3C, CellVoltage2 = 0x3D, CellVoltage3 = 0x3E, @@ -456,6 +461,33 @@ fn print_operation_status(value: u32) { } } +/// Calculates the HMAC using TI's specific nested SHA-1 method. +/// Formula: SHA1( Key || SHA1( Key || Challenge ) ) +/// Note: TI batteries expect bytes in reversed order +fn calculate_ti_hmac(key: &[u8; 16], challenge: &[u8; 20]) -> [u8; 20] { + // Reverse challenge bytes as per TI spec + let mut challenge_rev = *challenge; + challenge_rev.reverse(); + + // 1. Inner Hash: SHA1( Key + Challenge_reversed ) + let mut inner_hasher = Sha1::new(); + inner_hasher.update(key); + inner_hasher.update(challenge_rev); + let inner_digest = inner_hasher.finalize(); + + // 2. Outer Hash: SHA1( Key + Inner_Digest ) + let mut outer_hasher = Sha1::new(); + outer_hasher.update(key); + outer_hasher.update(inner_digest); + let outer_digest = outer_hasher.finalize(); + + // Convert GenericArray to standard [u8; 20] and reverse for output + let mut result = [0u8; 20]; + result.copy_from_slice(&outer_digest); + result.reverse(); + result +} + /// Reads a line from stdin without echoing (for sensitive input like keys) #[cfg(unix)] fn read_password() -> io::Result { @@ -643,6 +675,136 @@ impl SmartBattery { Ok(i2c_response.data[1..=actual_len].to_vec()) } + /// Raw I2C read without SMBus block length prefix handling + fn read_raw(&self, ec: &CrosEc, addr: u16, len: u16) -> EcResult> { + let i2c_response = i2c_read(ec, self.i2c_port, self.i2c_addr >> 1, addr, len)?; + i2c_response.is_successful()?; + Ok(i2c_response.data.to_vec()) + } + + fn smbus_write_block(&self, ec: &CrosEc, reg: u8, data: &[u8]) -> EcResult<()> { + i2c_write_block(ec, self.i2c_port, self.i2c_addr >> 1, reg, data)?; + Ok(()) + } + + /// Authenticate the battery using SHA-1 HMAC challenge-response + pub fn authenticate_battery(&self, ec: &CrosEc, auth_key: &[u8; 16]) -> EcResult { + // 1. Generate a random 20-byte challenge + let challenge: [u8; 20] = random(); + + println!("Step 1: Sending Challenge..."); + + // SMBus Block Write format: [Byte_Count, Data...] + // The register address is handled by smbus_write_block + let mut write_buf = Vec::new(); + write_buf.push(20); // Byte Count (0x14) + write_buf.extend_from_slice(&challenge); + + self.smbus_write_block(ec, SmartBatReg::Authenticate as u8, &write_buf)?; + + // 2. Wait for the gauge to calculate (Datasheet says 250ms) + println!("Step 2: Waiting 250ms..."); + thread::sleep(Duration::from_millis(250)); + + // 3. Calculate expected result locally while waiting + let expected_response = calculate_ti_hmac(auth_key, &challenge); + + // 4. Read Response from Authenticate register + // SMBus Block Read format: [Length] + [Data...] + println!("Step 3: Reading Response..."); + + // Read 21 bytes (1 byte length + 20 bytes signature) from Authenticate register + let raw_response = self.read_raw(ec, SmartBatReg::Authenticate as u16, 21)?; + + // 5. Parse and Compare + // First byte is the length, should be 20 + let response_len = raw_response[0] as usize; + if response_len != 20 { + return Err(EcError::DeviceError(format!( + "Expected 20-byte response, got {} bytes (first byte: 0x{:02X})", + response_len, raw_response[0] + ))); + } + + let device_response = &raw_response[1..21]; + + println!("Expected: {:02X?}", expected_response); + println!("Received: {:02X?}", device_response); + + if device_response == expected_response { + println!("SUCCESS: Battery is genuine."); + Ok(true) + } else { + println!("FAILURE: Signature mismatch."); + Ok(false) + } + } + + /// Interactive authentication - prompts for unseal key and authentication key + pub fn interactive_authenticate(&self, ec: &CrosEc) -> EcResult<()> { + // First, try to unseal the battery + println!("Some batteries require unsealing before authentication."); + print!("Unseal key in hex (e.g. 04143672), or enter to skip: "); + io::stdout() + .flush() + .map_err(|e| EcError::DeviceError(format!("Failed to flush stdout: {}", e)))?; + + let unseal_input = read_password() + .map_err(|e| EcError::DeviceError(format!("Failed to read key: {}", e)))?; + let unseal_input = unseal_input.trim(); + if !unseal_input.is_empty() { + let key: u32 = u32::from_str_radix(unseal_input, 16) + .map_err(|e| EcError::DeviceError(format!("Invalid unseal key: {}", e)))?; + println!("Unsealing battery..."); + self.unseal(ec, (key >> 16) as u16, key as u16)?; + // Wait a bit after unsealing + thread::sleep(Duration::from_millis(100)); + } + + // Now prompt for authentication key + print!("Auth key in hex (32 chars, e.g. 00112233...EEFF): "); + io::stdout() + .flush() + .map_err(|e| EcError::DeviceError(format!("Failed to flush stdout: {}", e)))?; + + let input_text = read_password() + .map_err(|e| EcError::DeviceError(format!("Failed to read key: {}", e)))?; + let input_text = input_text.trim(); + if input_text.is_empty() { + println!("No key provided, aborting."); + return Ok(()); + } + + if input_text.len() != 32 { + return Err(EcError::DeviceError(format!( + "Key must be 32 hex characters (16 bytes), got {} characters", + input_text.len() + ))); + } + + let mut auth_key = [0u8; 16]; + for i in 0..16 { + auth_key[i] = u8::from_str_radix(&input_text[i * 2..i * 2 + 2], 16).map_err(|e| { + EcError::DeviceError(format!("Invalid hex at position {}: {}", i * 2, e)) + })?; + } + + let result = self.authenticate_battery(ec, &auth_key)?; + + // Re-seal the battery if we unsealed it + if !unseal_input.is_empty() { + println!("Re-sealing battery..."); + self.seal(ec)?; + } + + match result { + true => println!("Authentication successful - battery is genuine."), + false => println!("Authentication failed - battery signature mismatch."), + } + + Ok(()) + } + /// Print battery information interactively (prompts for unseal key) pub fn dump_data(&self, ec: &CrosEc) -> EcResult<()> { // Prompt for unseal key to access ManufacturerAccess data From 35de7224affc7ad11e8a22c1ab6e44adb3aaed6e Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Tue, 17 Mar 2026 15:13:35 +0800 Subject: [PATCH 07/14] unseal: Flip word order Signed-off-by: Daniel Schaefer --- framework_lib/src/smart_battery.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/framework_lib/src/smart_battery.rs b/framework_lib/src/smart_battery.rs index 66c191e0..c8504ad8 100644 --- a/framework_lib/src/smart_battery.rs +++ b/framework_lib/src/smart_battery.rs @@ -597,14 +597,14 @@ impl SmartBattery { self.i2c_port, self.i2c_addr >> 1, 0x00, - &key1.to_le_bytes(), + &key2.to_le_bytes(), )?; i2c_write_block( ec, self.i2c_port, self.i2c_addr >> 1, 0x00, - &key2.to_le_bytes(), + &key1.to_le_bytes(), )?; Ok(()) } From 93291e016e7da8acb7a760c4884f047c4b5fe3bd Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Wed, 18 Mar 2026 02:19:34 +0800 Subject: [PATCH 08/14] smartbattery: Decode R3 firmware Signed-off-by: Daniel Schaefer --- framework_lib/src/smart_battery.rs | 216 +++++++++++++++++++++-------- 1 file changed, 160 insertions(+), 56 deletions(-) diff --git a/framework_lib/src/smart_battery.rs b/framework_lib/src/smart_battery.rs index c8504ad8..4a2578cf 100644 --- a/framework_lib/src/smart_battery.rs +++ b/framework_lib/src/smart_battery.rs @@ -43,6 +43,7 @@ pub struct BatteryData { pub lifetime3: Vec, pub lifetime4: Vec, pub lifetime5: Vec, + pub firmware_version: Vec, // MAC 0x0002 response (up to 14 bytes) } impl BatteryData { @@ -79,6 +80,11 @@ impl BatteryData { writeln!(file, "lifetime3={}", hex_encode(&self.lifetime3))?; writeln!(file, "lifetime4={}", hex_encode(&self.lifetime4))?; writeln!(file, "lifetime5={}", hex_encode(&self.lifetime5))?; + writeln!( + file, + "firmware_version={}", + hex_encode(&self.firmware_version) + )?; Ok(()) } @@ -135,6 +141,7 @@ impl BatteryData { "lifetime3" => data.lifetime3 = hex_decode(value), "lifetime4" => data.lifetime4 = hex_decode(value), "lifetime5" => data.lifetime5 = hex_decode(value), + "firmware_version" => data.firmware_version = hex_decode(value), _ => {} // Ignore unknown keys for forward compatibility } } @@ -165,10 +172,10 @@ enum SmartBatReg { ManufacturerName = 0x20, DeviceName = 0x21, Authenticate = 0x2F, - CellVoltage1 = 0x3C, - CellVoltage2 = 0x3D, - CellVoltage3 = 0x3E, - CellVoltage4 = 0x3F, + CellVoltage4 = 0x3C, + CellVoltage3 = 0x3D, + CellVoltage2 = 0x3E, + CellVoltage1 = 0x3F, } /// ManufacturerAccess block registers @@ -190,11 +197,11 @@ enum ManufReg { } /// Decode OperationStatus register bits (BQ40z50) -/// Based on TI bq40z50 device definitions -fn decode_operation_status(value: u32) -> Vec<&'static str> { +/// Based on TI bq40z50-R2 (SLUUA43A) and bq40z50-R3 (SLUUBU5A) datasheets +fn decode_operation_status(value: u32, is_r3: bool) -> Vec<&'static str> { let mut flags = Vec::new(); if value & (1 << 0) != 0 { - flags.push("PRES (Cell Voltages OK)"); + flags.push("PRES (System Present Low)"); } if value & (1 << 1) != 0 { flags.push("DSG (Discharge FET On)"); @@ -205,9 +212,15 @@ fn decode_operation_status(value: u32) -> Vec<&'static str> { if value & (1 << 3) != 0 { flags.push("PCHG (Precharge FET On)"); } + if value & (1 << 4) != 0 && is_r3 { + flags.push("ACTHR (Accumulated Charge Threshold)"); + } if value & (1 << 5) != 0 { flags.push("FUSE (Fuse Active)"); } + if value & (1 << 6) != 0 && is_r3 { + flags.push("EMSHUT (Emergency FET Shutdown)"); + } if value & (1 << 7) != 0 { flags.push("BTP_INT (Battery Trip Point)"); } @@ -277,13 +290,23 @@ fn decode_operation_status(value: u32) -> Vec<&'static str> { flags.push("CB (Cell Balancing)"); } if value & (1 << 29) != 0 { - flags.push("EMSHUT (Emergency Shutdown)"); + if is_r3 { + flags.push("DISCONN (System Disconnect)"); + } else { + flags.push("EMSHUT (Emergency Shutdown)"); + } + } + if value & (1 << 30) != 0 && is_r3 { + flags.push("PSSHUT (Power Saving Shutdown)"); + } + if value & (1 << 31) != 0 && is_r3 { + flags.push("IOSHUT (IO-Based Shutdown)"); } flags } /// Decode SafetyAlert/SafetyStatus register bits (BQ40z50) -/// Based on TI BQ40z50 Technical Reference Manual +/// Based on TI bq40z50-R2 (SLUUA43A) and bq40z50-R3 (SLUUBU5A) datasheets fn decode_safety_status(value: u32) -> Vec<&'static str> { let mut flags = Vec::new(); if value & (1 << 0) != 0 { @@ -304,14 +327,23 @@ fn decode_safety_status(value: u32) -> Vec<&'static str> { if value & (1 << 5) != 0 { flags.push("OCD2 (Over-Current Discharge Tier2)"); } + if value & (1 << 6) != 0 { + flags.push("AOLD (Overload in Discharge)"); + } if value & (1 << 7) != 0 { - flags.push("OCDL (Over-Current Discharge Latch)"); + flags.push("AOLDL (Overload in Discharge Latch)"); + } + if value & (1 << 8) != 0 { + flags.push("ASCC (Short-Circuit Charge)"); } if value & (1 << 9) != 0 { - flags.push("SCCL (Short-Circuit Charge Latch)"); + flags.push("ASCCL (Short-Circuit Charge Latch)"); + } + if value & (1 << 10) != 0 { + flags.push("ASCD (Short-Circuit Discharge)"); } if value & (1 << 11) != 0 { - flags.push("SCDL (Short-Circuit Discharge Latch)"); + flags.push("ASCDL (Short-Circuit Discharge Latch)"); } if value & (1 << 12) != 0 { flags.push("OTC (Over-Temp Charge)"); @@ -355,13 +387,20 @@ fn decode_safety_status(value: u32) -> Vec<&'static str> { if value & (1 << 27) != 0 { flags.push("UTD (Under-Temp Discharge)"); } + if value & (1 << 28) != 0 { + flags.push("COVL (Cell Over-Voltage Latch)"); + } + if value & (1 << 29) != 0 { + flags.push("OCDL (Over-Current Discharge Latch)"); + } flags } /// Decode PFAlert/PFStatus register bits (BQ40z50) -/// Based on TI BQ40z50 Technical Reference Manual -fn decode_pf_status(value: u32) -> Vec<&'static str> { +/// Based on TI bq40z50-R2 (SLUUA43A) and bq40z50-R3 (SLUUBU5A) datasheets +fn decode_pf_status(value: u32, is_r3: bool) -> Vec<&'static str> { let mut flags = Vec::new(); + // Lower 16 bits (PFAlert 0x0052 / PFStatus 0x0053) if value & (1 << 0) != 0 { flags.push("SUV (Safety Cell Under-Voltage)"); } @@ -378,64 +417,87 @@ fn decode_pf_status(value: u32) -> Vec<&'static str> { flags.push("SOT (Safety Over-Temp Cell)"); } if value & (1 << 5) != 0 { - flags.push("SOTF (Safety Over-Temp FET)"); + flags.push("COVL (Cell Over-Voltage Latch)"); } if value & (1 << 6) != 0 { - flags.push("VIMR (Voltage Imbalance at Rest)"); + flags.push("SOTF (Safety Over-Temp FET)"); } if value & (1 << 7) != 0 { - flags.push("VIMA (Voltage Imbalance Active)"); + flags.push("QIM (QMax Imbalance)"); } if value & (1 << 8) != 0 { - flags.push("QIM (QMax Imbalance)"); + flags.push("CB (Cell Balancing Failure)"); } if value & (1 << 9) != 0 { - flags.push("CB (Cell Balancing Failure)"); + flags.push("IMP (Impedance Failure)"); } if value & (1 << 10) != 0 { - flags.push("IMP (Impedance Failure)"); + flags.push("CD (Coulomb Counter Failure)"); } if value & (1 << 11) != 0 { - flags.push("CD (Coulomb Counter Failure)"); + flags.push("VIMR (Voltage Imbalance at Rest)"); } if value & (1 << 12) != 0 { - flags.push("FUSE (Chemical Fuse Failure)"); + flags.push("VIMA (Voltage Imbalance Active)"); } if value & (1 << 13) != 0 { - flags.push("AFER (AFE Register Failure)"); + flags.push("AOLDL (Overload in Discharge Latch)"); } if value & (1 << 14) != 0 { - flags.push("AFEC (AFE Communication Failure)"); + flags.push("ASCCL (Short-Circuit Charge Latch)"); } if value & (1 << 15) != 0 { - flags.push("2LVL (Second Level Safety)"); + flags.push("ASCDL (Short-Circuit Discharge Latch)"); } + // Upper 16 bits if value & (1 << 16) != 0 { - flags.push("PTC (Open PTC Failure)"); + flags.push("CFETF (Charge FET Failure)"); } if value & (1 << 17) != 0 { - flags.push("CFETF (Charge FET Failure)"); + flags.push("DFETF (Discharge FET Failure)"); } if value & (1 << 18) != 0 { - flags.push("DFETF (Discharge FET Failure)"); + flags.push("OCDL (Over-Current Discharge Latch)"); } if value & (1 << 19) != 0 { - flags.push("TS1 (Open Thermistor TS1)"); + flags.push("FUSE (Chemical Fuse Failure)"); } if value & (1 << 20) != 0 { - flags.push("TS2 (Open Thermistor TS2)"); + flags.push("AFER (AFE Register Failure)"); } if value & (1 << 21) != 0 { - flags.push("TS3 (Open Thermistor TS3)"); + flags.push("AFEC (AFE Communication Failure)"); } if value & (1 << 22) != 0 { - flags.push("TS4 (Open Thermistor TS4)"); + flags.push("2LVL (Second Level Safety)"); } if value & (1 << 23) != 0 { - flags.push("DFW (Data Flash Wearout)"); + flags.push("PTC (Open PTC Failure)"); } if value & (1 << 24) != 0 { - flags.push("HWMX (HW Max Cell Voltage)"); + flags.push("IFC (Instruction Flash Checksum)"); + } + if value & (1 << 25) != 0 { + if is_r3 { + flags.push("FORCE (Manual PF)"); + } else { + flags.push("OPNCELL (Open Cell Voltage Connection)"); + } + } + if value & (1 << 26) != 0 { + flags.push("DFW (Data Flash Wearout)"); + } + if value & (1 << 28) != 0 { + flags.push("TS1 (Open Thermistor TS1)"); + } + if value & (1 << 29) != 0 { + flags.push("TS2 (Open Thermistor TS2)"); + } + if value & (1 << 30) != 0 { + flags.push("TS3 (Open Thermistor TS3)"); + } + if value & (1 << 31) != 0 { + flags.push("TS4 (Open Thermistor TS4)"); } flags } @@ -453,8 +515,8 @@ fn print_status_flags(label: &str, value: u32, flags: Vec<&str>) { } /// Print operation status flags on multiple lines -fn print_operation_status(value: u32) { - let flags = decode_operation_status(value); +fn print_operation_status(value: u32, is_r3: bool) { + let flags = decode_operation_status(value, is_r3); println!("Operation Status: 0x{:08X}", value); for flag in flags { println!(" - {}", flag); @@ -687,6 +749,13 @@ impl SmartBattery { Ok(()) } + /// Read a ManufacturerAccess block command by writing the sub-command + /// to register 0x00 and reading the response from 0x44 + fn mac_read(&self, ec: &CrosEc, subcmd: u16, max_len: u16) -> EcResult> { + self.smbus_write_block(ec, 0x00, &subcmd.to_le_bytes())?; + self.read_block(ec, 0x44, max_len) + } + /// Authenticate the battery using SHA-1 HMAC challenge-response pub fn authenticate_battery(&self, ec: &CrosEc, auth_key: &[u8; 16]) -> EcResult { // 1. Generate a random 20-byte challenge @@ -842,10 +911,14 @@ impl SmartBattery { data.manufacture_date = self.read_i16(ec, SmartBatReg::ManufactureDate as u16)?; data.temperature = self.read_i16(ec, SmartBatReg::Temp as u16)?; data.voltage = self.read_i16(ec, SmartBatReg::Voltage as u16)?; + // Per datasheet: 0x3C=Cell4, 0x3D=Cell3, 0x3E=Cell2, 0x3F=Cell1 data.cell_voltage1 = self.read_i16(ec, SmartBatReg::CellVoltage1 as u16)?; data.cell_voltage2 = self.read_i16(ec, SmartBatReg::CellVoltage2 as u16)?; data.cell_voltage3 = self.read_i16(ec, SmartBatReg::CellVoltage3 as u16)?; data.cell_voltage4 = self.read_i16(ec, SmartBatReg::CellVoltage4 as u16)?; + + // Read firmware version (MAC 0x0002, available in SEALED mode) + data.firmware_version = self.mac_read(ec, 0x0002, 14).unwrap_or_default(); data.cycle_count = self.read_i16(ec, SmartBatReg::CycleCount as u16)?; data.device_name = self.read_string(ec, SmartBatReg::DeviceName as u16)?; data.manufacturer_name = self.read_string(ec, SmartBatReg::ManufacturerName as u16)?; @@ -901,6 +974,14 @@ impl SmartBattery { } } +/// Determine if the battery is bq40z50-R3 based on available data. +/// R3 returns 4-byte LifeTime3 (1×u32), R2 returns 16-byte (8×u16). +/// Defaults to R3 when no lifetime data is available. +fn is_r3(data: &BatteryData) -> bool { + // TODO: Use firmware_version device number once R2/R3 values are confirmed + data.lifetime3.len() != 16 +} + /// Display decoded battery data from a BatteryData struct pub fn display_battery_data(data: &BatteryData) { println!("Mode: 0x{:04X}", data.mode); @@ -942,6 +1023,9 @@ pub fn display_battery_data(data: &BatteryData) { println!("Cycle Count: {}", data.cycle_count); println!("Device Name: {}", data.device_name); println!("Manuf Name: {}", data.manufacturer_name); + if !data.firmware_version.is_empty() { + println!("FW Version: {}", hex_encode(&data.firmware_version)); + } // Unsealed data if !data.state_of_health.is_empty() { @@ -952,7 +1036,8 @@ pub fn display_battery_data(data: &BatteryData) { u16::from_le_bytes([soh[2], soh[3]]) / 100, u16::from_le_bytes([soh[2], soh[3]]) % 100, ); - print_operation_status(data.operation_status); + let r3 = is_r3(data); + print_operation_status(data.operation_status, r3); print_status_flags( "Safety Alert", data.safety_alert, @@ -963,11 +1048,15 @@ pub fn display_battery_data(data: &BatteryData) { data.safety_status, decode_safety_status(data.safety_status), ); - print_status_flags("PF Alert", data.pf_alert, decode_pf_status(data.pf_alert)); + print_status_flags( + "PF Alert", + data.pf_alert, + decode_pf_status(data.pf_alert, r3), + ); print_status_flags( "PF Status", data.pf_status, - decode_pf_status(data.pf_status), + decode_pf_status(data.pf_status, r3), ); if data.lifetime1.len() >= 32 { @@ -1068,41 +1157,56 @@ pub fn display_battery_data(data: &BatteryData) { if !data.lifetime3.is_empty() { println!("LifeTime3:"); - if data.lifetime3.len() >= 4 { + if data.lifetime3.len() == 4 { + // R3 format: 1×u32 Total FW Runtime in seconds + let runtime_s = u32::from_le_bytes([ + data.lifetime3[0], + data.lifetime3[1], + data.lifetime3[2], + data.lifetime3[3], + ]); + println!( + " Total FW Runtime: {}s ({:.1}h)", + runtime_s, + runtime_s as f64 / 3600.0 + ); + } else if data.lifetime3.len() == 16 { + // R2 format: 8×u16 (runtime + 7 temperature time bins) + let lt3 = &data.lifetime3; println!( " Total FW Runtime: {}h", - u16::from_le_bytes([data.lifetime3[0], data.lifetime3[1]]) + u16::from_le_bytes([lt3[0], lt3[1]]) ); println!( " Time in Under Temp: {}h", - u16::from_le_bytes([data.lifetime3[2], data.lifetime3[3]]) + u16::from_le_bytes([lt3[2], lt3[3]]) ); - } - if data.lifetime3.len() >= 16 { println!( " Time in Low Temp: {}h", - u16::from_le_bytes([data.lifetime3[4], data.lifetime3[5]]) + u16::from_le_bytes([lt3[4], lt3[5]]) ); println!( " Time in Std Temp Low: {}h", - u16::from_le_bytes([data.lifetime3[6], data.lifetime3[7]]) + u16::from_le_bytes([lt3[6], lt3[7]]) ); println!( " Time in Std Temp: {}h", - u16::from_le_bytes([data.lifetime3[8], data.lifetime3[9]]) + u16::from_le_bytes([lt3[8], lt3[9]]) ); println!( " Time in Std Temp High: {}h", - u16::from_le_bytes([data.lifetime3[10], data.lifetime3[11]]) + u16::from_le_bytes([lt3[10], lt3[11]]) ); println!( " Time in High Temp: {}h", - u16::from_le_bytes([data.lifetime3[12], data.lifetime3[13]]) + u16::from_le_bytes([lt3[12], lt3[13]]) ); println!( " Time in Over Temp: {}h", - u16::from_le_bytes([data.lifetime3[14], data.lifetime3[15]]) + u16::from_le_bytes([lt3[14], lt3[15]]) ); + } else { + println!(" Raw: {}", hex_encode(&data.lifetime3)); } } @@ -1141,7 +1245,7 @@ pub fn display_battery_data(data: &BatteryData) { u16::from_le_bytes([lt4[22], lt4[23]]) ); println!( - " Open Load Detection: {} (last @ cycle {})", + " Overload in Discharge: {} (last @ cycle {})", u16::from_le_bytes([lt4[24], lt4[25]]), u16::from_le_bytes([lt4[26], lt4[27]]) ); @@ -1193,7 +1297,7 @@ pub fn display_battery_data(data: &BatteryData) { u16::from_le_bytes([lt5[26], lt5[27]]) ); println!( - " Resistance Update Fails: {} (last @ cycle {})", + " Ra Disable: {} (last @ cycle {})", u16::from_le_bytes([lt5[28], lt5[29]]), u16::from_le_bytes([lt5[30], lt5[31]]) ); @@ -1343,12 +1447,12 @@ pub fn analyze_health(data: &BatteryData) { println!(" Cycle count: {}", data.cycle_count); if !data.state_of_health.is_empty() { let soh_mah = u16::from_le_bytes([data.state_of_health[0], data.state_of_health[1]]); - let soh_mwh = u16::from_le_bytes([data.state_of_health[2], data.state_of_health[3]]); + let soh_cwh = u16::from_le_bytes([data.state_of_health[2], data.state_of_health[3]]); println!( - " Remaining capacity: {}mAh ({}.{:02}Wh)", + " Full charge capacity: {}mAh ({}.{:02}Wh)", soh_mah, - soh_mwh / 100, - soh_mwh % 100 + soh_cwh / 100, + soh_cwh % 100 ); } println!(" Current cell balance: {}mV spread", cell_delta); From 0dc19089868e819b328a2d96b78f36333e1c31eb Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Wed, 18 Mar 2026 02:23:07 +0800 Subject: [PATCH 09/14] smartbattery: Decode firmware Signed-off-by: Daniel Schaefer --- framework_lib/src/smart_battery.rs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/framework_lib/src/smart_battery.rs b/framework_lib/src/smart_battery.rs index 4a2578cf..734bb7df 100644 --- a/framework_lib/src/smart_battery.rs +++ b/framework_lib/src/smart_battery.rs @@ -1023,7 +1023,22 @@ pub fn display_battery_data(data: &BatteryData) { println!("Cycle Count: {}", data.cycle_count); println!("Device Name: {}", data.device_name); println!("Manuf Name: {}", data.manufacturer_name); - if !data.firmware_version.is_empty() { + if data.firmware_version.len() >= 6 { + // MAC 0x0002 response: [subcmd_echo(2), device_num(2), fw_ver(2), build(2), ...] + let fw = &data.firmware_version; + let device_num = u16::from_le_bytes([fw[2], fw[3]]); + let fw_major = fw[5]; + let fw_minor = fw[4]; + let build = if fw.len() >= 8 { + format!(" Build=0x{:04X}", u16::from_le_bytes([fw[6], fw[7]])) + } else { + String::new() + }; + println!( + "FW Version: Device=0x{:04X} FW={:02}.{:02}{}", + device_num, fw_major, fw_minor, build + ); + } else if !data.firmware_version.is_empty() { println!("FW Version: {}", hex_encode(&data.firmware_version)); } From 42160378da7681b1f794c7bf2552aeddf233d900 Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Wed, 18 Mar 2026 02:23:41 +0800 Subject: [PATCH 10/14] smartbattery: runtime as days Signed-off-by: Daniel Schaefer --- framework_lib/src/smart_battery.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/framework_lib/src/smart_battery.rs b/framework_lib/src/smart_battery.rs index 734bb7df..9f23ff7b 100644 --- a/framework_lib/src/smart_battery.rs +++ b/framework_lib/src/smart_battery.rs @@ -1180,10 +1180,12 @@ pub fn display_battery_data(data: &BatteryData) { data.lifetime3[2], data.lifetime3[3], ]); + let hours = runtime_s as f64 / 3600.0; println!( - " Total FW Runtime: {}s ({:.1}h)", + " Total FW Runtime: {}s ({:.1}h / {:.1}d)", runtime_s, - runtime_s as f64 / 3600.0 + hours, + hours / 24.0 ); } else if data.lifetime3.len() == 16 { // R2 format: 8×u16 (runtime + 7 temperature time bins) From 0998377d18037c422d824357933368d2e53256e1 Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Wed, 18 Mar 2026 02:33:03 +0800 Subject: [PATCH 11/14] smartbattery: Read more when sealed Signed-off-by: Daniel Schaefer --- framework_lib/src/smart_battery.rs | 192 +++++++++++++++++++++++++++++ 1 file changed, 192 insertions(+) diff --git a/framework_lib/src/smart_battery.rs b/framework_lib/src/smart_battery.rs index 9f23ff7b..d5ffdf17 100644 --- a/framework_lib/src/smart_battery.rs +++ b/framework_lib/src/smart_battery.rs @@ -31,6 +31,18 @@ pub struct BatteryData { pub cycle_count: u16, pub device_name: String, pub manufacturer_name: String, + pub current: u16, + pub avg_current: u16, + pub rel_state_of_charge: u16, + pub abs_state_of_charge: u16, + pub remaining_capacity: u16, + pub full_charge_capacity: u16, + pub charging_current: u16, + pub charging_voltage: u16, + pub battery_status: u16, + pub design_capacity: u16, + pub design_voltage: u16, + pub device_chemistry: String, // Unsealed data (may be empty if not unsealed) pub state_of_health: Vec, pub operation_status: u32, @@ -65,6 +77,22 @@ impl BatteryData { writeln!(file, "cycle_count={:04X}", self.cycle_count)?; writeln!(file, "device_name={}", self.device_name)?; writeln!(file, "manufacturer_name={}", self.manufacturer_name)?; + writeln!(file, "current={:04X}", self.current)?; + writeln!(file, "avg_current={:04X}", self.avg_current)?; + writeln!(file, "rel_state_of_charge={:04X}", self.rel_state_of_charge)?; + writeln!(file, "abs_state_of_charge={:04X}", self.abs_state_of_charge)?; + writeln!(file, "remaining_capacity={:04X}", self.remaining_capacity)?; + writeln!( + file, + "full_charge_capacity={:04X}", + self.full_charge_capacity + )?; + writeln!(file, "charging_current={:04X}", self.charging_current)?; + writeln!(file, "charging_voltage={:04X}", self.charging_voltage)?; + writeln!(file, "battery_status={:04X}", self.battery_status)?; + writeln!(file, "design_capacity={:04X}", self.design_capacity)?; + writeln!(file, "design_voltage={:04X}", self.design_voltage)?; + writeln!(file, "device_chemistry={}", self.device_chemistry)?; writeln!( file, "state_of_health={}", @@ -124,6 +152,36 @@ impl BatteryData { "cycle_count" => data.cycle_count = u16::from_str_radix(value, 16).unwrap_or(0), "device_name" => data.device_name = value.to_string(), "manufacturer_name" => data.manufacturer_name = value.to_string(), + "current" => data.current = u16::from_str_radix(value, 16).unwrap_or(0), + "avg_current" => data.avg_current = u16::from_str_radix(value, 16).unwrap_or(0), + "rel_state_of_charge" => { + data.rel_state_of_charge = u16::from_str_radix(value, 16).unwrap_or(0) + } + "abs_state_of_charge" => { + data.abs_state_of_charge = u16::from_str_radix(value, 16).unwrap_or(0) + } + "remaining_capacity" => { + data.remaining_capacity = u16::from_str_radix(value, 16).unwrap_or(0) + } + "full_charge_capacity" => { + data.full_charge_capacity = u16::from_str_radix(value, 16).unwrap_or(0) + } + "charging_current" => { + data.charging_current = u16::from_str_radix(value, 16).unwrap_or(0) + } + "charging_voltage" => { + data.charging_voltage = u16::from_str_radix(value, 16).unwrap_or(0) + } + "battery_status" => { + data.battery_status = u16::from_str_radix(value, 16).unwrap_or(0) + } + "design_capacity" => { + data.design_capacity = u16::from_str_radix(value, 16).unwrap_or(0) + } + "design_voltage" => { + data.design_voltage = u16::from_str_radix(value, 16).unwrap_or(0) + } + "device_chemistry" => data.device_chemistry = value.to_string(), "state_of_health" => data.state_of_health = hex_decode(value), "operation_status" => { data.operation_status = u32::from_str_radix(value, 16).unwrap_or(0) @@ -166,11 +224,23 @@ enum SmartBatReg { Mode = 0x03, Temp = 0x08, Voltage = 0x09, + Current = 0x0A, + AverageCurrent = 0x0B, + RelativeStateOfCharge = 0x0D, + AbsoluteStateOfCharge = 0x0E, + RemainingCapacity = 0x0F, + FullChargeCapacity = 0x10, + ChargingCurrent = 0x14, + ChargingVoltage = 0x15, + BatteryStatus = 0x16, CycleCount = 0x17, + DesignCapacity = 0x18, + DesignVoltage = 0x19, ManufactureDate = 0x1B, SerialNum = 0x1C, ManufacturerName = 0x20, DeviceName = 0x21, + DeviceChemistry = 0x22, Authenticate = 0x2F, CellVoltage4 = 0x3C, CellVoltage3 = 0x3D, @@ -502,6 +572,52 @@ fn decode_pf_status(value: u32, is_r3: bool) -> Vec<&'static str> { flags } +/// Decode BatteryStatus register bits (SBS spec) +fn decode_battery_status(value: u16) -> Vec<&'static str> { + let mut flags = Vec::new(); + // Error code (bits 0-3) + match value & 0x0F { + 0 => {} + 1 => flags.push("EC=Busy"), + 3 => flags.push("EC=Unsupported"), + 4 => flags.push("EC=AccessDenied"), + 5 => flags.push("EC=Overflow"), + 7 => flags.push("EC=Unknown"), + _ => flags.push("EC=Reserved"), + } + if value & (1 << 4) != 0 { + flags.push("FD (Fully Discharged)"); + } + if value & (1 << 5) != 0 { + flags.push("FC (Fully Charged)"); + } + if value & (1 << 6) != 0 { + flags.push("DSG (Discharging)"); + } + if value & (1 << 7) != 0 { + flags.push("INIT (Initialization)"); + } + if value & (1 << 8) != 0 { + flags.push("RTA (Remaining Time Alarm)"); + } + if value & (1 << 9) != 0 { + flags.push("RCA (Remaining Capacity Alarm)"); + } + if value & (1 << 11) != 0 { + flags.push("TDA (Terminate Discharge Alarm)"); + } + if value & (1 << 12) != 0 { + flags.push("OTA (Over Temperature Alarm)"); + } + if value & (1 << 14) != 0 { + flags.push("TCA (Terminate Charge Alarm)"); + } + if value & (1 << 15) != 0 { + flags.push("OCA (Over-Charged Alarm)"); + } + flags +} + /// Print status flags with hex value (for Safety/PF registers) fn print_status_flags(label: &str, value: u32, flags: Vec<&str>) { if value == 0 { @@ -922,6 +1038,18 @@ impl SmartBattery { data.cycle_count = self.read_i16(ec, SmartBatReg::CycleCount as u16)?; data.device_name = self.read_string(ec, SmartBatReg::DeviceName as u16)?; data.manufacturer_name = self.read_string(ec, SmartBatReg::ManufacturerName as u16)?; + data.current = self.read_i16(ec, SmartBatReg::Current as u16)?; + data.avg_current = self.read_i16(ec, SmartBatReg::AverageCurrent as u16)?; + data.rel_state_of_charge = self.read_i16(ec, SmartBatReg::RelativeStateOfCharge as u16)?; + data.abs_state_of_charge = self.read_i16(ec, SmartBatReg::AbsoluteStateOfCharge as u16)?; + data.remaining_capacity = self.read_i16(ec, SmartBatReg::RemainingCapacity as u16)?; + data.full_charge_capacity = self.read_i16(ec, SmartBatReg::FullChargeCapacity as u16)?; + data.charging_current = self.read_i16(ec, SmartBatReg::ChargingCurrent as u16)?; + data.charging_voltage = self.read_i16(ec, SmartBatReg::ChargingVoltage as u16)?; + data.battery_status = self.read_i16(ec, SmartBatReg::BatteryStatus as u16)?; + data.design_capacity = self.read_i16(ec, SmartBatReg::DesignCapacity as u16)?; + data.design_voltage = self.read_i16(ec, SmartBatReg::DesignVoltage as u16)?; + data.device_chemistry = self.read_string(ec, SmartBatReg::DeviceChemistry as u16)?; // Unsealed data if let Some(key) = unseal_key { @@ -1020,6 +1148,70 @@ pub fn display_battery_data(data: &BatteryData) { data.cell_voltage4 / 1000, data.cell_voltage4 % 1000 ); + + // Current + let current_ma = data.current as i16; + let avg_current_ma = data.avg_current as i16; + println!( + "Current: {:.3}A (avg {:.3}A)", + current_ma as f32 / 1000.0, + avg_current_ma as f32 / 1000.0 + ); + + // Charge state - check CAPACITY_MODE (bit 15 of mode) + let capacity_10mwh = data.mode & 0x8000 != 0; + if capacity_10mwh { + println!( + "Charge: {}% ({:.2} / {:.2} Wh)", + data.rel_state_of_charge, + data.remaining_capacity as f32 / 100.0, + data.full_charge_capacity as f32 / 100.0 + ); + println!( + "Design: {:.2}Wh @ {}.{:03}V", + data.design_capacity as f32 / 100.0, + data.design_voltage / 1000, + data.design_voltage % 1000 + ); + } else { + println!( + "Charge: {}% ({} / {} mAh)", + data.rel_state_of_charge, data.remaining_capacity, data.full_charge_capacity + ); + println!( + "Design: {}mAh @ {}.{:03}V", + data.design_capacity, + data.design_voltage / 1000, + data.design_voltage % 1000 + ); + } + + // Charging info (only if battery is requesting charge) + if data.charging_current > 0 { + println!( + "Charging: {:.3}A @ {}.{:03}V", + data.charging_current as f32 / 1000.0, + data.charging_voltage / 1000, + data.charging_voltage % 1000 + ); + } + + if !data.device_chemistry.is_empty() { + println!("Chemistry: {}", data.device_chemistry); + } + + // Battery status + let status_flags = decode_battery_status(data.battery_status); + if status_flags.is_empty() { + println!("Bat Status: 0x{:04X}", data.battery_status); + } else { + println!( + "Bat Status: 0x{:04X} [{}]", + data.battery_status, + status_flags.join(", ") + ); + } + println!("Cycle Count: {}", data.cycle_count); println!("Device Name: {}", data.device_name); println!("Manuf Name: {}", data.manufacturer_name); From be90cac7da87f1e8094eb1fe6fb599cd9a471b18 Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Wed, 18 Mar 2026 02:39:09 +0800 Subject: [PATCH 12/14] smartbattery: Add sealed-mode SBS register reads Read standard SBS registers (current, charge state, capacity, design specs, battery status, chemistry) that are always available without unsealing. Also attempt MAC block reads (lifetime, safety, SOH) in sealed mode with fallback, and reorder display to group identity fields. --- framework_lib/src/smart_battery.rs | 88 ++++++++++++++++++++---------- 1 file changed, 59 insertions(+), 29 deletions(-) diff --git a/framework_lib/src/smart_battery.rs b/framework_lib/src/smart_battery.rs index d5ffdf17..f648384b 100644 --- a/framework_lib/src/smart_battery.rs +++ b/framework_lib/src/smart_battery.rs @@ -1051,7 +1051,36 @@ impl SmartBattery { data.design_voltage = self.read_i16(ec, SmartBatReg::DesignVoltage as u16)?; data.device_chemistry = self.read_string(ec, SmartBatReg::DeviceChemistry as u16)?; - // Unsealed data + // ManufacturerAccess block reads (typically readable in SEALED mode) + data.state_of_health = self + .read_bytes(ec, ManufReg::Soh as u16, 4) + .unwrap_or_default(); + data.operation_status = self + .read_i32(ec, ManufReg::OperationStatus as u16) + .unwrap_or(0); + data.safety_alert = self.read_i32(ec, ManufReg::SafetyAlert as u16).unwrap_or(0); + data.safety_status = self + .read_i32(ec, ManufReg::SafetyStatus as u16) + .unwrap_or(0); + data.pf_alert = self.read_i32(ec, ManufReg::PFAlert as u16).unwrap_or(0); + data.pf_status = self.read_i32(ec, ManufReg::PFStatus as u16).unwrap_or(0); + data.lifetime1 = self + .read_bytes(ec, ManufReg::LifeTimeDataBlock1 as u16, 32) + .unwrap_or_default(); + data.lifetime2 = self + .read_bytes(ec, ManufReg::LifeTimeDataBlock2 as u16, 20) + .unwrap_or_default(); + data.lifetime3 = self + .read_block(ec, ManufReg::LifeTimeDataBlock3 as u16, 16) + .unwrap_or_default(); + data.lifetime4 = self + .read_block(ec, ManufReg::LifeTimeDataBlock4 as u16, 32) + .unwrap_or_default(); + data.lifetime5 = self + .read_block(ec, ManufReg::LifeTimeDataBlock5 as u16, 32) + .unwrap_or_default(); + + // Unseal for registers that need it (re-read with full access) if let Some(key) = unseal_key { self.unseal(ec, (key >> 16) as u16, key as u16)?; @@ -1112,14 +1141,38 @@ fn is_r3(data: &BatteryData) -> bool { /// Display decoded battery data from a BatteryData struct pub fn display_battery_data(data: &BatteryData) { - println!("Mode: 0x{:04X}", data.mode); + // Identity + println!("Device Name: {}", data.device_name); + println!("Manuf Name: {}", data.manufacturer_name); println!("Serial Num: {:04X}", data.serial_num); - let day = data.manufacture_date & 0x1F; let month = (data.manufacture_date >> 5) & 0x0F; let year = (data.manufacture_date >> 9) + 1980; println!("Manuf Date: {:04}-{:02}-{:02}", year, month, day); + if !data.device_chemistry.is_empty() { + println!("Chemistry: {}", data.device_chemistry); + } + if data.firmware_version.len() >= 6 { + // MAC 0x0002 response: [subcmd_echo(2), device_num(2), fw_ver(2), build(2), ...] + let fw = &data.firmware_version; + let device_num = u16::from_le_bytes([fw[2], fw[3]]); + let fw_major = fw[5]; + let fw_minor = fw[4]; + let build = if fw.len() >= 8 { + format!(" Build=0x{:04X}", u16::from_le_bytes([fw[6], fw[7]])) + } else { + String::new() + }; + println!( + "FW Version: Device=0x{:04X} FW={:02}.{:02}{}", + device_num, fw_major, fw_minor, build + ); + } else if !data.firmware_version.is_empty() { + println!("FW Version: {}", hex_encode(&data.firmware_version)); + } + println!("Mode: 0x{:04X}", data.mode); + // Dynamic state let temp_c = (data.temperature as f32 / 10.0) - 273.15; println!("Temperature: {:.1}C", temp_c); @@ -1186,8 +1239,9 @@ pub fn display_battery_data(data: &BatteryData) { ); } - // Charging info (only if battery is requesting charge) - if data.charging_current > 0 { + // Charging info (only if actually charging — DSG bit clear) + let is_discharging = data.battery_status & (1 << 6) != 0; + if data.charging_current > 0 && !is_discharging { println!( "Charging: {:.3}A @ {}.{:03}V", data.charging_current as f32 / 1000.0, @@ -1196,10 +1250,6 @@ pub fn display_battery_data(data: &BatteryData) { ); } - if !data.device_chemistry.is_empty() { - println!("Chemistry: {}", data.device_chemistry); - } - // Battery status let status_flags = decode_battery_status(data.battery_status); if status_flags.is_empty() { @@ -1213,26 +1263,6 @@ pub fn display_battery_data(data: &BatteryData) { } println!("Cycle Count: {}", data.cycle_count); - println!("Device Name: {}", data.device_name); - println!("Manuf Name: {}", data.manufacturer_name); - if data.firmware_version.len() >= 6 { - // MAC 0x0002 response: [subcmd_echo(2), device_num(2), fw_ver(2), build(2), ...] - let fw = &data.firmware_version; - let device_num = u16::from_le_bytes([fw[2], fw[3]]); - let fw_major = fw[5]; - let fw_minor = fw[4]; - let build = if fw.len() >= 8 { - format!(" Build=0x{:04X}", u16::from_le_bytes([fw[6], fw[7]])) - } else { - String::new() - }; - println!( - "FW Version: Device=0x{:04X} FW={:02}.{:02}{}", - device_num, fw_major, fw_minor, build - ); - } else if !data.firmware_version.is_empty() { - println!("FW Version: {}", hex_encode(&data.firmware_version)); - } // Unsealed data if !data.state_of_health.is_empty() { From 1e6164171b7f560e7794bd2762733753cdcf7a9d Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Wed, 18 Mar 2026 02:44:00 +0800 Subject: [PATCH 13/14] smartbattery: Update health analysis Signed-off-by: Daniel Schaefer --- framework_lib/src/smart_battery.rs | 51 +++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/framework_lib/src/smart_battery.rs b/framework_lib/src/smart_battery.rs index f648384b..dc1f2822 100644 --- a/framework_lib/src/smart_battery.rs +++ b/framework_lib/src/smart_battery.rs @@ -1563,6 +1563,38 @@ pub fn analyze_health(data: &BatteryData) { issues.push("Permanent failure flags active"); } + // Check battery status alarm flags + if data.battery_status & (1 << 15) != 0 { + issues.push("Over-Charged Alarm active"); + } + if data.battery_status & (1 << 12) != 0 { + issues.push("Over Temperature Alarm active"); + } + if data.battery_status & (1 << 14) != 0 { + warnings.push("Terminate Charge Alarm active".to_string()); + } + if data.battery_status & (1 << 11) != 0 { + warnings.push("Terminate Discharge Alarm active".to_string()); + } + if data.battery_status & (1 << 4) != 0 { + warnings.push("Battery is fully discharged".to_string()); + } + + // Check capacity retention + if data.design_capacity > 0 && data.full_charge_capacity > 0 { + let retention = (data.full_charge_capacity as f32 / data.design_capacity as f32) * 100.0; + if retention < 50.0 { + issues.push("Severe capacity degradation (<50%)"); + } else if retention < 70.0 { + warnings.push(format!( + "Significant capacity degradation: {:.0}%", + retention + )); + } else if retention < 80.0 { + warnings.push(format!("Moderate capacity degradation: {:.0}%", retention)); + } + } + // Check cell voltage balance (current) let cells = [ data.cell_voltage1, @@ -1684,11 +1716,28 @@ pub fn analyze_health(data: &BatteryData) { // Print summary stats println!("\nSummary:"); println!(" Cycle count: {}", data.cycle_count); + if data.design_capacity > 0 && data.full_charge_capacity > 0 { + let capacity_10mwh = data.mode & 0x8000 != 0; + let retention = (data.full_charge_capacity as f32 / data.design_capacity as f32) * 100.0; + if capacity_10mwh { + println!( + " Capacity: {:.2} / {:.2} Wh ({:.0}%)", + data.full_charge_capacity as f32 / 100.0, + data.design_capacity as f32 / 100.0, + retention + ); + } else { + println!( + " Capacity: {} / {} mAh ({:.0}%)", + data.full_charge_capacity, data.design_capacity, retention + ); + } + } if !data.state_of_health.is_empty() { let soh_mah = u16::from_le_bytes([data.state_of_health[0], data.state_of_health[1]]); let soh_cwh = u16::from_le_bytes([data.state_of_health[2], data.state_of_health[3]]); println!( - " Full charge capacity: {}mAh ({}.{:02}Wh)", + " State of Health: {}mAh ({}.{:02}Wh)", soh_mah, soh_cwh / 100, soh_cwh % 100 From 56c98f1176ef28830d7e3ae0dfa4423cb8511640 Mon Sep 17 00:00:00 2001 From: Daniel Schaefer Date: Thu, 2 Jul 2026 15:14:06 +0800 Subject: [PATCH 14/14] EXAMPLES_ADVANCED: Add --smartbattery example Signed-off-by: Daniel Schaefer --- EXAMPLES_ADVANCED.md | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/EXAMPLES_ADVANCED.md b/EXAMPLES_ADVANCED.md index 98847228..0a457d5c 100644 --- a/EXAMPLES_ADVANCED.md +++ b/EXAMPLES_ADVANCED.md @@ -28,6 +28,50 @@ Keyboard backlight: 0% Keyboard backlight: 0% ``` +## Battery + +### Smart Battery + +Get smart battery information by talking to the battery directly through SMBUS. + +Allows unsealing the battery and reading privileged registers - this needs the +secret key and is not available to end-users. Implemented for Framework +internal analysis of RMA units. +End-users, please press enter to skip unseal key. + +``` +> sudo framework_tool --smartbattery +Enter unseal key in hex (e.g. 04143672), or press enter to skip: +Device Name: FRANEDA +Manuf Name: ATC +Serial Num: 0188 +Manuf Date: 2025-11-27 +Chemistry: LION +FW Version: Device=0x0045 FW=09.03 Build=0x4900 +Mode: 0x6001 +Temperature: 32.6C +Voltage: 17.719V + Cell 1: 4.430V + Cell 2: 4.429V + Cell 3: 4.431V + Cell 4: 4.428V +Current: 0.000A (avg 0.000A) +Charge: 98% (4776 / 4920 mAh) +Design: 4640mAh @ 15.640V +Bat Status: 0x00E0 [FC (Fully Charged), DSG (Discharging), INIT (Initialization)] +Cycle Count: 76 + +=== Battery Health Analysis === + +Status: HEALTHY + No issues detected. Battery is operating normally. + +Summary: + Cycle count: 76 + Capacity: 4920 / 4640 mAh (106%) + Current cell balance: 3mV spread +``` + ## PD ### Check PD state