From e00bc1ada34eef3735fd990e38e2631a18aab5b6 Mon Sep 17 00:00:00 2001 From: luytan Date: Sun, 3 May 2026 18:25:35 +0200 Subject: [PATCH 01/10] feat: started implementation of a upower battery listener for auto_switching mode --- Cargo.lock | 12 ++++++++ Cargo.toml | 1 + crates/cardwire-daemon/Cargo.toml | 2 ++ crates/cardwire-daemon/src/daemon.rs | 2 ++ crates/cardwire-daemon/src/listeners.rs | 37 +++++++++++++++++++++++++ 5 files changed, 54 insertions(+) create mode 100644 crates/cardwire-daemon/src/listeners.rs diff --git a/Cargo.lock b/Cargo.lock index 6e39716..f8aa1d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -339,6 +339,7 @@ dependencies = [ "serde_json", "thiserror 2.0.18", "tokio", + "tokio-stream", "toml", "zbus", ] @@ -1430,6 +1431,17 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "toml" version = "1.1.2+spec-1.1.0" diff --git a/Cargo.toml b/Cargo.toml index 34142f1..20d93eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ env_logger = "0.11" toml = "1.0.3" aya = "0.13.1" serde_json = "1.0.149" +tokio-stream = "0.1.18" [profile.dev] panic = "abort" diff --git a/crates/cardwire-daemon/Cargo.toml b/crates/cardwire-daemon/Cargo.toml index 91b5d76..419ef0d 100644 --- a/crates/cardwire-daemon/Cargo.toml +++ b/crates/cardwire-daemon/Cargo.toml @@ -20,6 +20,8 @@ anyhow.workspace = true env_logger.workspace = true toml.workspace = true serde_json.workspace = true +tokio-stream.workspace = true + [[bin]] name = "cardwired" path = "src/daemon.rs" diff --git a/crates/cardwire-daemon/src/daemon.rs b/crates/cardwire-daemon/src/daemon.rs index 024fdf7..b9fbe8d 100644 --- a/crates/cardwire-daemon/src/daemon.rs +++ b/crates/cardwire-daemon/src/daemon.rs @@ -1,6 +1,7 @@ //! entry point of cardwired mod config; mod dbus; +mod listeners; mod models; use crate::models::Daemon; @@ -27,6 +28,7 @@ async fn main() -> Result<()> { .serve_at("/com/github/opengamingcollective/cardwire", daemon)? .build() .await?; + tokio::task::spawn(listeners::watch_battery_status()); info!("Daemon started"); pending::<()>().await; Ok(()) diff --git a/crates/cardwire-daemon/src/listeners.rs b/crates/cardwire-daemon/src/listeners.rs new file mode 100644 index 0000000..c32df2a --- /dev/null +++ b/crates/cardwire-daemon/src/listeners.rs @@ -0,0 +1,37 @@ +//! Used to listen to other dbus interface, mainly for auto battery switch and display detection + +use log::info; +use tokio_stream::StreamExt; +use zbus::{Connection, proxy}; + +#[proxy( + interface = "org.freedesktop.UPower", + default_service = "org.freedesktop.UPower", + default_path = "/org/freedesktop/UPower" +)] +trait UPower { + #[zbus(signal)] + fn on_battery(&self, state: bool) -> zbus::Result<()>; +} + +#[proxy( + interface = "com.github.opengamingcollective.cardwire", + default_service = "com.github.opengamingcollective.cardwire", + default_path = "/com/github/opengamingcollective/cardwire" +)] +trait Cardwire {} + +pub async fn watch_battery_status() -> zbus::Result<()> { + let connection = Connection::system().await?; + let upower_proxy = UPowerProxy::new(&connection).await?; + + let cardwire = CardwireProxy::new(&connection).await?; + info!("Started listening to on_battery signal"); + let mut on_battery_stream = upower_proxy.receive_on_battery().await?; + + while let Some(msg) = on_battery_stream.next().await { + println!("{:?}", msg); + } + + Ok(()) +} From 787cc69a06d3b9d5a266aad80aba86893c374ce7 Mon Sep 17 00:00:00 2001 From: luytan Date: Sun, 3 May 2026 18:30:49 +0200 Subject: [PATCH 02/10] fix: replace println by info! --- crates/cardwire-daemon/src/listeners.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/cardwire-daemon/src/listeners.rs b/crates/cardwire-daemon/src/listeners.rs index c32df2a..416646e 100644 --- a/crates/cardwire-daemon/src/listeners.rs +++ b/crates/cardwire-daemon/src/listeners.rs @@ -30,7 +30,7 @@ pub async fn watch_battery_status() -> zbus::Result<()> { let mut on_battery_stream = upower_proxy.receive_on_battery().await?; while let Some(msg) = on_battery_stream.next().await { - println!("{:?}", msg); + info!("battery event: {:?}", msg) } Ok(()) From 9bf460bf0b2996d88ad453bec9ec89ca632dc15a Mon Sep 17 00:00:00 2001 From: luytan Date: Sun, 3 May 2026 18:39:15 +0200 Subject: [PATCH 03/10] fix: on_battery is a property not a signal --- crates/cardwire-daemon/src/listeners.rs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/crates/cardwire-daemon/src/listeners.rs b/crates/cardwire-daemon/src/listeners.rs index 416646e..0f08463 100644 --- a/crates/cardwire-daemon/src/listeners.rs +++ b/crates/cardwire-daemon/src/listeners.rs @@ -2,7 +2,7 @@ use log::info; use tokio_stream::StreamExt; -use zbus::{Connection, proxy}; +use zbus::{Connection, Result, proxy}; #[proxy( interface = "org.freedesktop.UPower", @@ -10,10 +10,9 @@ use zbus::{Connection, proxy}; default_path = "/org/freedesktop/UPower" )] trait UPower { - #[zbus(signal)] - fn on_battery(&self, state: bool) -> zbus::Result<()>; + #[zbus(property)] + fn on_battery(&self) -> Result; } - #[proxy( interface = "com.github.opengamingcollective.cardwire", default_service = "com.github.opengamingcollective.cardwire", @@ -26,11 +25,11 @@ pub async fn watch_battery_status() -> zbus::Result<()> { let upower_proxy = UPowerProxy::new(&connection).await?; let cardwire = CardwireProxy::new(&connection).await?; - info!("Started listening to on_battery signal"); - let mut on_battery_stream = upower_proxy.receive_on_battery().await?; + info!("Started listening to on_battery property"); + let mut battery_stream = upower_proxy.receive_on_battery_changed().await; - while let Some(msg) = on_battery_stream.next().await { - info!("battery event: {:?}", msg) + while let Some(msg) = battery_stream.next().await { + info!("battery event detected: {:?}", msg.get().await) } Ok(()) From 70d9a69cc2a1ad328ce47db3092b30ee8fa9ef5f Mon Sep 17 00:00:00 2001 From: luytan Date: Sun, 3 May 2026 18:42:24 +0200 Subject: [PATCH 04/10] fix: on_battery returns a bool not a string --- crates/cardwire-daemon/src/listeners.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/cardwire-daemon/src/listeners.rs b/crates/cardwire-daemon/src/listeners.rs index 0f08463..73ccd83 100644 --- a/crates/cardwire-daemon/src/listeners.rs +++ b/crates/cardwire-daemon/src/listeners.rs @@ -11,7 +11,7 @@ use zbus::{Connection, Result, proxy}; )] trait UPower { #[zbus(property)] - fn on_battery(&self) -> Result; + fn on_battery(&self) -> Result; } #[proxy( interface = "com.github.opengamingcollective.cardwire", From 2590d6aea86aa40e33a6adafd9855f4e76c612e1 Mon Sep 17 00:00:00 2001 From: luytan Date: Sun, 3 May 2026 18:56:38 +0200 Subject: [PATCH 05/10] chore: move battery listen to model --- crates/cardwire-daemon/src/daemon.rs | 1 - crates/cardwire-daemon/src/models.rs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/cardwire-daemon/src/daemon.rs b/crates/cardwire-daemon/src/daemon.rs index b9fbe8d..fa9a767 100644 --- a/crates/cardwire-daemon/src/daemon.rs +++ b/crates/cardwire-daemon/src/daemon.rs @@ -28,7 +28,6 @@ async fn main() -> Result<()> { .serve_at("/com/github/opengamingcollective/cardwire", daemon)? .build() .await?; - tokio::task::spawn(listeners::watch_battery_status()); info!("Daemon started"); pending::<()>().await; Ok(()) diff --git a/crates/cardwire-daemon/src/models.rs b/crates/cardwire-daemon/src/models.rs index e885693..1820f4c 100644 --- a/crates/cardwire-daemon/src/models.rs +++ b/crates/cardwire-daemon/src/models.rs @@ -146,7 +146,7 @@ impl Daemon { }; self.set_mode(mode_to_apply as u32).await?; - + tokio::task::spawn(crate::listeners::watch_battery_status()); Ok(()) } } From 21243aba037b503d70de11683976be70d9a46208 Mon Sep 17 00:00:00 2001 From: luytan Date: Sun, 3 May 2026 19:19:32 +0200 Subject: [PATCH 06/10] feat: add auto_battery_switch to config, and default configuration --- crates/cardwire-daemon/src/config.rs | 23 ++++++++++++++++------- crates/cardwire-daemon/src/models.rs | 7 +++++-- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/crates/cardwire-daemon/src/config.rs b/crates/cardwire-daemon/src/config.rs index 30444e0..8a6d508 100644 --- a/crates/cardwire-daemon/src/config.rs +++ b/crates/cardwire-daemon/src/config.rs @@ -16,13 +16,22 @@ enum FileKind { ModeState, PciState, } -// TODO: Handle fs error for tomorrow -#[derive(Deserialize, Serialize)] +#[derive(Deserialize, Serialize, Debug)] +#[serde(default)] pub struct CardwireConfig { auto_apply_gpu_state: bool, block_nvidia_vulkan: bool, + battery_auto_switch: bool, +} +impl Default for CardwireConfig { + fn default() -> Self { + CardwireConfig { + auto_apply_gpu_state: true, + block_nvidia_vulkan: false, + battery_auto_switch: false, + } + } } - impl CardwireConfig { /// Read TOML config file and return it's settings as a struct // TODO: Error handling on std::fs @@ -51,6 +60,9 @@ impl CardwireConfig { pub fn auto_apply_gpu_state(&self) -> bool { self.auto_apply_gpu_state } + pub fn battery_auto_switch(&self) -> bool { + self.battery_auto_switch + } } // This is the easiest way i found to have a good looking json, might change later @@ -186,10 +198,7 @@ fn create_default_file(kind: FileKind) -> anyhow::Result<()> { .context("could not create default folder for cardwire.toml")?; // Default config for cardwire // TODO: Move to default trait? - let default_config = toml::to_string_pretty(&CardwireConfig { - auto_apply_gpu_state: true, - block_nvidia_vulkan: false, - })?; + let default_config = toml::to_string_pretty(&CardwireConfig::default())?; // write fs::write(format!("{}/cardwire.toml", CONFIG_PATH), default_config) } diff --git a/crates/cardwire-daemon/src/models.rs b/crates/cardwire-daemon/src/models.rs index 1820f4c..2e2c8d6 100644 --- a/crates/cardwire-daemon/src/models.rs +++ b/crates/cardwire-daemon/src/models.rs @@ -144,9 +144,12 @@ impl Daemon { Modes::Hybrid => 1, Modes::Manual => 2, }; - self.set_mode(mode_to_apply as u32).await?; - tokio::task::spawn(crate::listeners::watch_battery_status()); + // get config lock again + let config = self.state.config.read().await; + if config.battery_auto_switch() { + tokio::task::spawn(crate::listeners::watch_battery_status()); + } Ok(()) } } From dfb866d615922451e63f2eeac59fbf9f0bb8b253 Mon Sep 17 00:00:00 2001 From: luytan Date: Sun, 3 May 2026 19:25:10 +0200 Subject: [PATCH 07/10] feat: add mode switching on battery change --- crates/cardwire-daemon/src/listeners.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/crates/cardwire-daemon/src/listeners.rs b/crates/cardwire-daemon/src/listeners.rs index 73ccd83..d8128bb 100644 --- a/crates/cardwire-daemon/src/listeners.rs +++ b/crates/cardwire-daemon/src/listeners.rs @@ -18,7 +18,9 @@ trait UPower { default_service = "com.github.opengamingcollective.cardwire", default_path = "/com/github/opengamingcollective/cardwire" )] -trait Cardwire {} +trait Cardwire { + fn set_mode(&self, mode: u32) -> Result<()>; +} pub async fn watch_battery_status() -> zbus::Result<()> { let connection = Connection::system().await?; @@ -29,7 +31,13 @@ pub async fn watch_battery_status() -> zbus::Result<()> { let mut battery_stream = upower_proxy.receive_on_battery_changed().await; while let Some(msg) = battery_stream.next().await { - info!("battery event detected: {:?}", msg.get().await) + if let Ok(state) = msg.get().await { + info!("battery event detected: {:?}", state); + match state { + true => cardwire.set_mode(0).await?, + false => cardwire.set_mode(1).await?, + }; + } } Ok(()) From 36d26901b5651047ac6bb04ca5c3b74321a4fae0 Mon Sep 17 00:00:00 2001 From: luytan Date: Sun, 3 May 2026 19:30:38 +0200 Subject: [PATCH 08/10] fix: set set_mode to zbus property --- crates/cardwire-daemon/src/listeners.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/cardwire-daemon/src/listeners.rs b/crates/cardwire-daemon/src/listeners.rs index d8128bb..daf05d1 100644 --- a/crates/cardwire-daemon/src/listeners.rs +++ b/crates/cardwire-daemon/src/listeners.rs @@ -19,6 +19,7 @@ trait UPower { default_path = "/com/github/opengamingcollective/cardwire" )] trait Cardwire { + #[zbus(property)] fn set_mode(&self, mode: u32) -> Result<()>; } From d220545a15040fbefe1823b9277a6cefcc33aa80 Mon Sep 17 00:00:00 2001 From: luytan Date: Sun, 3 May 2026 19:47:21 +0200 Subject: [PATCH 09/10] ci: rename to new error message --- nix/ci-2gpu.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/ci-2gpu.nix b/nix/ci-2gpu.nix index 79c557c..c93b3a1 100644 --- a/nix/ci-2gpu.nix +++ b/nix/ci-2gpu.nix @@ -71,6 +71,6 @@ t.assertIn("Hybrid", machine.succeed("cat /var/lib/cardwire/mode.json"), "mode.json didnt get saved") with subtest("Try to block default gpu"): - t.assertIn("cannot be blocked", machine.succeed("cardwire gpu 0 --block 2>&1"), "Default gpu got blocked") + t.assertIn("Per GPU block is only available on manual mode", machine.succeed("cardwire gpu 0 --block 2>&1"), "Default gpu got blocked") ''; } From 27d4bac52949dd9153897c3bd19acfc5831cf2f2 Mon Sep 17 00:00:00 2001 From: luytan Date: Sun, 3 May 2026 19:54:07 +0200 Subject: [PATCH 10/10] feat: add upower to packaging --- nix/default.nix | 1 + packages/arch-linux/cardwire-PKGBUILD | 2 +- packages/fedora/cardwire.spec | 3 +++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/nix/default.nix b/nix/default.nix index 9eb07a5..006dc46 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -27,6 +27,7 @@ in ]; runtimeDeps = [ pkgs.hwdata + pkgs.upower ]; doCheck = false; doInstallCheck = true; diff --git a/packages/arch-linux/cardwire-PKGBUILD b/packages/arch-linux/cardwire-PKGBUILD index 64089a3..e6348a8 100644 --- a/packages/arch-linux/cardwire-PKGBUILD +++ b/packages/arch-linux/cardwire-PKGBUILD @@ -9,7 +9,7 @@ pkgdesc='GPU manager for Linux using eBPF LSM hooks' arch=('x86_64') url='https://github.com/OpenGamingCollective/cardwire' license=('GPL3') -depends=('hwdata' 'dbus' 'systemd') +depends=('hwdata' 'dbus' 'systemd' 'upower') makedepends=('libbpf' 'rust' 'clang') source=("https://github.com/OpenGamingCollective/cardwire/archive/refs/tags/v$pkgver.tar.gz") sha256sums=('37882d4d0d431c3ff48e24bd47cea03b7847080623254d8bdb4af9846134d700') diff --git a/packages/fedora/cardwire.spec b/packages/fedora/cardwire.spec index 3f869cd..17d9da6 100644 --- a/packages/fedora/cardwire.spec +++ b/packages/fedora/cardwire.spec @@ -13,6 +13,9 @@ BuildRequires: libbpf-devel BuildRequires: make BuildRequires: systemd-rpm-macros +Requires: hwdata +Requires: upower + %description Cardwire is a GPU manager for Linux that uses eBPF LSM hooks to block or unblock access to GPU device nodes.