blob: 94dc070b3cb89231880727078de6537a9b0c6a60 [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use crate::error::PowerManagerError;
use crate::log_if_err;
use crate::message::{Message, MessageReturn};
use crate::node::Node;
use crate::types::Celsius;
use crate::utils::connect_proxy;
use anyhow::{format_err, Error};
use async_trait::async_trait;
use fidl_fuchsia_hardware_thermal as fthermal;
use fuchsia_inspect::{self as inspect, NumericProperty, Property};
use fuchsia_inspect_contrib::{inspect_log, nodes::BoundedListNode};
use fuchsia_zircon as zx;
use serde_derive::Deserialize;
use serde_json as json;
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
/// Node: TemperatureHandler
///
/// Summary: Responds to temperature requests from other nodes by polling the specified driver
/// using the thermal FIDL protocol
///
/// Handles Messages:
/// - ReadTemperature
///
/// Sends Messages: N/A
///
/// FIDL dependencies:
/// - fuchsia.hardware.thermal: the node uses this protocol to query the thermal driver
/// specified by `driver_path` in the TemperatureHandler constructor
/// A builder for constructing the TemperatureHandler node
pub struct TemperatureHandlerBuilder<'a> {
driver_path: String,
driver_proxy: Option<fthermal::DeviceProxy>,
inspect_root: Option<&'a inspect::Node>,
}
impl<'a> TemperatureHandlerBuilder<'a> {
pub fn new_with_driver_path(driver_path: String) -> Self {
Self { driver_path, driver_proxy: None, inspect_root: None }
}
#[cfg(test)]
pub fn new_with_proxy(driver_path: String, proxy: fthermal::DeviceProxy) -> Self {
Self { driver_path, driver_proxy: Some(proxy), inspect_root: None }
}
pub fn new_from_json(json_data: json::Value, _nodes: &HashMap<String, Rc<dyn Node>>) -> Self {
#[derive(Deserialize)]
struct Config {
driver_path: String,
};
#[derive(Deserialize)]
struct JsonData {
config: Config,
};
let data: JsonData = json::from_value(json_data).unwrap();
Self::new_with_driver_path(data.config.driver_path)
}
#[cfg(test)]
pub fn with_inspect_root(mut self, root: &'a inspect::Node) -> Self {
self.inspect_root = Some(root);
self
}
pub fn build(self) -> Result<Rc<TemperatureHandler>, Error> {
// Optionally use the default proxy
let proxy = if self.driver_proxy.is_none() {
connect_proxy::<fthermal::DeviceMarker>(&self.driver_path)?
} else {
self.driver_proxy.unwrap()
};
// Optionally use the default inspect root node
let inspect_root = self.inspect_root.unwrap_or(inspect::component::inspector().root());
Ok(Rc::new(TemperatureHandler {
driver_path: self.driver_path.clone(),
driver_proxy: proxy,
inspect: InspectData::new(
inspect_root,
format!("TemperatureHandler ({})", self.driver_path),
),
}))
}
}
pub struct TemperatureHandler {
driver_path: String,
driver_proxy: fthermal::DeviceProxy,
/// A struct for managing Component Inspection data
inspect: InspectData,
}
impl TemperatureHandler {
async fn handle_read_temperature(&self) -> Result<MessageReturn, PowerManagerError> {
fuchsia_trace::duration!(
"power_manager",
"TemperatureHandler::handle_read_temperature",
"driver" => self.driver_path.as_str()
);
// TODO(pshickel): What if multiple other nodes want to know about the current
// temperature from the same driver? If two requests come back to back, does it mean we
// should blindly query the driver twice, or maybe we want to cache the last value with
// some staleness tolerance parameter? There isn't a use-case for this yet, but it's
// something to think about.
let result = self.read_temperature().await;
log_if_err!(
result,
format!("Failed to read temperature from {}", self.driver_path).as_str()
);
fuchsia_trace::instant!(
"power_manager",
"TemperatureHandler::read_temperature_result",
fuchsia_trace::Scope::Thread,
"driver" => self.driver_path.as_str(),
"result" => format!("{:?}", result).as_str()
);
if result.is_ok() {
self.inspect.log_temperature_reading(*result.as_ref().unwrap())
} else {
self.inspect.read_errors.add(1);
self.inspect.last_read_error.set(format!("{}", result.as_ref().unwrap_err()).as_str());
}
Ok(MessageReturn::ReadTemperature(result?))
}
async fn read_temperature(&self) -> Result<Celsius, Error> {
fuchsia_trace::duration!(
"power_manager",
"TemperatureHandler::read_temperature",
"driver" => self.driver_path.as_str()
);
let (status, temperature) =
self.driver_proxy.get_temperature_celsius().await.map_err(|e| {
format_err!(
"{} ({}): get_temperature_celsius IPC failed: {}",
self.name(),
self.driver_path,
e
)
})?;
zx::Status::ok(status).map_err(|e| {
format_err!(
"{} ({}): get_temperature_celsius driver returned error: {}",
self.name(),
self.driver_path,
e
)
})?;
Ok(Celsius(temperature.into()))
}
}
#[async_trait(?Send)]
impl Node for TemperatureHandler {
fn name(&self) -> &'static str {
"TemperatureHandler"
}
async fn handle_message(&self, msg: &Message) -> Result<MessageReturn, PowerManagerError> {
match msg {
Message::ReadTemperature => self.handle_read_temperature().await,
_ => Err(PowerManagerError::Unsupported),
}
}
}
struct InspectData {
temperature_readings: RefCell<BoundedListNode>,
read_errors: inspect::UintProperty,
last_read_error: inspect::StringProperty,
}
impl InspectData {
/// Number of inspect samples to store in the `temperature_readings` BoundedListNode.
const NUM_INSPECT_TEMPERATURE_SAMPLES: usize = 10;
fn new(parent: &inspect::Node, name: String) -> Self {
// Create a local root node and properties
let root = parent.create_child(name);
let temperature_readings = RefCell::new(BoundedListNode::new(
root.create_child("temperature_readings"),
Self::NUM_INSPECT_TEMPERATURE_SAMPLES,
));
let read_errors = root.create_uint("read_temperature_error_count", 0);
let last_read_error = root.create_string("last_read_error", "");
// Pass ownership of the new node to the parent node, otherwise it'll be dropped
parent.record(root);
InspectData { temperature_readings, read_errors, last_read_error }
}
fn log_temperature_reading(&self, temperature: Celsius) {
inspect_log!(self.temperature_readings.borrow_mut(), temperature: temperature.0);
}
}
#[cfg(test)]
pub mod tests {
use super::*;
use fuchsia_async as fasync;
use futures::TryStreamExt;
use inspect::assert_inspect_tree;
/// Spawns a new task that acts as a fake thermal driver for testing purposes. The driver only
/// handles requests for GetTemperatureCelsius - trying to send any other requests to it is a
/// bug. Each GetTemperatureCelsius responds with a value provided by the supplied
/// `get_temperature` closure.
fn setup_fake_driver(
mut get_temperature: impl FnMut() -> Celsius + 'static,
) -> fthermal::DeviceProxy {
let (proxy, mut stream) =
fidl::endpoints::create_proxy_and_stream::<fthermal::DeviceMarker>().unwrap();
fasync::spawn_local(async move {
while let Ok(req) = stream.try_next().await {
match req {
Some(fthermal::DeviceRequest::GetTemperatureCelsius { responder }) => {
let _ =
responder.send(zx::Status::OK.into_raw(), get_temperature().0 as f32);
}
_ => assert!(false),
}
}
});
proxy
}
/// Sets up a test TemperatureHandler node that receives temperature readings from the
/// provided closure.
pub fn setup_test_node(
get_temperature: impl FnMut() -> Celsius + 'static,
) -> Rc<TemperatureHandler> {
TemperatureHandlerBuilder::new_with_proxy(
"Fake".to_string(),
setup_fake_driver(get_temperature),
)
.build()
.unwrap()
}
/// Tests that the node can handle the 'ReadTemperature' message as expected. The test
/// checks for the expected temperature value which is returned by the fake thermal driver.
#[fasync::run_singlethreaded(test)]
async fn test_read_temperature() {
// Readings for the fake temperature driver.
let temperature_readings = vec![1.2, 3.4, 5.6, 7.8, 9.0];
// Readings piped through the fake driver will be cast to f32 and back to f64.
let expected_readings: Vec<f64> =
temperature_readings.iter().map(|x| *x as f32 as f64).collect();
// Each ReadTemperature request will respond with the next element from
// `temperature_readings`, wrapping back around when the end of the vector is reached.
let mut index = 0;
let get_temperature = move || {
let value = temperature_readings[index];
index = (index + 1) % temperature_readings.len();
Celsius(value)
};
let node = setup_test_node(get_temperature);
// Send ReadTemperature message and check for expected value.
for expected_reading in expected_readings {
let result = node.handle_message(&Message::ReadTemperature).await;
let temperature = result.unwrap();
if let MessageReturn::ReadTemperature(t) = temperature {
assert_eq!(t.0, expected_reading);
} else {
assert!(false);
}
}
}
/// Tests that an unsupported message is handled gracefully and an error is returned.
#[fasync::run_singlethreaded(test)]
async fn test_unsupported_msg() {
let node = setup_test_node(|| Celsius(0.0));
match node.handle_message(&Message::GetTotalCpuLoad).await {
Err(PowerManagerError::Unsupported) => {}
e => panic!("Unexpected return value: {:?}", e),
}
}
/// Tests for the presence and correctness of dynamically-added inspect data
#[fasync::run_singlethreaded(test)]
async fn test_inspect_data() {
let temperature = Celsius(30.0);
let inspector = inspect::Inspector::new();
let node = TemperatureHandlerBuilder::new_with_proxy(
"Fake".to_string(),
setup_fake_driver(move || temperature),
)
.with_inspect_root(inspector.root())
.build()
.unwrap();
// The node will read the current temperature and log the sample into Inspect
node.handle_message(&Message::ReadTemperature).await.unwrap();
assert_inspect_tree!(
inspector,
root: {
"TemperatureHandler (Fake)": contains {
temperature_readings: {
"0": {
temperature: temperature.0,
"@time": inspect::testing::AnyProperty
}
}
}
}
);
}
/// Tests that well-formed configuration JSON does not panic the `new_from_json` function.
#[fasync::run_singlethreaded(test)]
async fn test_new_from_json() {
let json_data = json::json!({
"type": "TemperatureHandler",
"name": "temperature",
"config": {
"driver_path": "/dev/class/thermal/000"
}
});
let _ = TemperatureHandlerBuilder::new_from_json(json_data, &HashMap::new());
}
}