power: Add interface for accessing platform-profiles

This commit is contained in:
Vicki Pfau 2025-03-20 16:21:26 -07:00
parent b26cc0f45c
commit 45edfe2c7c
10 changed files with 305 additions and 12 deletions

View file

@ -230,6 +230,38 @@
</interface>
<!--
com.steampowered.SteamOSManager1.PerformanceProfile1
@short_description: Optional interface for platform power properties.
-->
<interface name="com.steampowered.SteamOSManager1.PerformanceProfile1">
<!--
AvailablePerformanceProfiles:
Enumerate the supported performance profiles available on the system.
A list of supported profiles.
-->
<property name="AvailablePerformanceProfiles" type="as" access="read"/>
<!--
PerformanceProfile:
The current perrormance profile. Valid values come from the
AvailablePerformanceProfiles property.
-->
<property name="PerformanceProfile" type="s" access="readwrite"/>
<!--
SuggestedDefaultPerformanceProfile:
The suggested default performance profile for the system.
-->
<property name="SuggestedDefaultPerformanceProfile" type="s" access="read"/>
</interface>
<!--
com.steampowered.SteamOSManager1.Storage1
@short_description: Optional interface for managing storage devices

View file

@ -0,0 +1,3 @@
[performance_profile]
platform_profile_name = "lenovo-wmi-gamezone"
suggested_default = "custom"

View file

