power_monitor/powerd/
monitor.rs

1// Copyright 2024 The ChromiumOS Authors
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5//! Dbus monitor for polling signal from powerd to update power properties.
6
7use std::error::Error;
8use std::os::unix::io::RawFd;
9
10use base::AsRawDescriptor;
11use base::RawDescriptor;
12use base::ReadNotifier;
13use dbus::ffidisp::BusType;
14use dbus::ffidisp::Connection;
15use dbus::ffidisp::ConnectionItem;
16use dbus::ffidisp::WatchEvent;
17use protobuf::Message;
18use remain::sorted;
19use thiserror::Error;
20
21use crate::powerd::POWER_INTERFACE_NAME;
22use crate::protos::power_supply_properties::PowerSupplyProperties;
23use crate::BatteryData;
24use crate::PowerData;
25use crate::PowerMonitor;
26
27// Signal name from power_manager/dbus_constants.h.
28const POLL_SIGNAL_NAME: &str = "PowerSupplyPoll";
29
30#[sorted]
31#[derive(Error, Debug)]
32pub enum DBusMonitorError {
33    #[error("failed to convert protobuf message: {0}")]
34    ConvertProtobuf(protobuf::Error),
35    #[error("failed to add D-Bus match rule: {0}")]
36    DBusAddMatch(dbus::Error),
37    #[error("failed to connect to D-Bus: {0}")]
38    DBusConnect(dbus::Error),
39    #[error("failed to read D-Bus message: {0}")]
40    DBusRead(dbus::arg::TypeMismatchError),
41    #[error("multiple D-Bus fds")]
42    MultipleDBusFd,
43    #[error("no D-Bus fd")]
44    NoDBusFd,
45}
46
47pub struct DBusMonitor {
48    connection: Connection,
49    connection_fd: RawFd,
50    previous_data: Option<BatteryData>,
51}
52
53impl DBusMonitor {
54    /// Connects and configures a new D-Bus connection to listen for powerd's PowerSupplyPoll
55    /// signal.
56    pub fn connect() -> std::result::Result<Box<dyn PowerMonitor>, Box<dyn Error>> {
57        let connection =
58            Connection::get_private(BusType::System).map_err(DBusMonitorError::DBusConnect)?;
59        connection
60            .add_match(&format!(
61                "interface='{POWER_INTERFACE_NAME}',member='{POLL_SIGNAL_NAME}'"
62            ))
63            .map_err(DBusMonitorError::DBusAddMatch)?;
64        // Get the D-Bus connection's fd for async I/O. This should always return a single fd.
65        let fds: Vec<RawFd> = connection
66            .watch_fds()
67            .into_iter()
68            .filter(|w| w.readable())
69            .map(|w| w.fd())
70            .collect();
71        if fds.is_empty() {
72            return Err(DBusMonitorError::NoDBusFd.into());
73        }
74        if fds.len() > 1 {
75            return Err(DBusMonitorError::MultipleDBusFd.into());
76        }
77        Ok(Box::new(Self {
78            connection,
79            connection_fd: fds[0],
80            previous_data: None,
81        }))
82    }
83}
84
85fn denoise_value(new_val: u32, prev_val: u32, margin: f64) -> u32 {
86    if new_val.abs_diff(prev_val) as f64 / prev_val.min(new_val).max(1) as f64 >= margin {
87        new_val
88    } else {
89        prev_val
90    }
91}
92
93impl PowerMonitor for DBusMonitor {
94    /// Returns the newest pending `PowerData` message, if any.
95    /// Callers should poll `PowerMonitor` to determine when messages are available.
96    fn read_message(&mut self) -> std::result::Result<Option<PowerData>, Box<dyn Error>> {
97        // Select the newest available power message before converting to protobuf.
98        let newest_message: Option<dbus::Message> = self
99            .connection
100            .watch_handle(
101                self.connection_fd,
102                WatchEvent::Readable as std::os::raw::c_uint,
103            )
104            .fold(None, |last, item| match item {
105                ConnectionItem::Signal(message) => {
106                    // Ignore non-matching signals: although match rules are configured, some system
107                    // signals can still get through, eg. NameAcquired.
108                    let interface = match message.interface() {
109                        Some(i) => i,
110                        None => {
111                            return last;
112                        }
113                    };
114
115                    if &*interface != POWER_INTERFACE_NAME {
116                        return last;
117                    }
118
119                    let member = match message.member() {
120                        Some(m) => m,
121                        None => {
122                            return last;
123                        }
124                    };
125
126                    if &*member != POLL_SIGNAL_NAME {
127                        return last;
128                    }
129
130                    Some(message)
131                }
132                _ => last,
133            });
134
135        let previous_data = self.previous_data.take();
136        match newest_message {
137            Some(message) => {
138                let data_bytes: Vec<u8> = message.read1().map_err(DBusMonitorError::DBusRead)?;
139                let mut props = PowerSupplyProperties::new();
140                props
141                    .merge_from_bytes(&data_bytes)
142                    .map_err(DBusMonitorError::ConvertProtobuf)?;
143                let mut data: PowerData = props.into();
144                if let (Some(new_data), Some(previous)) = (data.battery.as_mut(), previous_data) {
145                    // The raw information from powerd signals isn't really that useful to
146                    // the guest. Voltage/current are volatile values, so the .0333 hZ
147                    // snapshot provided by powerd isn't particularly meaningful. We do
148                    // need to provide *something*, but we might as well make it less noisy
149                    // to avoid having the guest try to process mostly useless information.
150                    // charge_counter is potentially useful to the guest, but it doesn't
151                    // need to be higher precision than battery.percent.
152                    new_data.voltage = denoise_value(new_data.voltage, previous.voltage, 0.1);
153                    new_data.current = denoise_value(new_data.current, previous.current, 0.1);
154                    new_data.charge_counter =
155                        denoise_value(new_data.charge_counter, previous.charge_counter, 0.01);
156                }
157                self.previous_data = data.battery;
158                Ok(Some(data))
159            }
160            None => Ok(None),
161        }
162    }
163}
164
165impl AsRawDescriptor for DBusMonitor {
166    fn as_raw_descriptor(&self) -> RawDescriptor {
167        self.connection_fd
168    }
169}
170
171impl ReadNotifier for DBusMonitor {
172    fn get_read_notifier(&self) -> &dyn AsRawDescriptor {
173        self
174    }
175}