diff --git a/Cargo.lock b/Cargo.lock index f1927da..eb8644e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -633,6 +633,17 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" +[[package]] +name = "dbus" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bb21987b9fb1613058ba3843121dd18b163b254d8a6e797e144cbac14d96d1b" +dependencies = [ + "libc", + "libdbus-sys", + "winapi", +] + [[package]] name = "debug-helper" version = "0.3.13" @@ -846,6 +857,16 @@ dependencies = [ "num-traits", ] +[[package]] +name = "framework-inputmodule-dbus-monitor" +version = "0.0.1" +dependencies = [ + "clap", + "dbus", + "inputmodule-control", + "libdbus-sys", +] + [[package]] name = "fuchsia-cprng" version = "0.1.1" @@ -1154,6 +1175,16 @@ version = "0.2.141" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5" +[[package]] +name = "libdbus-sys" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06085512b750d640299b79be4bad3d2fa90a9c00b1fd9e1b46364f66f0485c72" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "libloading" version = "0.7.4" diff --git a/Cargo.toml b/Cargo.toml index ec5e8cb..fe07178 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ members = [ "ledmatrix", "fl16-inputmodules", "inputmodule-control", + "dbus-monitor" ] # Don't build all of them by default. # Because that'll lead to all features enabled in `fl16-inputmodules` and it diff --git a/dbus-monitor/Cargo.toml b/dbus-monitor/Cargo.toml new file mode 100644 index 0000000..4849a80 --- /dev/null +++ b/dbus-monitor/Cargo.toml @@ -0,0 +1,16 @@ +[package] +edition = "2021" +name = "framework-inputmodule-dbus-monitor" +version = "0.0.1" + +[dependencies] +dbus = { version = "0.9.7", features = ["vendored"] } +clap = { version = "4.0", features = ["derive"] } + +[dependencies.libdbus-sys] +default-features = false +features = ["vendored"] +version = "0.2.5" + +[dependencies.inputmodule-control] +path = "../inputmodule-control" \ No newline at end of file diff --git a/dbus-monitor/release/framework-inputmodule-dbus-monitor-0.0.1.deb b/dbus-monitor/release/framework-inputmodule-dbus-monitor-0.0.1.deb new file mode 100644 index 0000000..d6c25b0 Binary files /dev/null and b/dbus-monitor/release/framework-inputmodule-dbus-monitor-0.0.1.deb differ diff --git a/dbus-monitor/release/framework-inputmodule-dbus-monitor.service b/dbus-monitor/release/framework-inputmodule-dbus-monitor.service new file mode 100644 index 0000000..f7660b2 --- /dev/null +++ b/dbus-monitor/release/framework-inputmodule-dbus-monitor.service @@ -0,0 +1,8 @@ +[Unit] +Description=framework-inputmodule-dbus-monitor monitors dbus for messages and makes input modules react to them + +[Service] +ExecStart=/usr/bin/framework-inputmodule-dbus-monitor + +[Install] +WantedBy=graphical.target \ No newline at end of file diff --git a/dbus-monitor/release/package/DEBIAN/control b/dbus-monitor/release/package/DEBIAN/control new file mode 100644 index 0000000..9f94742 --- /dev/null +++ b/dbus-monitor/release/package/DEBIAN/control @@ -0,0 +1,5 @@ +Package: framework-inputmodule-dbus-monitor +Version: 0.0.1 +Maintainer: Zach Feldman +Architecture: all +Description: Monitors dbus for messages and makes input modules react to them \ No newline at end of file diff --git a/dbus-monitor/release/package/usr/bin/framework-inputmodule-dbus-monitor b/dbus-monitor/release/package/usr/bin/framework-inputmodule-dbus-monitor new file mode 100755 index 0000000..741b5f5 Binary files /dev/null and b/dbus-monitor/release/package/usr/bin/framework-inputmodule-dbus-monitor differ diff --git a/dbus-monitor/release/package/usr/lib/systemd/user/framework-inputmodule-dbus-monitor.service b/dbus-monitor/release/package/usr/lib/systemd/user/framework-inputmodule-dbus-monitor.service new file mode 100644 index 0000000..f7660b2 --- /dev/null +++ b/dbus-monitor/release/package/usr/lib/systemd/user/framework-inputmodule-dbus-monitor.service @@ -0,0 +1,8 @@ +[Unit] +Description=framework-inputmodule-dbus-monitor monitors dbus for messages and makes input modules react to them + +[Service] +ExecStart=/usr/bin/framework-inputmodule-dbus-monitor + +[Install] +WantedBy=graphical.target \ No newline at end of file diff --git a/dbus-monitor/release/release.sh b/dbus-monitor/release/release.sh new file mode 100755 index 0000000..d85b44e --- /dev/null +++ b/dbus-monitor/release/release.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +set -euxo pipefail + +: 'Checking for fpm Ruby gem, installing if not present' +installed=`gem list -i fpm` || true + +if [ $installed = 'false' ]; then + gem install fpm +fi + +: 'Running the build' +cargo build + +cp -f target/debug/framework-inputmodule-dbus-monitor release/package/usr/bin/ + +: "Packaging" +fpm \ + -s dir -t deb \ + -p release/framework-inputmodule-dbus-monitor-0.0.1.deb \ + --name framework-inputmodule-dbus-monitor\ + --version 0.0.1 \ + --architecture all \ + --description "framework-inputmodule-dbus-monitor monitors dbus for messages and makes input modules react to them" \ + --url "https://frame.work" \ + --maintainer "Framework " \ + --deb-no-default-config-files \ + release/package/usr/bin/framework-inputmodule-dbus-monitor=/usr/bin/framework-inputmodule-dbus-monitor \ + release/package/usr/lib/systemd/user/framework-inputmodule-dbus-monitor.service=/lib/systemd/user/framework-inputmodule-dbus-monitor.service \ + release/package/DEBIAN/control + +: 'Packaging successful, install with "sudo dpkg -i .deb"' \ No newline at end of file diff --git a/dbus-monitor/src/dbus_monitor.rs b/dbus-monitor/src/dbus_monitor.rs new file mode 100644 index 0000000..811cb1e --- /dev/null +++ b/dbus-monitor/src/dbus_monitor.rs @@ -0,0 +1,120 @@ +// Mostly taken from https://github.com/diwic/dbus-rs/blob/366a6dca3d20745f5dcfa006b1b1311c376d420e/dbus/examples/monitor.rs + +// This programs implements the equivalent of running the "dbus-monitor" tool +// modified to only search for messages in the org.freedesktop.Notifications interface +use dbus::blocking::Connection; +use dbus::channel::MatchingReceiver; +use dbus::message::MatchRule; + +use dbus::Message; +use dbus::MessageType; + +use std::process::Command; +use std::time::Duration; + +use crate::utils; + +use inputmodule_control::inputmodule::find_serialdevs; +use inputmodule_control::commands::ClapCli; +use inputmodule_control::inputmodule::{serial_commands}; +use clap::{Parser, Subcommand}; + +fn handle_message(msg: &Message) { + println!("Got message from DBus: {:?}", msg); + + run_inputmodule_command(vec!["led-matrix", "--pattern", "all-on", "--blinking"]); + // Can't seem to get to this second command + run_inputmodule_command(vec!["led-matrix", "--brightness", "0"]); + + println!("Message handled"); +} + +pub fn run_inputmodule_command(args: Vec<&str>){ + let bin_placeholder = vec!["bin-placeholder"]; + let full_args = [&bin_placeholder[..], &args[..]].concat(); + let args = ClapCli::parse_from(full_args); + + serial_commands(&args); +} + +pub fn run_dbus_monitor() { + // First open up a connection to the desired bus. + let conn = Connection::new_session().expect("D-Bus connection failed"); + println!("Connection to DBus session monitor opened"); + + // Second create a rule to match messages we want to receive; in this example we add no + // further requirements, so all messages will match + let rule = MatchRule::new() + .with_type(MessageType::MethodCall) + .with_interface("org.freedesktop.Notifications") + .with_member("Notify"); + + // Try matching using new scheme + let proxy = conn.with_proxy( + "org.freedesktop.DBus", + "/org/freedesktop/DBus", + Duration::from_millis(5000), + ); + let result: Result<(), dbus::Error> = proxy.method_call( + "org.freedesktop.DBus.Monitoring", + "BecomeMonitor", + (vec![rule.match_str()], 0u32), + ); + println!("Monitoring DBus channel..."); + + match result { + // BecomeMonitor was successful, start listening for messages + Ok(_) => { + conn.start_receive( + rule, + Box::new(|msg, _| { + println!("Start listening"); + handle_message(&msg); + true + }), + ); + } + // BecomeMonitor failed, fallback to using the old scheme + Err(e) => { + eprintln!( + "Failed to BecomeMonitor: '{}', falling back to eavesdrop", + e + ); + + // First, we'll try "eavesdrop", which as the name implies lets us receive + // *all* messages, not just ours. + let rule_with_eavesdrop = { + let mut rule = rule.clone(); + rule.eavesdrop = true; + rule + }; + + let result = conn.add_match(rule_with_eavesdrop, |_: (), _, msg| { + handle_message(&msg); + true + }); + + match result { + Ok(_) => { + // success, we're now listening + } + // This can sometimes fail, for example when listening to the system bus as a non-root user. + // So, just like `dbus-monitor`, we attempt to fallback without `eavesdrop=true`: + Err(e) => { + eprintln!("Failed to eavesdrop: '{}', trying without it", e); + conn.add_match(rule, |_: (), _, msg| { + handle_message(&msg); + true + }) + .expect("add_match failed"); + } + } + } + } + + // Loop and print out all messages received (using handle_message()) as they come. + // Some can be quite large, e.g. if they contain embedded images.. + loop { + conn.process(Duration::from_millis(1000)).unwrap(); + } +} diff --git a/dbus-monitor/src/main.rs b/dbus-monitor/src/main.rs new file mode 100644 index 0000000..ddcfa2a --- /dev/null +++ b/dbus-monitor/src/main.rs @@ -0,0 +1,6 @@ +mod dbus_monitor; +mod utils; + +fn main() { + dbus_monitor::run_dbus_monitor(); +} diff --git a/dbus-monitor/src/utils.rs b/dbus-monitor/src/utils.rs new file mode 100644 index 0000000..93d4914 --- /dev/null +++ b/dbus-monitor/src/utils.rs @@ -0,0 +1,27 @@ +use std::process::Child; +use std::process::Command; +use std::time::Duration; +use std::time::Instant; + +pub fn run_command_with_timeout( + command: &str, + timeout_seconds: u64, +) -> Result> { + let mut child_process = Command::new("bash").arg("-c").arg(command).spawn()?; + + let start_time = Instant::now(); + while start_time.elapsed() < Duration::from_secs(timeout_seconds) { + if let Some(exit_status) = child_process.try_wait()? { + println!( + "Command finished before the specified duration. Exit status: {:?}", + exit_status + ); + return Ok(child_process); + } + } + + child_process.kill()?; // Attempt to kill the process + + println!("Command terminated after {} seconds", timeout_seconds); + Ok(child_process) +} diff --git a/inputmodule-control/src/commands.rs b/inputmodule-control/src/commands.rs new file mode 100644 index 0000000..fa122aa --- /dev/null +++ b/inputmodule-control/src/commands.rs @@ -0,0 +1,50 @@ +#![allow(clippy::needless_range_loop)] +#![allow(clippy::single_match)] + +use crate::inputmodule::{B1_LCD_PID, LED_MATRIX_PID}; +use crate::b1display::B1DisplaySubcommand; +use crate::c1minimal::C1MinimalSubcommand; +use crate::ledmatrix::LedMatrixSubcommand; + +use clap::{Parser, Subcommand}; + +#[derive(Subcommand, Debug)] +pub enum Commands { + LedMatrix(LedMatrixSubcommand), + B1Display(B1DisplaySubcommand), + C1Minimal(C1MinimalSubcommand), +} + +impl Commands { + pub fn to_pid(&self) -> u16 { + match self { + Self::LedMatrix(_) => LED_MATRIX_PID, + Self::B1Display(_) => B1_LCD_PID, + Self::C1Minimal(_) => 0x22, + } + } +} + +/// RAW HID and VIA commandline for QMK devices +#[derive(Parser, Debug)] +#[command(version, arg_required_else_help = true)] +pub struct ClapCli { + #[command(subcommand)] + pub command: Option, + + /// List connected HID devices + #[arg(short, long)] + pub list: bool, + + /// Verbose outputs to the console + #[arg(short, long)] + pub verbose: bool, + + /// Serial device, like /dev/ttyACM0 or COM0 + #[arg(long)] + pub serial_dev: Option, + + /// Retry connecting to the device until it works + #[arg(long)] + pub wait_for_device: bool, +} diff --git a/inputmodule-control/src/inputmodule.rs b/inputmodule-control/src/inputmodule.rs index 2f26244..2513293 100644 --- a/inputmodule-control/src/inputmodule.rs +++ b/inputmodule-control/src/inputmodule.rs @@ -12,6 +12,7 @@ use crate::b1display::{B1Pattern, Fps, PowerMode}; use crate::c1minimal::Color; use crate::font::{convert_font, convert_symbol}; use crate::ledmatrix::{Game, GameOfLifeStartParam, Pattern}; +use crate::commands::ClapCli; const FWK_MAGIC: &[u8] = &[0x32, 0xAC]; pub const FRAMEWORK_VID: u16 = 0x32AC; @@ -97,7 +98,7 @@ fn match_serialdevs( } } -pub fn find_serialdevs(args: &crate::ClapCli, wait_for_device: bool) -> (Vec, bool) { +pub fn find_serialdevs(args: &ClapCli, wait_for_device: bool) -> (Vec, bool) { let mut serialdevs: Vec; let mut waited = false; loop { @@ -149,7 +150,7 @@ pub fn find_serialdevs(args: &crate::ClapCli, wait_for_device: bool) -> (Vec, bool) = find_serialdevs(args, args.wait_for_device); if serialdevs.is_empty() { println!("Failed to find serial devivce. Please manually specify with --serial-dev"); @@ -162,7 +163,7 @@ pub fn serial_commands(args: &crate::ClapCli) { match &args.command { // TODO: Handle generic commands without code deduplication - Some(crate::Commands::LedMatrix(ledmatrix_args)) => { + Some(crate::commands::Commands::LedMatrix(ledmatrix_args)) => { for serialdev in &serialdevs { if args.verbose { println!("Selected serialdev: {:?}", serialdev); @@ -253,7 +254,7 @@ pub fn serial_commands(args: &crate::ClapCli) { clock_cmd(&serialdevs); } } - Some(crate::Commands::B1Display(b1display_args)) => { + Some(crate::commands::Commands::B1Display(b1display_args)) => { for serialdev in &serialdevs { if args.verbose { println!("Selected serialdev: {:?}", serialdev); @@ -303,7 +304,7 @@ pub fn serial_commands(args: &crate::ClapCli) { } } } - Some(crate::Commands::C1Minimal(c1minimal_args)) => { + Some(crate::commands::Commands::C1Minimal(c1minimal_args)) => { for serialdev in &serialdevs { if args.verbose { println!("Selected serialdev: {:?}", serialdev); diff --git a/inputmodule-control/src/lib.rs b/inputmodule-control/src/lib.rs new file mode 100644 index 0000000..a421c53 --- /dev/null +++ b/inputmodule-control/src/lib.rs @@ -0,0 +1,9 @@ +#![allow(clippy::needless_range_loop)] + +mod b1display; +mod c1minimal; +mod font; +pub mod inputmodule; +mod ledmatrix; +pub mod commands; + diff --git a/inputmodule-control/src/main.rs b/inputmodule-control/src/main.rs index 8348ad7..ba2aee9 100644 --- a/inputmodule-control/src/main.rs +++ b/inputmodule-control/src/main.rs @@ -5,60 +5,21 @@ mod c1minimal; mod font; mod inputmodule; mod ledmatrix; +mod commands; -use clap::{Parser, Subcommand}; +use clap::{Parser}; use inputmodule::find_serialdevs; -use crate::b1display::B1DisplaySubcommand; -use crate::c1minimal::C1MinimalSubcommand; -use crate::inputmodule::{serial_commands, B1_LCD_PID, LED_MATRIX_PID}; -use crate::ledmatrix::LedMatrixSubcommand; +use crate::inputmodule::serial_commands; -#[derive(Subcommand, Debug)] -enum Commands { - LedMatrix(LedMatrixSubcommand), - B1Display(B1DisplaySubcommand), - C1Minimal(C1MinimalSubcommand), -} - -impl Commands { - pub fn to_pid(&self) -> u16 { - match self { - Self::LedMatrix(_) => LED_MATRIX_PID, - Self::B1Display(_) => B1_LCD_PID, - Self::C1Minimal(_) => 0x22, - } - } -} - -/// RAW HID and VIA commandline for QMK devices -#[derive(Parser, Debug)] -#[command(version, arg_required_else_help = true)] -pub struct ClapCli { - #[command(subcommand)] - command: Option, - - /// List connected HID devices - #[arg(short, long)] - list: bool, - - /// Verbose outputs to the console - #[arg(short, long)] - verbose: bool, - - /// Serial device, like /dev/ttyACM0 or COM0 - #[arg(long)] - pub serial_dev: Option, - - /// Retry connecting to the device until it works - #[arg(long)] - wait_for_device: bool, -} +use crate::commands::ClapCli; fn main() { let args: Vec = std::env::args().collect(); + println!("Args were {:?}", args); let args = ClapCli::parse_from(args); + println!("Args are {:?}", args); match args.command { Some(_) => serial_commands(&args), None => {