@ -16,8 +16,8 @@ use steamos_manager::power::{CPUScalingGovernor, GPUPerformanceLevel, GPUPowerPr
use steamos_manager::proxy::{
AmbientLightSensor1Proxy, BatteryChargeLimit1Proxy, CpuScaling1Proxy, FactoryReset1Proxy,
FanControl1Proxy, GpuPerformanceLevel1Proxy, GpuPowerProfile1Proxy, HdmiCec1Proxy,
Manager2Proxy, Storage1Proxy, TdpLimit1Proxy, UpdateBios1Proxy, UpdateDock1Proxy,
WifiDebug1Proxy, WifiDebugDump1Proxy, WifiPowerManagement1Proxy,
Manager2Proxy, PerformanceProfile1Proxy, Storage1Proxy, TdpLimit1Proxy, UpdateBios1Proxy,
UpdateDock1Proxy, WifiDebug1Proxy, WifiDebugDump1Proxy, WifiPowerManagement1Proxy,
};
use steamos_manager::wifi::{WifiBackend, WifiDebugMode, WifiPowerManagement};
use zbus::fdo::{IntrospectableProxy, PropertiesProxy};
@ -111,6 +111,21 @@ enum Commands {
/// Get the minimum allowed TDP limit
GetTDPLimitMin,
/// Get the performance profiles supported on this device
GetAvailablePerformanceProfiles,
/// Get the current performance profile
GetPerformanceProfile,
/// Set the performance profile
SetPerformanceProfile {
/// Valid profiles can be found using get-available-performance-profiles.
profile: String,
},
/// Get the suggested default performance profile
SuggestedDefaultPerformanceProfile,
/// Set the Wi-Fi backend, if possible
SetWifiBackend {
/// Supported backends are `iwd`, `wpa_supplicant`
@ -348,6 +363,28 @@ async fn main() -> Result<()> {
let value = proxy.manual_gpu_clock_min().await?;
println!("Manual GPU Clock Min: {value}");
}
Commands::GetAvailablePerformanceProfiles => {
let proxy = PerformanceProfile1Proxy::new(&conn).await?;
let profiles = proxy.available_performance_profiles().await?;
println!("Profiles:\n");
for name in profiles.into_iter().sorted() {
println!("- {name}");
}
}
Commands::GetPerformanceProfile => {
let proxy = PerformanceProfile1Proxy::new(&conn).await?;
let profile = proxy.performance_profile().await?;
println!("Performance Profile: {profile}");
}
Commands::SetPerformanceProfile { profile } => {
let proxy = PerformanceProfile1Proxy::new(&conn).await?;
proxy.set_performance_profile(profile.as_str()).await?;
}
Commands::SuggestedDefaultPerformanceProfile => {
let proxy = PerformanceProfile1Proxy::new(&conn).await?;
let profile = proxy.suggested_default_performance_profile().await?;
println!("Suggested Default Performance Profile: {profile}");
}
Commands::SetTDPLimit { limit } => {
let proxy = TdpLimit1Proxy::new(&conn).await?;
proxy.set_tdp_limit(*limit).await?;

View file

@ -390,6 +390,7 @@ pub mod test {
tdp_limit: None,
gpu_clocks: None,
battery_charge_limit: None,
performance_profile: None,
}));
let fan_control = FanControl::new(connection);

View file

@ -27,7 +27,8 @@ use crate::job::JobManager;
use crate::platform::platform_config;
use crate::power::{
set_cpu_scaling_governor, set_gpu_clocks, set_gpu_performance_level, set_gpu_power_profile,
set_max_charge_level, set_tdp_limit, CPUScalingGovernor, GPUPerformanceLevel, GPUPowerProfile,
set_max_charge_level, set_platform_profile, set_tdp_limit, CPUScalingGovernor,
GPUPerformanceLevel, GPUPowerProfile,
};
use crate::process::{run_script, script_output};
use crate::wifi::{
@ -429,6 +430,19 @@ impl SteamOSManager {
.map_err(to_zbus_fdo_error)
}
async fn set_performance_profile(&self, profile: &str) -> fdo::Result<()> {
let config = platform_config().await.map_err(to_zbus_fdo_error)?;
let config = config
.as_ref()
.and_then(|config| config.performance_profile.as_ref())
.ok_or(fdo::Error::Failed(String::from(
"No performance platform-profile configured",
)))?;
set_platform_profile(&config.platform_profile_name, profile)
.await
.map_err(to_zbus_fdo_error)
}
/// A version property.
#[zbus(property(emits_changed_signal = "const"))]
async fn version(&self) -> u32 {

View file

@ -26,9 +26,9 @@ use crate::job::JobManagerCommand;
use crate::platform::platform_config;
use crate::power::{
get_available_cpu_scaling_governors, get_available_gpu_performance_levels,
get_available_gpu_power_profiles, get_cpu_scaling_governor, get_gpu_clocks,
get_gpu_clocks_range, get_gpu_performance_level, get_gpu_power_profile, get_max_charge_level,
get_tdp_limit, get_tdp_limit_range,
get_available_gpu_power_profiles, get_available_platform_profiles, get_cpu_scaling_governor,
get_gpu_clocks, get_gpu_clocks_range, get_gpu_performance_level, get_gpu_power_profile,
get_max_charge_level, get_platform_profile, get_tdp_limit, get_tdp_limit_range,
};
use crate::wifi::{
get_wifi_backend, get_wifi_power_management_state, list_wifi_interfaces, WifiBackend,
@ -143,6 +143,10 @@ struct Manager2 {
channel: Sender<Command>,
}
struct PerformanceProfile1 {
proxy: Proxy<'static>,
}
struct Storage1 {
proxy: Proxy<'static>,
job_manager: UnboundedSender<JobManagerCommand>,
@ -456,6 +460,54 @@ impl Manager2 {
}
}
#[interface(name = "com.steampowered.SteamOSManager1.PerformanceProfile1")]
impl PerformanceProfile1 {
#[zbus(property(emits_changed_signal = "false"))]
async fn available_performance_profiles(&self) -> fdo::Result<Vec<String>> {
let config = platform_config().await.map_err(to_zbus_fdo_error)?;
let config = config
.as_ref()
.and_then(|config| config.performance_profile.as_ref())
.ok_or(fdo::Error::Failed(String::from(
"No performance platform-profile configured",
)))?;
get_available_platform_profiles(&config.platform_profile_name)
.await
.map_err(to_zbus_fdo_error)
}
#[zbus(property(emits_changed_signal = "false"))]
async fn performance_profile(&self) -> fdo::Result<String> {
let config = platform_config().await.map_err(to_zbus_fdo_error)?;
let config = config
.as_ref()
.and_then(|config| config.performance_profile.as_ref())
.ok_or(fdo::Error::Failed(String::from(
"No performance platform-profile configured",
)))?;
get_platform_profile(&config.platform_profile_name)
.await
.map_err(to_zbus_fdo_error)
}
#[zbus(property)]
async fn set_performance_profile(&self, profile: &str) -> zbus::Result<()> {
self.proxy.call("SetPerformanceProfile", &(profile)).await
}
#[zbus(property(emits_changed_signal = "const"))]
async fn suggested_default_performance_profile(&self) -> fdo::Result<String> {
let config = platform_config().await.map_err(to_zbus_fdo_error)?;
let config = config
.as_ref()
.and_then(|config| config.performance_profile.as_ref())
.ok_or(fdo::Error::Failed(String::from(
"No performance platform-profile configured",
)))?;
Ok(config.suggested_default.to_string())
}
}
#[interface(name = "com.steampowered.SteamOSManager1.Storage1")]
impl Storage1 {
async fn format_device(
@ -591,6 +643,9 @@ async fn create_config_interfaces(
let fan_control = FanControl1 {
proxy: proxy.clone(),
};
let performance_profile = PerformanceProfile1 {
proxy: proxy.clone(),
};
let storage = Storage1 {
proxy: proxy.clone(),
job_manager: job_manager.clone(),
@ -612,6 +667,16 @@ async fn create_config_interfaces(
object_server.at(MANAGER_PATH, fan_control).await?;
}
if let Some(ref config) = config.performance_profile {
if !get_available_platform_profiles(&config.platform_profile_name)
.await
.unwrap_or_default()
.is_empty()
{
object_server.at(MANAGER_PATH, performance_profile).await?;
}
}
if config.storage.is_some() {
object_server.at(MANAGER_PATH, storage).await?;
}
@ -756,8 +821,8 @@ mod test {
use crate::hardware::test::fake_model;
use crate::hardware::SteamDeckVariant;
use crate::platform::{
BatteryChargeLimitConfig, PlatformConfig, RangeConfig, ResetConfig, ScriptConfig,
ServiceConfig, StorageConfig,
BatteryChargeLimitConfig, PerformanceProfileConfig, PlatformConfig, RangeConfig,
ResetConfig, ScriptConfig, ServiceConfig, StorageConfig,
};
use crate::systemd::test::{MockManager, MockUnit};
use crate::{path, power, testing};
@ -791,6 +856,10 @@ mod test {
hwmon_name: String::from("steamdeck_hwmon"),
attribute: String::from("max_battery_charge_level"),
}),
performance_profile: Some(PerformanceProfileConfig {
platform_profile_name: String::from("power-driver"),
suggested_default: String::from("balanced"),
}),
})
}
@ -989,6 +1058,24 @@ mod test {
.unwrap());
}
#[tokio::test]
async fn interface_matches_performance_profile1() {
let test = start(all_config()).await.expect("start");
assert!(
test_interface_matches::<PerformanceProfile1>(&test.connection)
.await
.unwrap()
);
}
#[tokio::test]
async fn interface_missing_performance_profile1() {
let test = start(None).await.expect("start");
assert!(test_interface_missing::<PerformanceProfile1>(&test.connection).await);
}
#[tokio::test]
async fn interface_matches_storage1() {
let test = start(all_config()).await.expect("start");

View file

@ -34,6 +34,7 @@ pub(crate) struct PlatformConfig {
pub tdp_limit: Option<RangeConfig<u32>>,
pub gpu_clocks: Option<RangeConfig<u32>>,
pub battery_charge_limit: Option<BatteryChargeLimitConfig>,
pub performance_profile: Option<PerformanceProfileConfig>,
}
#[derive(Clone, Deserialize, Debug)]
@ -113,6 +114,12 @@ pub(crate) struct BatteryChargeLimitConfig {
pub attribute: String,
}
#[derive(Clone, Deserialize, Debug)]
pub(crate) struct PerformanceProfileConfig {
pub suggested_default: String,
pub platform_profile_name: String,
}
#[derive(Clone, Default, Deserialize, Debug)]
pub(crate) struct FormatDeviceConfig {
pub script: PathBuf,
@ -139,6 +146,7 @@ impl PlatformConfig {
async fn load() -> Result<Option<PlatformConfig>> {
let platform = match device_type().await? {
DeviceType::SteamDeck => "jupiter",
DeviceType::LegionGoS => "legion-go-s",
_ => return Ok(None),
};
let config = read_to_string(format!(

View file

@ -37,6 +37,8 @@ const GPU_CLOCK_LEVELS_SUFFIX: &str = "device/pp_dpm_sclk";
const CPU_SCALING_GOVERNOR_SUFFIX: &str = "scaling_governor";
const CPU_SCALING_AVAILABLE_GOVERNORS_SUFFIX: &str = "scaling_available_governors";
const PLATFORM_PROFILE_PREFIX: &str = "/sys/class/platform-profile";
const TDP_LIMIT1: &str = "power1_cap";
const TDP_LIMIT2: &str = "power2_cap";
@ -346,24 +348,32 @@ pub(crate) async fn get_gpu_clocks() -> Result<u32> {
Ok(0)
}
async fn find_hwmon(hwmon: &str) -> Result<PathBuf> {
let mut dir = fs::read_dir(path(HWMON_PREFIX)).await?;
async fn find_sysdir(prefix: impl AsRef<Path>, expected: &str) -> Result<PathBuf> {
let mut dir = fs::read_dir(prefix.as_ref()).await?;
loop {
let base = match dir.next_entry().await? {
Some(entry) => entry.path(),
None => bail!("hwmon not found"),
None => bail!("prefix not found"),
};
let file_name = base.join("name");
let name = fs::read_to_string(file_name.as_path())
.await?
.trim()
.to_string();
if name == hwmon {
if name == expected {
return Ok(base);
}
}
}
async fn find_hwmon(hwmon: &str) -> Result<PathBuf> {
find_sysdir(path(HWMON_PREFIX), hwmon).await
}
async fn find_platform_profile(name: &str) -> Result<PathBuf> {
find_sysdir(path(PLATFORM_PROFILE_PREFIX), name).await
}
pub(crate) async fn get_tdp_limit() -> Result<u32> {
let base = find_hwmon(GPU_HWMON_NAME).await?;
let power1cap = fs::read_to_string(base.join(TDP_LIMIT1)).await?;
@ -435,6 +445,33 @@ pub(crate) async fn set_max_charge_level(limit: i32) -> Result<()> {
.inspect_err(|message| error!("Error writing to sysfs file: {message}"))
}
pub(crate) async fn get_available_platform_profiles(name: &str) -> Result<Vec<String>> {
let base = find_platform_profile(name).await?;
Ok(fs::read_to_string(base.join("choices"))
.await
.map_err(|message| anyhow!("Error reading sysfs: {message}"))?
.trim()
.split(' ')
.map(ToString::to_string)
.collect())
}
pub(crate) async fn get_platform_profile(name: &str) -> Result<String> {
let base = find_platform_profile(name).await?;
Ok(fs::read_to_string(base.join("profile"))
.await
.map_err(|message| anyhow!("Error reading sysfs: {message}"))?
.trim()
.to_string())
}
pub(crate) async fn set_platform_profile(name: &str, profile: &str) -> Result<()> {
let base = find_platform_profile(name).await?;
fs::write(base.join("profile"), profile.as_bytes())
.await
.map_err(|message| anyhow!("Error writing to sysfs: {message}"))
}
#[cfg(test)]
pub(crate) mod test {
use super::*;
@ -484,6 +521,11 @@ pub(crate) mod test {
write(base.join("max_battery_charge_level"), "10\n").await?;
let base = path(PLATFORM_PROFILE_PREFIX).join("platform-profile0");
create_dir_all(&base).await?;
write_synced(base.join("name"), b"power-driver\n").await?;
write_synced(base.join("choices"), b"a b c\n").await?;
Ok(())
}
@ -1140,4 +1182,36 @@ CCLK_RANGE in Core0:
assert!(set_max_charge_level(101).await.is_err());
assert!(set_max_charge_level(-1).await.is_err());
}
#[tokio::test]
async fn read_available_performance_profiles() {
let _h = testing::start();
assert!(get_available_platform_profiles("power-driver")
.await
.is_err());
let base = path(PLATFORM_PROFILE_PREFIX).join("platform-profile0");
create_dir_all(&base).await.unwrap();
assert!(get_available_platform_profiles("power-driver")
.await
.is_err());
write_synced(base.join("name"), b"power-driver\n")
.await
.unwrap();
assert!(get_available_platform_profiles("power-driver")
.await
.is_err());
write_synced(base.join("choices"), b"a b c\n")
.await
.unwrap();
assert_eq!(
get_available_platform_profiles("power-driver")
.await
.unwrap(),
&["a", "b", "c"]
);
}
}

View file

@ -23,6 +23,7 @@ mod gpu_performance_level1;
mod gpu_power_profile1;
mod hdmi_cec1;
mod manager2;
mod performance_profile1;
mod storage1;
mod tdp_limit1;
mod update_bios1;
@ -39,6 +40,7 @@ pub use crate::proxy::gpu_performance_level1::GpuPerformanceLevel1Proxy;
pub use crate::proxy::gpu_power_profile1::GpuPowerProfile1Proxy;
pub use crate::proxy::hdmi_cec1::HdmiCec1Proxy;
pub use crate::proxy::manager2::Manager2Proxy;
pub use crate::proxy::performance_profile1::PerformanceProfile1Proxy;
pub use crate::proxy::storage1::Storage1Proxy;
pub use crate::proxy::tdp_limit1::TdpLimit1Proxy;
pub use crate::proxy::update_bios1::UpdateBios1Proxy;

View file

@ -0,0 +1,35 @@
//! # D-Bus interface proxy for: `com.steampowered.SteamOSManager1.PerformanceProfile1`
//!
//! This code was generated by `zbus-xmlgen` `5.1.0` from D-Bus introspection data.
//! Source: `com.steampowered.SteamOSManager1.xml`.
//!
//! You may prefer to adapt it, instead of using it verbatim.
//!
//! More information can be found in the [Writing a client proxy] section of the zbus
//! documentation.
//!
//!
//! [Writing a client proxy]: https://dbus2.github.io/zbus/client.html
//! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces,
use zbus::proxy;
#[proxy(
interface = "com.steampowered.SteamOSManager1.PerformanceProfile1",
default_service = "com.steampowered.SteamOSManager1",
default_path = "/com/steampowered/SteamOSManager1",
assume_defaults = true
)]
pub trait PerformanceProfile1 {
/// AvailablePerformanceProfiles property
#[zbus(property)]
fn available_performance_profiles(&self) -> zbus::Result<Vec<String>>;
/// PerformanceProfile property
#[zbus(property)]
fn performance_profile(&self) -> zbus::Result<String>;
#[zbus(property)]
fn set_performance_profile(&self, value: &str) -> zbus::Result<()>;
/// SuggestedDefaultPerformanceProfile property
#[zbus(property)]
fn suggested_default_performance_profile(&self) -> zbus::Result<String>;
}