Add screenreader support to steamos-manager.

Add ScreenReader1 interface to xml to enable/disable using screen
reader.
Implements getting and setting pitch, rate, volume, enabled.
Restarts orca when any of the above properties are changed.
Load values from orca user-settings.conf
Use systemd unit to start/stop/restart orca.
This commit is contained in:
Jeremy Whiting 2025-05-19 10:53:33 -06:00 committed by Jeremy Whiting
parent b926dbd50b
commit 4fd9ccdd2e
6 changed files with 432 additions and 0 deletions

25
Cargo.lock generated
View file

@ -437,6 +437,12 @@ dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "lazy_static"
version = "1.5.0"
@ -725,6 +731,12 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
[[package]]
name = "ryu"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "serde"
version = "1.0.219"
@ -745,6 +757,18 @@ dependencies = [
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.140"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
]
[[package]]
name = "serde_repr"
version = "0.1.20"
@ -815,6 +839,7 @@ dependencies = [
"num_enum",
"regex",
"serde",
"serde_json",
"strum",
"tempfile",
"tokio",

View file

@ -19,6 +19,7 @@ nix = { version = "0.30", default-features = false, features = ["fs", "poll", "s
num_enum = "0.7"
regex = "1"
serde = { version = "1.0", default-features = false, features = ["derive"] }
serde_json = "1.0"
strum = { version = "0.27", features = ["derive"] }
tempfile = "3"
tokio = { version = "1", default-features = false, features = ["fs", "io-std", "io-util", "macros", "process", "rt-multi-thread", "signal", "sync"] }

View file

@ -298,6 +298,40 @@
</interface>
<!--
com.steampowered.SteamOSManager1.ScreenReader1
@short_description: Optional interface for managing a screen reader.
-->
<interface name="com.steampowered.SteamOSManager1.ScreenReader1">
<!--
Enabled
True if screen reader is enabled, false otherwise.
-->
<property name="Enabled" type="b" access="readwrite"/>
<!--
Rate
The rate of speech output. Valid values are 0 for slowest to 100 for fastest.
-->
<property name="Rate" type="d" access="readwrite"/>
<!--
Pitch
The pitch for speech output. Valid values are 0.0 for lowest, and 10.0 for highest.
-->
<property name="Pitch" type="d" access="readwrite"/>
<!--
Volume
The volume for speech output. Valid values ar 0.0 for off, 10.0 for highest.
-->
<property name="Volume" type="d" access="readwrite"/>
</interface>
<!--
com.steampowered.SteamOSManager1.Storage1
@short_description: Optional interface for managing storage devices

View file

@ -25,6 +25,7 @@ mod job;
mod manager;
mod platform;
mod process;
mod screenreader;
mod sls;
mod systemd;
mod udev;

View file

@ -31,6 +31,7 @@ use crate::power::{
get_gpu_clocks, get_gpu_clocks_range, get_gpu_performance_level, get_gpu_power_profile,
get_max_charge_level, get_platform_profile, tdp_limit_manager, TdpManagerCommand,
};
use crate::screenreader::OrcaManager;
use crate::wifi::{
get_wifi_backend, get_wifi_power_management_state, list_wifi_interfaces, WifiBackend,
};
@ -153,6 +154,10 @@ struct PerformanceProfile1 {
tdp_limit_manager: UnboundedSender<TdpManagerCommand>,
}
struct ScreenReader1 {
screen_reader: OrcaManager<'static>,
}
struct Storage1 {
proxy: Proxy<'static>,
job_manager: UnboundedSender<JobManagerCommand>,
@ -618,6 +623,80 @@ impl PerformanceProfile1 {
}
}
impl ScreenReader1 {
async fn new(connection: &Connection) -> Result<ScreenReader1> {
let screen_reader = OrcaManager::new(connection).await?;
Ok(ScreenReader1 { screen_reader })
}
}
#[interface(name = "com.steampowered.SteamOSManager1.ScreenReader1")]
impl ScreenReader1 {
#[zbus(property)]
async fn enabled(&self) -> fdo::Result<bool> {
match self.screen_reader.enabled().await {
Ok(enabled) => Ok(enabled),
Err(e) => Err(to_zbus_fdo_error(e)),
}
}
#[zbus(property)]
async fn set_enabled(&mut self, enabled: bool) -> fdo::Result<()> {
self.screen_reader
.set_enabled(enabled)
.await
.map_err(to_zbus_fdo_error)
}
#[zbus(property)]
async fn rate(&self) -> fdo::Result<f64> {
match self.screen_reader.rate().await {
Ok(rate) => Ok(rate),
Err(e) => Err(to_zbus_fdo_error(e)),
}
}
#[zbus(property)]
async fn set_rate(&mut self, rate: f64) -> fdo::Result<()> {
self.screen_reader
.set_rate(rate)
.await
.map_err(to_zbus_fdo_error)
}
#[zbus(property)]
async fn pitch(&self) -> fdo::Result<f64> {
match self.screen_reader.pitch().await {
Ok(pitch) => Ok(pitch),
Err(e) => Err(to_zbus_fdo_error(e)),
}
}
#[zbus(property)]
async fn set_pitch(&mut self, pitch: f64) -> fdo::Result<()> {
self.screen_reader
.set_pitch(pitch)
.await
.map_err(to_zbus_fdo_error)
}
#[zbus(property)]
async fn volume(&self) -> fdo::Result<f64> {
match self.screen_reader.volume().await {
Ok(volume) => Ok(volume),
Err(e) => Err(to_zbus_fdo_error(e)),
}
}
#[zbus(property)]
async fn set_volume(&mut self, volume: f64) -> fdo::Result<()> {
self.screen_reader
.set_volume(volume)
.await
.map_err(to_zbus_fdo_error)
}
}
#[interface(name = "com.steampowered.SteamOSManager1.Storage1")]
impl Storage1 {
async fn format_device(
@ -919,6 +998,7 @@ pub(crate) async fn create_interfaces(
proxy: proxy.clone(),
channel: daemon,
};
let screen_reader = ScreenReader1::new(&session).await?;
let wifi_debug = WifiDebug1 {
proxy: proxy.clone(),
};
@ -971,6 +1051,8 @@ pub(crate) async fn create_interfaces(
object_server.at(MANAGER_PATH, manager2).await?;
object_server.at(MANAGER_PATH, screen_reader).await?;
if steam_deck_variant().await.unwrap_or_default() == SteamDeckVariant::Galileo {
object_server.at(MANAGER_PATH, wifi_debug).await?;
}

289
src/screenreader.rs Normal file
View file

@ -0,0 +1,289 @@
/*
* Copyright © 2025 Collabora Ltd.
* Copyright © 2025 Valve Software
*
* SPDX-License-Identifier: MIT
*/
use anyhow::{anyhow, Result};
use std::fs::File;
use std::io::Read;
use std::io::Write;
//use std::process::{Child, Command};
use serde_json::{json, Value};
use std::process::Command;
use tracing::{info, warn};
use zbus::Connection;
use crate::systemd::SystemdUnit;
const ORCA_SETTINGS: &str = "/home/deck/.local/share/orca/user-settings.conf";
const PITCH_SETTING: &str = "average-pitch";
const RATE_SETTING: &str = "rate";
const VOLUME_SETTING: &str = "gain";
pub(crate) struct OrcaManager<'dbus> {
orca_unit: SystemdUnit<'dbus>,
rate: f64,
pitch: f64,
volume: f64,
enabled: bool,
}
impl<'dbus> OrcaManager<'dbus> {
pub async fn new(connection: &Connection) -> Result<OrcaManager<'dbus>> {
let mut manager = OrcaManager {
orca_unit: SystemdUnit::new(connection.clone(), "orca.service").await?,
rate: 1.0,
pitch: 1.0,
volume: 1.0,
enabled: true,
};
manager.load_values()?;
Ok(manager)
}
pub async fn enabled(&self) -> Result<bool> {
// Check if screen reader is enabled
Ok(self.enabled)
}
pub async fn set_enabled(&mut self, enable: bool) -> std::io::Result<()> {
// Set screen reader enabled based on value of enable
if enable {
// Enable screen reader gsettings
let _ = Command::new("gsettings")
.args([
"set",
"org.gnome.desktop.a11y.applications",
"screen-reader-enabled",
"true",
])
.spawn();
// Set orca enabled also
let _setting_result = self.set_orca_enabled(true);
let _result = self.restart_orca().await;
} else {
// Disable screen reader gsettings
let _ = Command::new("gsettings")
.args([
"set",
"org.gnome.desktop.a11y.applications",
"screen-reader-enabled",
"false",
])
.spawn();
// Set orca disabled also
let _setting_result = self.set_orca_enabled(false);
// Stop orca
let _result = self.stop_orca().await;
}
self.enabled = enable;
Ok(())
}
pub async fn pitch(&self) -> Result<f64> {
Ok(self.pitch)
}
pub async fn set_pitch(&mut self, pitch: f64) -> Result<()> {
info!("set_pitch called with {:?}", pitch);
let result = self.set_orca_option(PITCH_SETTING.to_owned(), pitch);
match result {
Ok(_) => {
self.pitch = pitch;
Ok(())
}
Err(_) => Err(anyhow!("Unable to set orca pitch value")),
}
}
pub async fn rate(&self) -> Result<f64> {
Ok(self.rate)
}
pub async fn set_rate(&mut self, rate: f64) -> Result<()> {
info!("set_rate called with {:?}", rate);
let result = self.set_orca_option(RATE_SETTING.to_owned(), rate);
match result {
Ok(_) => {
self.rate = rate;
Ok(())
}
Err(_) => Err(anyhow!("Unable to set orca rate")),
}
}
pub async fn volume(&self) -> Result<f64> {
Ok(self.volume)
}
pub async fn set_volume(&mut self, volume: f64) -> Result<()> {
info!("set_volume called with {:?}", volume);
let result = self.set_orca_option(VOLUME_SETTING.to_owned(), volume);
match result {
Ok(_) => {
self.volume = volume;
Ok(())
}
Err(_) => Err(anyhow!("Unable to set orca volume")),
}
}
fn set_orca_enabled(&mut self, enabled: bool) -> Result<()> {
// Change json file
let mut file = File::open(ORCA_SETTINGS)?;
let mut data = String::new();
file.read_to_string(&mut data)?;
let mut json: Value = serde_json::from_str(&data)?;
if let Some(general) = json.get_mut("general") {
if let Some(enable_speech) = general.get_mut("enableSpeech") {
*enable_speech = json!(&enabled);
} else {
warn!("No enabledSpeech value in general in orca settings");
}
} else {
warn!("No general section in orca settings");
}
data = serde_json::to_string_pretty(&json)?;
let mut out_file = File::create(ORCA_SETTINGS)?;
match out_file.write_all(&data.into_bytes()) {
Ok(_) => {
self.enabled = enabled;
Ok(())
}
Err(_) => Err(anyhow!("Unable to write orca settings file")),
}
}
fn load_values(&mut self) -> Result<()> {
info!("Loading orca values from user-settings.conf");
let mut file = File::open(ORCA_SETTINGS)?;
let mut data = String::new();
file.read_to_string(&mut data)?;
let json: Value = serde_json::from_str(&data)?;
if let Some(profiles) = json.get("profiles") {
if let Some(default_profile) = profiles.get("default") {
if let Some(voices) = default_profile.get("voices") {
if let Some(default_voice) = voices.get("default") {
if let Some(pitch) = default_voice.get(PITCH_SETTING.to_owned()) {
self.pitch = pitch
.as_f64()
.expect("Unable to convert orca pitch setting to float value");
} else {
warn!("Unable to load default pitch from orca user-settings.conf");
}
if let Some(rate) = default_voice.get(RATE_SETTING.to_owned()) {
self.rate = rate
.as_f64()
.expect("Unable to convert orca rate setting to float value");
} else {
warn!("Unable to load default voice rate from orca user-settings.conf");
}
if let Some(volume) = default_voice.get(VOLUME_SETTING.to_owned()) {
self.volume = volume
.as_f64()
.expect("Unable to convert orca volume value to float value");
} else {
warn!(
"Unable to load default voice volume from orca user-settings.conf"
);
}
} else {
warn!("Orca user-settings.conf missing default voice");
}
} else {
warn!("Orca user-settings.conf missing voices list");
}
} else {
warn!("Orca user-settings.conf missing default profile");
}
} else {
warn!("Orca user-settings.conf missing profiles");
}
info!(
"Done loading orca user-settings.conf, values: Rate: {:?}, Pitch: {:?}, Volume: {:?}",
self.rate, self.pitch, self.volume
);
Ok(())
}
fn set_orca_option(&self, option: String, value: f64) -> Result<()> {
// Verify option is one we know about
// Verify value is in range
// Change json file
let mut file = File::open(ORCA_SETTINGS)?;
let mut data = String::new();
file.read_to_string(&mut data)?;
let mut json: Value = serde_json::from_str(&data)?;
if let Some(profiles) = json.get_mut("profiles") {
if let Some(default_profile) = profiles.get_mut("default") {
if let Some(voices) = default_profile.get_mut("voices") {
if let Some(default_voice) = voices.get_mut("default") {
if let Some(mut_option) = default_voice.get_mut(&option) {
*mut_option = json!(value);
} else {
let object = default_voice.as_object_mut().ok_or(anyhow!(
"Unable to generate mutable object for adding {:?} with value {:?}",
&option,
value
))?;
object.insert(option, json!(value));
}
} else {
warn!(
"No default voice in voices list to set {:?} to {:?} in",
&option, value
);
}
} else {
warn!(
"No voices in default profile to set {:?} to {:?} in",
&option, value
);
}
} else {
warn!("No default profile to set {:?} to {:?} in", &option, value);
}
} else {
warn!("No profiles in orca user-settings.conf to modify");
}
data = serde_json::to_string_pretty(&json)?;
let mut out_file = File::create(ORCA_SETTINGS)?;
match out_file.write_all(&data.into_bytes()) {
Ok(_) => Ok(()),
Err(_) => Err(anyhow!("Unable to write orca settings file")),
}
}
async fn restart_orca(&self) -> Result<()> {
info!("Restarting orca...");
let _result = self.orca_unit.restart().await;
info!("Done restarting orca...");
Ok(())
}
async fn stop_orca(&self) -> Result<()> {
// Stop orca user unit
info!("Stopping orca...");
let _result = self.orca_unit.stop().await;
info!("Done stopping orca...");
Ok(())
}
}