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, USBC_VBUS_REG,
14    ChargerActivity, ChargingState, UsbCInputState, UsbCPowerDelivery,
15};
16
17/// Default I2C address of the Waveshare UPS Hat E
18pub const DEFAULT_I2C_ADDRESS: u16 = 0x2d;
19
20/// The default I2C bus device path to interface with the UPS Hat E
21pub const DEFAULT_I2C_DEV_PATH: &str = "/dev/i2c-1";
22
23/// Represent the composite power state of the UPS Hat E.
24#[derive(Debug)]
25pub struct PowerState {
26    pub charging_state: ChargingState,
27    pub charger_activity: ChargerActivity,
28    pub usbc_input_state: UsbCInputState,
29    pub usbc_power_delivery: UsbCPowerDelivery,
30}
31
32/// Aggregate battery state of the UPS Hat E.
33///
34/// A negative `milliamps` value indicates the UPS is discharging the battery cells. A positive
35/// `milliamps` value indicates the UPS has USB-C power and is charging.
36///
37/// The Waveshare wiki states it may take a few charge cycles for the UPS to calibrate the
38/// `remaining_*` and `time_to_full_minutes` values correctly.
39#[derive(Debug)]
40pub struct BatteryState {
41    pub millivolts: u16,
42    pub milliamps: i16,
43    pub remaining_percent: u16,
44    pub remaining_capacity_milliamphours: u16,
45    pub remaining_runtime_minutes: u16,
46    pub time_to_full_minutes: u16,
47}
48
49/// Voltage readings for each of the four battery cells.
50#[derive(Debug)]
51pub struct CellVoltage {
52    pub cell_1_millivolts: u16,
53    pub cell_2_millivolts: u16,
54    pub cell_3_millivolts: u16,
55    pub cell_4_millivolts: u16,
56}
57
58/// Voltage and current readings from the USB-C port.
59#[derive(Debug)]
60pub struct UsbCVBus {
61    pub millivolts: u16,
62    pub milliamps: u16,
63    pub milliwatts: u16,
64}
65
66/// Monitor a [Waveshare UPS HAT E](https://www.waveshare.com/wiki/UPS_HAT_(E))
67/// (Uninterruptible Power Supply model E) for a Raspberry Pi.
68///
69/// This struct can monitor the UPS HAT status, such as battery voltage, current, power, and
70/// other interesting information
71pub struct UpsHatE {
72    i2c_bus: LinuxI2CDevice,
73}
74
75impl Default for UpsHatE {
76    /// Create a new instance of the UPS Hat E monitor using the default I2C bus device path and
77    /// address. This works in most cases.
78    fn default() -> Self {
79        let i2c = LinuxI2CDevice::new(DEFAULT_I2C_DEV_PATH, DEFAULT_I2C_ADDRESS)
80            .expect("Failed to open I2C device");
81
82        Self { i2c_bus: i2c }
83    }
84}
85
86impl UpsHatE {
87    /// Create a new instance of the UPS Hat E monitor using the default I2C bus device path and
88    /// address. This works in most cases.
89    pub fn new() -> Self {
90        Self::default()
91    }
92
93    /// Expert option: create a new instance of the UPS Hat E monitor using a custom I2C bus device
94    /// (custom path and address).
95    pub fn from_i2c_device(i2c_bus: LinuxI2CDevice) -> Self {
96        Self { i2c_bus }
97    }
98
99    pub fn get_cell_voltage(&mut self) -> Result<CellVoltage, Error> {
100        let data = self.read_block(CELL_VOLTAGE_REG.id, CELL_VOLTAGE_REG.length)?;
101
102        let voltages = CellVoltage {
103            cell_1_millivolts: data[0] as u16 | (data[1] as u16) << 8,
104            cell_2_millivolts: data[2] as u16 | (data[3] as u16) << 8,
105            cell_3_millivolts: data[4] as u16 | (data[5] as u16) << 8,
106            cell_4_millivolts: data[6] as u16 | (data[7] as u16) << 8,
107        };
108
109        Ok(voltages)
110    }
111
112    pub fn get_usbc_vbus(&mut self) -> Result<UsbCVBus, Error> {
113        let data = self.read_block(USBC_VBUS_REG.id, USBC_VBUS_REG.length)?;
114
115        let vbus = UsbCVBus {
116            millivolts: data[0] as u16 | (data[1] as u16) << 8,
117            milliamps: data[2] as u16 | (data[3] as u16) << 8,
118            milliwatts: data[4] as u16 | (data[5] as u16) << 8,
119        };
120
121        Ok(vbus)
122    }
123
124    pub fn get_battery_state(&mut self) -> Result<BatteryState, Error> {
125        let data = self.read_block(BATTERY_REG.id, BATTERY_REG.length)?;
126
127        let milliamps: i16 = {
128            let mut current = data[2] as i32 | (data[3] as i32) << 8;
129            // sign treatment mimics the reference python code
130            if current > 0x7fff {
131                current -= 0xffff;
132            }
133            current as i16
134        };
135
136        let mut remaining_runtime_minutes: u16 = 0;
137        let mut time_to_full_minutes: u16 = 0;
138
139        if milliamps < 0 {
140            // negative means discharging the battery
141            remaining_runtime_minutes = data[8] as u16 | (data[9] as u16) << 8;
142        } else {
143            // positive means charging the battery, power is available
144            time_to_full_minutes = data[10] as u16 | (data[11] as u16) << 8;
145        }
146
147        let state = BatteryState {
148            millivolts: data[0] as u16 | (data[1] as u16) << 8,
149            milliamps,
150            remaining_percent: data[4] as u16 | (data[5] as u16) << 8,
151            remaining_capacity_milliamphours: data[6] as u16 | (data[7] as u16) << 8,
152            remaining_runtime_minutes,
153            time_to_full_minutes,
154        };
155
156        Ok(state)
157    }
158
159    pub fn get_power_state(&mut self) -> Result<PowerState, Error> {
160        let data = self.read_block(CHARGING_REG.id, CHARGING_REG.length)?;
161        let byte = data[0];
162
163        let charger_activity = ChargerActivity::try_from(byte & 0b111)?;
164        let usbc_input_state = UsbCInputState::from(byte & (1 << 5) != 0);
165        let usbc_power_delivery = UsbCPowerDelivery::from(byte & (1 << 6) != 0);
166        let charging_state = ChargingState::from(byte & (1 << 7) != 0);
167
168        Ok(PowerState {
169            charging_state,
170            charger_activity,
171            usbc_input_state,
172            usbc_power_delivery,
173        })
174    }
175
176    fn read_block(&mut self, register: u8, length: u8) -> Result<Vec<u8>, Error> {
177        let data = self.i2c_bus.smbus_read_i2c_block_data(register, length)?;
178
179        if data.len() != length as usize {
180            return Err(Error::InvalidDataLen(register, length as usize, data.len()));
181        }
182
183        Ok(data)
184    }
185}