feat!: wivrn pairing support

This commit is contained in:
GabMus 2024-11-27 18:47:54 +00:00
commit 4638ac1bf4
7 changed files with 1173 additions and 579 deletions

1360
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -30,3 +30,4 @@ ash = "0.38.0"
sha2 = "0.10.8" sha2 = "0.10.8"
tokio = { version = "1.39.3", features = ["process"] } tokio = { version = "1.39.3", features = ["process"] }
notify-rust = "4.11.3" notify-rust = "4.11.3"
zbus = { version = "5.1.1", features = ["tokio"] }

View file

@ -41,6 +41,7 @@ pub mod termcolor;
pub mod ui; pub mod ui;
pub mod util; pub mod util;
pub mod vulkaninfo; pub mod vulkaninfo;
pub mod wivrn_dbus;
pub mod xdg; pub mod xdg;
pub mod xr_devices; pub mod xr_devices;

View file

@ -47,6 +47,7 @@ use crate::{
}, },
util::file_utils::{setcap_cap_sys_nice_eip, setcap_cap_sys_nice_eip_cmd}, util::file_utils::{setcap_cap_sys_nice_eip, setcap_cap_sys_nice_eip_cmd},
vulkaninfo::VulkanInfo, vulkaninfo::VulkanInfo,
wivrn_dbus,
xr_devices::XRDevice, xr_devices::XRDevice,
}; };
use adw::{prelude::*, ResponseAppearance}; use adw::{prelude::*, ResponseAppearance};
@ -64,7 +65,7 @@ pub struct App {
app_win: adw::ApplicationWindow, app_win: adw::ApplicationWindow,
inhibit_id: Option<u32>, inhibit_id: Option<u32>,
main_view: Controller<MainView>, main_view: AsyncController<MainView>,
debug_view: Controller<DebugView>, debug_view: Controller<DebugView>,
split_view: Option<adw::NavigationSplitView>, split_view: Option<adw::NavigationSplitView>,
about_dialog: adw::AboutDialog, about_dialog: adw::AboutDialog,
@ -119,6 +120,7 @@ pub enum Msg {
HandleCommandLine(CmdLineOpts), HandleCommandLine(CmdLineOpts),
StartProber, StartProber,
OnProberExit(bool), OnProberExit(bool),
WivrnCheckPairMode,
NoOp, NoOp,
} }
@ -375,11 +377,11 @@ impl AsyncComponent for App {
Msg::OnAutostartExit(_) => self.autostart_worker = None, Msg::OnAutostartExit(_) => self.autostart_worker = None,
Msg::ClockTicking => { Msg::ClockTicking => {
self.main_view.sender().emit(MainViewMsg::ClockTicking); self.main_view.sender().emit(MainViewMsg::ClockTicking);
let should_poll_for_devices = self.xrservice_ready let xrservice_worker_is_alive = self
&& self
.xrservice_worker .xrservice_worker
.as_ref() .as_ref()
.is_some_and(JobWorker::is_alive); .is_some_and(JobWorker::is_alive);
let should_poll_for_devices = self.xrservice_ready && xrservice_worker_is_alive;
if should_poll_for_devices { if should_poll_for_devices {
if let Some(monado) = self.libmonado.as_ref() { if let Some(monado) = self.libmonado.as_ref() {
self.xr_devices = XRDevice::from_libmonado(monado); self.xr_devices = XRDevice::from_libmonado(monado);
@ -393,6 +395,32 @@ impl AsyncComponent for App {
} }
} }
} }
if xrservice_worker_is_alive
&& self.get_selected_profile().xrservice_type == XRServiceType::Wivrn
{
// is in pairing mode?
sender.input(Msg::WivrnCheckPairMode);
}
}
Msg::WivrnCheckPairMode => {
if self.get_selected_profile().xrservice_type == XRServiceType::Wivrn {
match wivrn_dbus::is_pairing_mode().await {
Ok(state) => {
self.main_view
.sender()
.emit(MainViewMsg::SetWivrnPairingMode(state));
self.main_view
.sender()
.emit(MainViewMsg::SetWivrnSupportsPairing(true));
}
Err(e) => {
eprintln!("Error: failed to get wivrn pairing mode: {e:?}");
self.main_view
.sender()
.emit(MainViewMsg::SetWivrnSupportsPairing(false));
}
};
}
} }
Msg::EnableDebugViewChanged(val) => { Msg::EnableDebugViewChanged(val) => {
self.config.debug_view_enabled = val; self.config.debug_view_enabled = val;

View file

@ -25,6 +25,7 @@ use crate::{
steamvr_utils::chaperone_info_exists, steamvr_utils::chaperone_info_exists,
}, },
vulkaninfo::VulkanInfo, vulkaninfo::VulkanInfo,
wivrn_dbus,
xr_devices::XRDevice, xr_devices::XRDevice,
}; };
use adw::{prelude::*, ResponseAppearance}; use adw::{prelude::*, ResponseAppearance};
@ -33,7 +34,6 @@ use relm4::{
actions::{ActionGroupName, RelmAction, RelmActionGroup}, actions::{ActionGroupName, RelmAction, RelmActionGroup},
new_action_group, new_stateless_action, new_action_group, new_stateless_action,
prelude::*, prelude::*,
ComponentParts, ComponentSender, SimpleComponent,
}; };
use std::{fs::read_to_string, io::Write}; use std::{fs::read_to_string, io::Write};
@ -73,6 +73,9 @@ pub struct MainView {
xrservice_ready: bool, xrservice_ready: bool,
#[tracker::do_not_track] #[tracker::do_not_track]
vkinfo: Option<VulkanInfo>, vkinfo: Option<VulkanInfo>,
wivrn_pairing_mode: bool,
wivrn_pin: Option<String>,
wivrn_supports_pairing: bool,
} }
#[derive(Debug)] #[derive(Debug)]
@ -96,6 +99,10 @@ pub enum MainViewMsg {
ExportProfile, ExportProfile,
ImportProfile, ImportProfile,
OpenProfileEditor(Profile), OpenProfileEditor(Profile),
SetWivrnSupportsPairing(bool),
SetWivrnPairingMode(bool),
StopWivrnPairingMode,
StartWivrnPairingMode,
} }
#[derive(Debug)] #[derive(Debug)]
@ -116,7 +123,7 @@ pub struct MainViewInit {
} }
impl MainView { impl MainView {
fn create_profile_editor(&mut self, sender: ComponentSender<MainView>, prof: Profile) { fn create_profile_editor(&mut self, sender: AsyncComponentSender<Self>, prof: Profile) {
self.profile_editor = Some( self.profile_editor = Some(
ProfileEditor::builder() ProfileEditor::builder()
.launch(ProfileEditorInit { .launch(ProfileEditorInit {
@ -130,11 +137,12 @@ impl MainView {
} }
} }
#[relm4::component(pub)] #[relm4::component(pub async)]
impl SimpleComponent for MainView { impl AsyncComponent for MainView {
type Init = MainViewInit; type Init = MainViewInit;
type Input = MainViewMsg; type Input = MainViewMsg;
type Output = MainViewOutMsg; type Output = MainViewOutMsg;
type CommandOutput = ();
menu! { menu! {
app_menu: { app_menu: {
@ -240,28 +248,136 @@ impl SimpleComponent for MainView {
set_visible: model.xrservice_active, set_visible: model.xrservice_active,
add_css_class: "card", add_css_class: "card",
gtk::Label { gtk::Label {
#[track = "model.changed(Self::xrservice_active()) || model.changed(Self::xrservice_ready())"] #[track = "model.changed(Self::xrservice_active()) || model.changed(Self::xrservice_ready()) || model.changed(Self::wivrn_pairing_mode())"]
set_label: if model.xrservice_ready { set_label: {
"Service ready, you can launch XR apps"
} else {
match model.selected_profile.xrservice_type { match model.selected_profile.xrservice_type {
XRServiceType::Monado => XRServiceType::Monado =>
"Starting…", if model.xrservice_ready {
"Service ready, you can launch XR apps"
} else {
"Starting…"
}
XRServiceType::Wivrn => XRServiceType::Wivrn =>
"Starting, please connect your client device…", if model.wivrn_pairing_mode {
"Pairing mode"
} else {
"Starting, connect your client device…"
}
} }
}, },
set_margin_all: 18, set_margin_all: 18,
add_css_class: "heading", add_css_class: "heading",
add_css_class: "success", add_css_class: "success",
add_css_class: "warning", add_css_class: "warning",
#[track = "model.changed(Self::xrservice_active()) || model.changed(Self::xrservice_ready())"] #[track = "model.changed(Self::xrservice_active()) || model.changed(Self::xrservice_ready()) || model.changed(Self::wivrn_pairing_mode())"]
set_class_active: ("warning", !model.xrservice_ready), set_class_active: (
"success",
model.xrservice_ready
&& (
model.selected_profile.xrservice_type != XRServiceType::Wivrn
|| !model.wivrn_pairing_mode
)
),
set_wrap: true, set_wrap: true,
set_justify: gtk::Justification::Center, set_justify: gtk::Justification::Center,
}, },
}, },
model.devices_box.widget(), model.devices_box.widget(),
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_hexpand: true,
set_vexpand: false,
set_spacing: 12,
add_css_class: "card",
add_css_class: "padded",
#[track = "model.changed(Self::wivrn_supports_pairing()) || model.changed(Self::xrservice_active()) || model.changed(Self::selected_profile()) || model.changed(Self::wivrn_pairing_mode()) || model.changed(Self::wivrn_pin())"]
set_visible: model.wivrn_supports_pairing
&& model.xrservice_active
&& model.selected_profile.xrservice_type == XRServiceType::Wivrn
&& !model.wivrn_pairing_mode,
gtk::Label {
add_css_class: "heading",
set_hexpand: true,
set_xalign: 0.0,
set_label: "Pairing mode",
set_wrap: true,
set_wrap_mode: gtk::pango::WrapMode::Word,
},
gtk::Label {
add_css_class: "dim-label",
set_hexpand: true,
set_label: concat!(
"To connect a new device to WiVRn, you ",
"will need to pair it first.\n\n",
"You can do so by starting the pairing mode ",
"with the button below."
),
set_xalign: 0.0,
set_wrap: true,
set_wrap_mode: gtk::pango::WrapMode::Word,
},
gtk::Button {
add_css_class: "suggested-action",
set_label: "Start pairing mode",
set_halign: gtk::Align::Start,
connect_clicked[sender] => move |_| {
sender.input(Self::Input::StartWivrnPairingMode);
}
},
},
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_hexpand: true,
set_vexpand: false,
set_spacing: 12,
add_css_class: "card",
add_css_class: "padded",
#[track = "model.changed(Self::wivrn_supports_pairing()) || model.changed(Self::xrservice_active()) || model.changed(Self::selected_profile()) || model.changed(Self::wivrn_pairing_mode()) || model.changed(Self::wivrn_pin())"]
set_visible: model.wivrn_supports_pairing
&& model.xrservice_active
&& model.selected_profile.xrservice_type == XRServiceType::Wivrn
&& model.wivrn_pairing_mode && model.wivrn_pin.is_some(),
gtk::Label {
add_css_class: "heading",
set_hexpand: true,
set_xalign: 0.0,
set_label: "Pairing mode",
set_wrap: true,
set_wrap_mode: gtk::pango::WrapMode::Word,
},
gtk::Label {
add_css_class: "dim-label",
set_hexpand: true,
set_label: concat!(
"WiVRn is in pairing mode. Pair your client ",
"device with the following PIN:"
),
set_xalign: 0.0,
set_wrap: true,
set_wrap_mode: gtk::pango::WrapMode::Word,
},
gtk::Label {
add_css_class: "title-2",
add_css_class: "monospace",
set_hexpand: true,
set_selectable: true,
#[track = "model.changed(Self::wivrn_pin())"]
set_label: model.wivrn_pin
.as_deref().unwrap_or(""),
set_xalign: 0.5,
set_justify: gtk::Justification::Center,
set_wrap: true,
set_wrap_mode: gtk::pango::WrapMode::Word,
},
gtk::Button {
add_css_class: "destructive-action",
set_label: "Stop pairing mode",
set_halign: gtk::Align::Start,
connect_clicked[sender] => move |_| {
sender.input(Self::Input::StopWivrnPairingMode);
}
},
},
gtk::Box { gtk::Box {
set_orientation: gtk::Orientation::Vertical, set_orientation: gtk::Orientation::Vertical,
set_hexpand: true, set_hexpand: true,
@ -486,11 +602,48 @@ impl SimpleComponent for MainView {
} }
} }
fn update(&mut self, message: Self::Input, sender: ComponentSender<Self>) { async fn update(
&mut self,
message: Self::Input,
sender: AsyncComponentSender<Self>,
_root: &Self::Root,
) {
self.reset(); self.reset();
match message { match message {
Self::Input::ClockTicking => {} Self::Input::ClockTicking => {}
Self::Input::SetWivrnSupportsPairing(supported) => {
if self.wivrn_supports_pairing != supported {
self.set_wivrn_supports_pairing(supported)
}
}
Self::Input::SetWivrnPairingMode(enabled) => {
if self.wivrn_pairing_mode != enabled {
self.set_wivrn_pairing_mode(enabled);
if enabled {
match wivrn_dbus::pairing_pin().await {
Ok(pin) => {
self.set_wivrn_pin(Some(pin));
}
Err(e) => {
eprintln!("Error: failed to get wivrn pairing pin: {e:?}");
}
};
} else {
self.set_wivrn_pin(None);
}
}
}
Self::Input::StopWivrnPairingMode => {
if let Err(e) = wivrn_dbus::disable_pairing().await {
eprintln!("Error: failed to stop wivrn pairing mode: {e:?}");
}
}
Self::Input::StartWivrnPairingMode => {
if let Err(e) = wivrn_dbus::enable_pairing().await {
eprintln!("Error: failed to start wivrn pairing mode: {e:?}");
}
}
Self::Input::StartStopClicked => { Self::Input::StartStopClicked => {
sender sender
.output(Self::Output::DoStartStopXRService) .output(Self::Output::DoStartStopXRService)
@ -504,6 +657,7 @@ impl SimpleComponent for MainView {
Self::Input::XRServiceActiveChanged(active, profile, show_launch_opts) => { Self::Input::XRServiceActiveChanged(active, profile, show_launch_opts) => {
if !active { if !active {
self.set_xrservice_ready(false); self.set_xrservice_ready(false);
sender.input(Self::Input::SetWivrnPairingMode(false));
} }
self.set_xrservice_active(active); self.set_xrservice_active(active);
self.steamvr_calibration_box self.steamvr_calibration_box
@ -742,11 +896,11 @@ impl SimpleComponent for MainView {
} }
} }
fn init( async fn init(
init: Self::Init, init: Self::Init,
root: Self::Root, root: Self::Root,
sender: ComponentSender<Self>, sender: AsyncComponentSender<Self>,
) -> ComponentParts<Self> { ) -> AsyncComponentParts<Self> {
let profile_not_editable_dialog = adw::AlertDialog::builder() let profile_not_editable_dialog = adw::AlertDialog::builder()
.heading("This profile is not editable") .heading("This profile is not editable")
.body(concat!( .body(concat!(
@ -866,6 +1020,9 @@ impl SimpleComponent for MainView {
profile_delete_action, profile_delete_action,
profile_export_action, profile_export_action,
vkinfo: init.vkinfo, vkinfo: init.vkinfo,
wivrn_pairing_mode: false,
wivrn_supports_pairing: false,
wivrn_pin: None,
tracker: 0, tracker: 0,
}; };
let widgets = view_output!(); let widgets = view_output!();
@ -919,7 +1076,7 @@ impl SimpleComponent for MainView {
root.insert_action_group(ProfileActionGroup::NAME, Some(&actions.into_action_group())); root.insert_action_group(ProfileActionGroup::NAME, Some(&actions.into_action_group()));
ComponentParts { model, widgets } AsyncComponentParts { model, widgets }
} }
} }

122
src/wivrn_dbus/internal.rs Normal file
View file

@ -0,0 +1,122 @@
//! # D-Bus interface proxy for: `io.github.wivrn.Server`
//!
//! This code was generated by `zbus-xmlgen` `5.0.1` from D-Bus introspection data.
//! Source: `io.github.wivrn.Server.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.
//!
//! This type implements the [D-Bus standard interfaces], (`org.freedesktop.DBus.*`) for which the
//! following zbus API can be used:
//!
//! * [`zbus::fdo::PropertiesProxy`]
//!
//! Consequently `zbus-xmlgen` did not generate code for the above interfaces.
//!
//! [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 = "io.github.wivrn.Server",
default_path = "/io/github/wivrn/Server",
default_service = "io.github.wivrn.Server"
)]
pub trait Server {
/// DisablePairing method
fn disable_pairing(&self) -> zbus::Result<()>;
/// Disconnect method
fn disconnect(&self) -> zbus::Result<()>;
/// EnablePairing method
fn enable_pairing(&self, TimeoutSecs: i32) -> zbus::Result<String>;
/// Quit method
fn quit(&self) -> zbus::Result<()>;
/// RenameKey method
fn rename_key(&self, PublicKey: &str, Name: &str) -> zbus::Result<()>;
/// RevokeKey method
fn revoke_key(&self, PublicKey: &str) -> zbus::Result<()>;
/// AvailableRefreshRates property
#[zbus(property)]
fn available_refresh_rates(&self) -> zbus::Result<Vec<f64>>;
/// EncryptionEnabled property
#[zbus(property)]
fn encryption_enabled(&self) -> zbus::Result<bool>;
/// EyeGaze property
#[zbus(property)]
fn eye_gaze(&self) -> zbus::Result<bool>;
/// FaceTracking property
#[zbus(property)]
fn face_tracking(&self) -> zbus::Result<bool>;
/// FieldOfView property
#[zbus(property)]
fn field_of_view(&self) -> zbus::Result<Vec<(f64, f64, f64, f64)>>;
/// HandTracking property
#[zbus(property)]
fn hand_tracking(&self) -> zbus::Result<bool>;
/// HeadsetConnected property
#[zbus(property)]
fn headset_connected(&self) -> zbus::Result<bool>;
/// JsonConfiguration property
#[zbus(property)]
fn json_configuration(&self) -> zbus::Result<String>;
#[zbus(property)]
fn set_json_configuration(&self, value: &str) -> zbus::Result<()>;
/// KnownKeys property
#[zbus(property)]
fn known_keys(&self) -> zbus::Result<Vec<(String, String)>>;
/// MicChannels property
#[zbus(property)]
fn mic_channels(&self) -> zbus::Result<u32>;
/// MicSampleRate property
#[zbus(property)]
fn mic_sample_rate(&self) -> zbus::Result<u32>;
/// PairingEnabled property
#[zbus(property)]
fn pairing_enabled(&self) -> zbus::Result<bool>;
/// Pin property
#[zbus(property)]
fn pin(&self) -> zbus::Result<String>;
/// PreferredRefreshRate property
#[zbus(property)]
fn preferred_refresh_rate(&self) -> zbus::Result<f64>;
/// RecommendedEyeSize property
#[zbus(property)]
fn recommended_eye_size(&self) -> zbus::Result<(u32, u32)>;
/// SpeakerChannels property
#[zbus(property)]
fn speaker_channels(&self) -> zbus::Result<u32>;
/// SpeakerSampleRate property
#[zbus(property)]
fn speaker_sample_rate(&self) -> zbus::Result<u32>;
/// SteamCommand property
#[zbus(property)]
fn steam_command(&self) -> zbus::Result<String>;
/// SupportedCodecs property
#[zbus(property)]
fn supported_codecs(&self) -> zbus::Result<Vec<String>>;
}

37
src/wivrn_dbus/mod.rs Normal file
View file

@ -0,0 +1,37 @@
// how to regenerate this one:
//
// ```bash
// cargo install zbus_xmlgen
// curl -sSLO https://github.com/WiVRn/WiVRn/blob/master/dbus/io.github.wivrn.Server.xml
// zbus-xmlgen file io.github.wivrn.Server.xml
// ```
//
// it should output a file called server.rs, move it accordingly
#[rustfmt::skip]
#[allow(non_snake_case)]
mod internal;
/// timeout for dbus methods in seconds
const TIMEOUT: i32 = 10;
async fn proxy<'a>() -> zbus::Result<internal::ServerProxy<'a>> {
let connection = zbus::Connection::session().await?;
let proxy = internal::ServerProxy::new(&connection).await?;
Ok(proxy)
}
pub async fn is_pairing_mode() -> zbus::Result<bool> {
proxy().await?.pairing_enabled().await
}
pub async fn enable_pairing() -> zbus::Result<String> {
proxy().await?.enable_pairing(TIMEOUT).await
}
pub async fn disable_pairing() -> zbus::Result<()> {
proxy().await?.disable_pairing().await
}
pub async fn pairing_pin() -> zbus::Result<String> {
proxy().await?.pin().await
}