waveshare_ups_hat_e/
lib.rs

1// Copyright (c) 2025 Stuart Stock
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4#![doc = include_str!("../README.md")]
5
6pub mod error;
7pub mod registers;
8
9use error::Error;
10use i2cdev::core::I2CDevice;
11use i2cdev::linux::LinuxI2CDevice;
12use registers::{
13    BATTERY_REG, CELL_VOLTAGE_REG, CHARGING_REG, COMMUNICATION_REG, ChargerActivity, ChargingState,
14    CommState, POWEROFF_REG, USBC_VBUS_REG, UsbCInputState, UsbCPowerDelivery,
15};
16use crate::registers::SOFTWARE_REV_REG;
17
18/// Default I2C address of the Waveshare UPS Hat E
19pub const DEFAULT_I2C_ADDRESS: u16 = 0x2d;
20
21/// The default I2C bus device path to interface with the UPS Hat E
22pub const DEFAULT_I2C_DEV_PATH: &str = "/dev/i2c-1";
23
24/// The default threshold for low cell voltage, in millivolts. The UPS Hat E low-voltage cutoff
25/// is observed to be 3.2V (not documented), using 3.4V for our cutoff so there's enough power
26/// remaining to run a shutdown sequence.
27pub const DEFAULT_CELL_LOW_VOLTAGE_THRESHOLD: u16 = 3400; // 3.4V
28
29/// Value to write to the [`POWEROFF_REG`] register to initiate a power-off, or if read from
30/// [`POWEROFF_REG`], indicates that a power-off is pending.
31pub const POWEROFF_VALUE: u8 = 0x55;
32
33/// Represents the composite power state of the UPS Hat E.
34#[derive(Debug)]
35pub struct PowerState {
36    pub charging_state: ChargingState,
37    pub charger_activity: ChargerActivity,
38    pub usbc_input_state: UsbCInputState,
39    pub usbc_power_delivery: UsbCPowerDelivery,
40}
41
42/// Ability of the UPS to communicate with the on-board BQ4050 gas gauge chip and IP2368 battery
43/// charge management chip.
44#[derive(Debug)]
45pub struct CommunicationState {
46    pub bq4050: CommState,
47    pub ip2368: CommState,
48}
49
50/// Aggregate battery state of the UPS Hat E.
51///
52/// A negative `milliamps` value indicates the UPS is discharging the battery cells. A positive
53/// `milliamps` value indicates the UPS has USB-C power and is charging.
54///
55/// The Waveshare wiki states it may take a few charge cycles for the UPS to calibrate the
56/// `remaining_*` and `time_to_full_minutes` values correctly.
57#[derive(Debug)]
58pub struct BatteryState {
59    pub millivolts: u16,
60    pub milliamps: i16,
61    pub remaining_percent: u16,
62    pub remaining_capacity_milliamphours: u16,
63    pub remaining_runtime_minutes: u16,
64    pub time_to_full_minutes: u16,
65}
66
67/// Voltage readings for each of the four battery cells.
68#[derive(Debug)]
69pub struct CellVoltage {
70    pub cell_1_millivolts: u16,
71    pub cell_2_millivolts: u16,
72    pub cell_3_millivolts: u16,
73    pub cell_4_millivolts: u16,
74}
75
76/// Voltage and current readings from the USB-C port.
77#[derive(Debug)]
78pub struct UsbCVBus {
79    pub millivolts: u16,
80    pub milliamps: u16,
81    pub milliwatts: u16,
82}
83
84/// Monitor a [Waveshare UPS HAT E](https://www.waveshare.com/wiki/UPS_HAT_(E))
85/// (Uninterruptible Power Supply model E) for a Raspberry Pi.
86///
87/// This struct can monitor the UPS HAT status, such as battery voltage, current, power, and
88/// other interesting information
89pub struct UpsHatE {
90    i2c_bus: LinuxI2CDevice,
91}
92
93impl Default for UpsHatE {
94    /// Create a new instance of the UPS Hat E monitor using the default I2C bus device path and
95    /// address. This works in most cases.
96    fn default() -> Self {
97        let i2c = LinuxI2CDevice::new(DEFAULT_I2C_DEV_PATH, DEFAULT_I2C_ADDRESS)
98            .expect("Failed to open I2C device");
99
100        Self { i2c_bus: i2c }
101    }
102}
103
104impl UpsHatE {
105    /// Create a new instance of the UPS Hat E monitor using the default I2C bus device path and
106    /// address. This works in most cases.
107    pub fn new() -> Self {
108        Self::default()
109    }
110
111    /// Expert option: create a new instance of the UPS Hat E monitor using a custom I2C bus device
112    /// (custom path and address).
113    pub fn from_i2c_device(i2c_bus: LinuxI2CDevice) -> Self {
114        Self { i2c_bus }
115    }
116
117    pub fn get_cell_voltage(&mut self) -> Result<CellVoltage, Error> {
118        let data = self.read_block(CELL_VOLTAGE_REG.id, CELL_VOLTAGE_REG.length)?;
119
120        let voltages = CellVoltage {
121            cell_1_millivolts: data[0] as u16 | (data[1] as u16) << 8,
122            cell_2_millivolts: data[2] as u16 | (data[3] as u16) << 8,
123            cell_3_millivolts: data[4] as u16 | (data[5] as u16) << 8,
124            cell_4_millivolts: data[6] as u16 | (data[7] as u16) << 8,
125        };
126
127        Ok(voltages)
128    }
129
130    pub fn get_usbc_vbus(&mut self) -> Result<UsbCVBus, Error> {
131        let data = self.read_block(USBC_VBUS_REG.id, USBC_VBUS_REG.length)?;
132
133        let vbus = UsbCVBus {
134            millivolts: data[0] as u16 | (data[1] as u16) << 8,
135            milliamps: data[2] as u16 | (data[3] as u16) << 8,
136            milliwatts: data[4] as u16 | (data[5] as u16) << 8,
137        };
138
139        Ok(vbus)
140    }
141
142    pub fn get_battery_state(&mut self) -> Result<BatteryState, Error> {
143        let data = self.read_block(BATTERY_REG.id, BATTERY_REG.length)?;
144
145        let milliamps: i16 = {
146            let mut current = data[2] as i32 | (data[3] as i32) << 8;
147            // sign treatment mimics the reference python code
148            if current > 0x7fff {
149                current -= 0xffff;
150            }
151            current as i16
152        };
153
154        let mut remaining_runtime_minutes: u16 = 0;
155        let mut time_to_full_minutes: u16 = 0;
156
157        if milliamps < 0 {
158            // negative means discharging the battery
159            remaining_runtime_minutes = data[8] as u16 | (data[9] as u16) << 8;
160        } else {
161            // positive means charging the battery, power is available
162            time_to_full_minutes = data[10] as u16 | (data[11] as u16) << 8;
163        }
164
165        let state = BatteryState {
166            millivolts: data[0] as u16 | (data[1] as u16) << 8,
167            milliamps,
168            remaining_percent: data[4] as u16 | (data[5] as u16) << 8,
169            remaining_capacity_milliamphours: data[6] as u16 | (data[7] as u16) << 8,
170            remaining_runtime_minutes,
171            time_to_full_minutes,
172        };
173
174        Ok(state)
175    }
176
177    pub fn get_power_state(&mut self) -> Result<PowerState, Error> {
178        let data = self.read_block(CHARGING_REG.id, CHARGING_REG.length)?;
179        let byte = data[0];
180
181        let charger_activity = ChargerActivity::try_from(byte & 0b111)?;
182        let usbc_input_state = UsbCInputState::from(byte & (1 << 5) != 0);
183        let usbc_power_delivery = UsbCPowerDelivery::from(byte & (1 << 6) != 0);
184        let charging_state = ChargingState::from(byte & (1 << 7) != 0);
185
186        Ok(PowerState {
187            charging_state,
188            charger_activity,
189            usbc_input_state,
190            usbc_power_delivery,
191        })
192    }
193
194    pub fn get_communication_state(&mut self) -> Result<CommunicationState, Error> {
195        let data = self.read_block(COMMUNICATION_REG.id, COMMUNICATION_REG.length)?;
196        let byte = data[0];
197
198        let ip2368 = CommState::from(byte & (1 << 0) != 0);
199        let bq4050 = CommState::from(byte & (1 << 1) != 0);
200
201        Ok(CommunicationState { bq4050, ip2368 })
202    }
203    
204    pub fn get_software_revision(&mut self) -> Result<u8, Error> {
205        let data = self.read_block(SOFTWARE_REV_REG.id, SOFTWARE_REV_REG.length)?;
206        Ok(data[0])
207    }
208
209    /// Returns true if the overall battery voltage is less than or equal to
210    /// `(4 * DEFAULT_CELL_LOW_VOLTAGE_THRESHOLD)`.
211    ///
212    /// If you want an easy "is the battery low?" indicator, use this function.
213    #[allow(clippy::wrong_self_convention)]
214    pub fn is_battery_low(&mut self) -> Result<bool, Error> {
215        const CUTOFF: u32 = 4 * DEFAULT_CELL_LOW_VOLTAGE_THRESHOLD as u32;
216
217        let cell_voltages = self.get_cell_voltage()?;
218
219        let total_voltage: u32 = (cell_voltages.cell_1_millivolts
220            + cell_voltages.cell_2_millivolts
221            + cell_voltages.cell_3_millivolts
222            + cell_voltages.cell_4_millivolts) as u32;
223
224        Ok(total_voltage <= CUTOFF)
225    }
226
227    /// Unconditionally and uncleanly power-off the Raspberry Pi in 30 seconds.
228    ///
229    /// This operation cannot be canceled once called.
230    pub fn force_power_off(&mut self) -> Result<(), Error> {
231        self.i2c_bus
232            .smbus_write_byte_data(POWEROFF_REG.id, POWEROFF_VALUE)?;
233        Ok(())
234    }
235
236    /// Returns true if a power-off has been initiated.
237    #[allow(clippy::wrong_self_convention)]
238    pub fn is_power_off_pending(&mut self) -> Result<bool, Error> {
239        let data = self.read_block(POWEROFF_REG.id, POWEROFF_REG.length)?;
240        Ok(data[0] == POWEROFF_VALUE)
241    }
242
243    fn read_block(&mut self, register: u8, length: u8) -> Result<Vec<u8>, Error> {
244        let data = self.i2c_bus.smbus_read_i2c_block_data(register, length)?;
245
246        if data.len() != length as usize {
247            return Err(Error::InvalidDataLen(register, length as usize, data.len()));
248        }
249
250        Ok(data)
251    }
252}