use futures::StreamExt;
use std::sync::Mutex;
use tauri::{plugin::PluginApi, AppHandle, Emitter, Manager, Runtime};
use zbus::{
zvariant::{ObjectPath, OwnedValue, Value as ZbusValue},
Connection, MessageStream, MessageType, Proxy,
};
use std::collections::HashMap;
use std::convert::TryFrom;
use crate::commands::{get_adapter_state, get_device_info};
use crate::models::*;
use crate::Result as CrateResult;
pub struct BluetoothManager {
pub conn: Connection,
pub initialized: Mutex<bool>,
}
pub async fn init<R: Runtime>(app: AppHandle<R>, _api: PluginApi<R, ()>) -> CrateResult<()> {
let conn = Connection::system().await?;
let manager = BluetoothManager {
conn: conn.clone(),
initialized: Mutex::new(false),
};
app.manage(manager);
setup_dbus_subscriptions(&conn).await?;
tauri::async_runtime::spawn(run_signal_listener(conn, app));
Ok(())
}
async fn setup_dbus_subscriptions(conn: &Connection) -> CrateResult<()> {
let proxy = Proxy::new(
conn,
"org.bluez",
"/",
"org.freedesktop.DBus.ObjectManager",
).await?;
match proxy.call_method("GetManagedObjects", &()).await {
Ok(_) => println!("[bluetooth-plugin] Successfully connected to BlueZ ObjectManager"),
Err(e) => {
eprintln!("[bluetooth-plugin] Failed to connect to BlueZ ObjectManager: {:?}", e);
return Err(e.into());
}
}
let dbus_proxy = Proxy::new(
conn,
"org.freedesktop.DBus",
"/org/freedesktop/DBus",
"org.freedesktop.DBus",
).await?;
let rules = vec![
"type='signal',sender='org.bluez',interface='org.freedesktop.DBus.ObjectManager'",
"type='signal',sender='org.bluez',interface='org.freedesktop.DBus.Properties'",
"type='signal',sender='org.bluez',interface='org.bluez.Adapter1'",
"type='signal',sender='org.bluez',interface='org.bluez.Device1'",
];
for rule in rules {
match dbus_proxy.call_method("AddMatch", &(rule,)).await {
Ok(_) => println!("[bluetooth-plugin] Added D-Bus match rule: {}", rule),
Err(e) => eprintln!("[bluetooth-plugin] Failed to add match rule '{}': {:?}", rule, e),
}
}
Ok(())
}
fn helper_adapter_info_from_props(
path: String,
props: &HashMap<String, OwnedValue>,
) -> AdapterInfo {
AdapterInfo {
path,
address: props.get("Address").and_then(|v| String::try_from(v.clone()).ok()).unwrap_or_default(),
name: props.get("Name").and_then(|v| String::try_from(v.clone()).ok()).unwrap_or_default(),
alias: props.get("Alias").and_then(|v| String::try_from(v.clone()).ok()).unwrap_or_default(),
class: props.get("Class").and_then(|v| u32::try_from(v.clone()).ok()).unwrap_or_default(),
powered: props.get("Powered").and_then(|v| bool::try_from(v.clone()).ok()).unwrap_or(false),
discoverable: props.get("Discoverable").and_then(|v| bool::try_from(v.clone()).ok()).unwrap_or(false),
discoverable_timeout: props.get("DiscoverableTimeout").and_then(|v| u32::try_from(v.clone()).ok()).unwrap_or_default(),
pairable: props.get("Pairable").and_then(|v| bool::try_from(v.clone()).ok()).unwrap_or(false),
pairable_timeout: props.get("PairableTimeout").and_then(|v| u32::try_from(v.clone()).ok()).unwrap_or_default(),
discovering: props.get("Discovering").and_then(|v| bool::try_from(v.clone()).ok()).unwrap_or(false),
uuids: props.get("UUIDs")
.and_then(|v| Vec::<String>::try_from(v.clone()).ok())
.unwrap_or_default(),
modalias: props.get("Modalias").and_then(|v| String::try_from(v.clone()).ok()),
}
}
fn helper_device_info_from_props(path: String, props: &HashMap<String, OwnedValue>) -> DeviceInfo {
DeviceInfo {
path,
address: props.get("Address").and_then(|v| String::try_from(v.clone()).ok()).unwrap_or_default(),
name: props.get("Name").and_then(|v| String::try_from(v.clone()).ok()),
alias: props.get("Alias").and_then(|v| String::try_from(v.clone()).ok()),
class: props.get("Class").and_then(|v| u32::try_from(v.clone()).ok()),
appearance: props.get("Appearance").and_then(|v| u16::try_from(v.clone()).ok()),
icon: props.get("Icon").and_then(|v| String::try_from(v.clone()).ok()),
paired: props.get("Paired").and_then(|v| bool::try_from(v.clone()).ok()).unwrap_or(false),
trusted: props.get("Trusted").and_then(|v| bool::try_from(v.clone()).ok()).unwrap_or(false),
blocked: props.get("Blocked").and_then(|v| bool::try_from(v.clone()).ok()).unwrap_or(false),
legacy_pairing: props.get("LegacyPairing").and_then(|v| bool::try_from(v.clone()).ok()).unwrap_or(false),
rssi: props.get("RSSI").and_then(|v| i16::try_from(v.clone()).ok()),
tx_power: props.get("TxPower").and_then(|v| i16::try_from(v.clone()).ok()),
connected: props.get("Connected").and_then(|v| bool::try_from(v.clone()).ok()).unwrap_or(false),
uuids: props.get("UUIDs")
.and_then(|v| Vec::<String>::try_from(v.clone()).ok())
.unwrap_or_default(),
adapter: props.get("Adapter")
.and_then(|v| ObjectPath::try_from(v.clone()).ok())
.map(|p: ObjectPath| p.to_string())
.unwrap_or_default(),
services_resolved: props.get("ServicesResolved").and_then(|v| bool::try_from(v.clone()).ok()).unwrap_or(false),
}
}
async fn run_signal_listener<R: Runtime>(conn: Connection, app: AppHandle<R>) {
let mut stream = MessageStream::from(conn.clone());
let mut bluez_unique_name: Option<String> = None;
let _dbus_proxy = match Proxy::new(
&conn,
"org.freedesktop.DBus",
"/org/freedesktop/DBus",
"org.freedesktop.DBus",
).await {
Ok(proxy) => {
match proxy.call_method("GetNameOwner", &("org.bluez",)).await {
Ok(reply) => {
if let Ok(unique_name) = reply.body::<String>() {
bluez_unique_name = Some(unique_name);
}
}
Err(e) => eprintln!("[bluetooth-plugin] Failed to get org.bluez unique name: {:?}", e),
}
Some(proxy)
}
Err(_e) => {
None
}
};
while let Some(msg_res) = stream.next().await {
match msg_res {
Ok(msg) => {
if msg.message_type() == MessageType::Signal {
let header = match msg.header() {
Ok(h) => h,
Err(_e) => {
continue;
}
};
let sender_opt_str = match header.sender() {
Ok(Some(unique_name_ref)) => Some(unique_name_ref.as_str()),
Ok(None) => None,
Err(e) => {
eprintln!("[bluetooth-plugin] Error getting sender from header: {:?}", e);
None
}
};
let is_bluez_signal = sender_opt_str == Some("org.bluez") ||
(bluez_unique_name.is_some() && sender_opt_str == bluez_unique_name.as_deref());
if is_bluez_signal {
let interface_opt_string = match header.interface() {
Ok(Some(i_ref)) => Some(i_ref.as_str().to_string()),
Ok(None) => None,
Err(e) => {
eprintln!("[bluetooth-plugin] Error getting interface from header: {:?}", e);
None
}
};
let member_opt_string = match header.member() {
Ok(Some(m_ref)) => Some(m_ref.as_str().to_string()),
Ok(None) => None,
Err(e) => {
eprintln!("[bluetooth-plugin] Error getting member from header: {:?}", e);
None
}
};
let path_opt_string = match header.path() {
Ok(Some(p_ref)) => Some(p_ref.as_str().to_string()),
Ok(None) => None,
Err(e) => {
eprintln!("[bluetooth-plugin] Error getting path from header: {:?}", e);
None
}
};
match (interface_opt_string.as_deref(), member_opt_string.as_deref()) {
(Some("org.freedesktop.DBus.ObjectManager"), Some("InterfacesAdded")) => {
match msg.body::<(ObjectPath<'_>, HashMap<String, HashMap<String, OwnedValue>>)>() {
Ok((object_path, interfaces_and_properties)) => {
let path_string = object_path.to_string();
if let Some(adapter_props) = interfaces_and_properties.get("org.bluez.Adapter1") {
let adapter_info = helper_adapter_info_from_props(path_string.clone(), adapter_props);
app.emit("bluetooth-change", BluetoothChange {
change_type: "adapter-added".to_string(),
data: serde_json::to_value(adapter_info).unwrap_or_default(),
}).unwrap_or_else(|e| eprintln!("[bluetooth-plugin] Failed to emit adapter-added: {}", e));
}
if let Some(device_props) = interfaces_and_properties.get("org.bluez.Device1") {
let device_info = helper_device_info_from_props(path_string.clone(), device_props);
app.emit("bluetooth-change", BluetoothChange {
change_type: "device-added".to_string(),
data: serde_json::to_value(device_info).unwrap_or_default(),
}).unwrap_or_else(|e| eprintln!("[bluetooth-plugin] Failed to emit device-added: {}", e));
}
}
Err(e) => {
eprintln!("[bluetooth-plugin] Error decoding InterfacesAdded body: {:?}", e);
app.emit("bluetooth-change", BluetoothChange {
change_type: "error".to_string(),
data: serde_json::json!({ "message": format!("Error decoding InterfacesAdded: {:?}", e) }),
}).unwrap_or_else(|err| eprintln!("[bluetooth-plugin] Failed to emit error: {}", err));
}
}
}
(Some("org.freedesktop.DBus.ObjectManager"), Some("InterfacesRemoved")) => {
match msg.body::<(ObjectPath<'_>, Vec<String>)>() {
Ok((object_path, interfaces_removed)) => {
let path_string = object_path.to_string();
if interfaces_removed.contains(&"org.bluez.Adapter1".to_string()) {
app.emit("bluetooth-change", BluetoothChange {
change_type: "adapter-removed".to_string(),
data: serde_json::json!({ "path": path_string.clone() }),
}).unwrap_or_else(|e| eprintln!("[bluetooth-plugin] Failed to emit adapter-removed: {}", e));
}
if interfaces_removed.contains(&"org.bluez.Device1".to_string()) {
app.emit("bluetooth-change", BluetoothChange {
change_type: "device-removed".to_string(),
data: serde_json::json!({ "path": path_string }),
}).unwrap_or_else(|e| eprintln!("[bluetooth-plugin] Failed to emit device-removed: {}", e));
}
}
Err(e) => {
eprintln!("[bluetooth-plugin] Error decoding InterfacesRemoved body: {:?}", e);
app.emit("bluetooth-change", BluetoothChange {
change_type: "error".to_string(),
data: serde_json::json!({ "message": format!("Error decoding InterfacesRemoved: {:?}", e) }),
}).unwrap_or_else(|err| eprintln!("[bluetooth-plugin] Failed to emit error: {}", err));
}
}
}
(Some("org.freedesktop.DBus.Properties"), Some("PropertiesChanged")) => {
if let Some(p_str) = path_opt_string {
match msg.body::<(String, HashMap<String, ZbusValue<'_>>, Vec<String>)>() {
Ok((changed_interface_name, _changed_properties, _invalidated_properties)) => {
if changed_interface_name == "org.bluez.Adapter1" {
match get_adapter_state(p_str.clone()).await {
Ok(adapter_info) => {
app.emit("bluetooth-change", BluetoothChange {
change_type: "adapter-property-changed".to_string(),
data: serde_json::to_value(adapter_info).unwrap_or_default(),
}).unwrap_or_else(|e| eprintln!("[bluetooth-plugin] Failed to emit adapter-property-changed: {}", e));
}
Err(e) => {
eprintln!("[bluetooth-plugin] Error getting adapter state for {}: {:?}", p_str, e);
app.emit("bluetooth-change", BluetoothChange {
change_type: "error".to_string(),
data: serde_json::json!({ "message": format!("Error getting adapter state: {:?}", e) }),
}).unwrap_or_else(|err| eprintln!("[bluetooth-plugin] Failed to emit error: {}", err));
}
}
}
else if changed_interface_name == "org.bluez.Device1" {
match get_device_info(p_str.clone()).await {
Ok(device_info) => {
println!("[bluetooth-plugin] Device property changed: {}", p_str);
app.emit("bluetooth-change", BluetoothChange {
change_type: "device-property-changed".to_string(),
data: serde_json::to_value(device_info).unwrap_or_default(),
}).unwrap_or_else(|e| eprintln!("[bluetooth-plugin] Failed to emit device-property-changed: {}", e));
}
Err(e) => {
eprintln!("[bluetooth-plugin] Error getting device info for {}: {:?}", p_str, e);
app.emit("bluetooth-change", BluetoothChange {
change_type: "error".to_string(),
data: serde_json::json!({ "message": format!("Error getting device info: {:?}", e) }),
}).unwrap_or_else(|err| eprintln!("[bluetooth-plugin] Failed to emit error: {}", err));
}
}
}
}
Err(e) => {
eprintln!("[bluetooth-plugin] Error decoding PropertiesChanged body: {:?}", e);
app.emit("bluetooth-change", BluetoothChange {
change_type: "error".to_string(),
data: serde_json::json!({ "message": format!("Error decoding PropertiesChanged: {:?}", e) }),
}).unwrap_or_else(|err| eprintln!("[bluetooth-plugin] Failed to emit error: {}", err));
}
}
} else {
eprintln!("[bluetooth-plugin] PropertiesChanged signal received without a valid path.");
app.emit("bluetooth-change", BluetoothChange {
change_type: "error".to_string(),
data: serde_json::json!({ "message": "PropertiesChanged signal without path" }),
}).unwrap_or_else(|err| eprintln!("[bluetooth-plugin] Failed to emit error: {}", err));
}
}
(Some("org.bluez.Device1"), Some("Disconnected")) => {
if let Some(p_str) = path_opt_string {
match get_device_info(p_str.clone()).await {
Ok(device_info) => {
app.emit("bluetooth-change", BluetoothChange {
change_type: "device-disconnected".to_string(),
data: serde_json::to_value(device_info).unwrap_or_default(),
}).unwrap_or_else(|e| eprintln!("[bluetooth-plugin] Failed to emit device-disconnected: {}", e));
}
Err(e) => {
eprintln!("[bluetooth-plugin] Error getting device info for disconnected device {}: {:?}", p_str, e);
}
}
}
}
(Some("org.bluez.Device1"), Some("Connected")) => {
if let Some(p_str) = path_opt_string {
match get_device_info(p_str.clone()).await {
Ok(device_info) => {
app.emit("bluetooth-change", BluetoothChange {
change_type: "device-connected".to_string(),
data: serde_json::to_value(device_info).unwrap_or_default(),
}).unwrap_or_else(|e| eprintln!("[bluetooth-plugin] Failed to emit device-connected: {}", e));
}
Err(e) => {
eprintln!("[bluetooth-plugin] Error getting device info for connected device {}: {:?}", p_str, e);
}
}
}
}
_ => {
}
}
}
}
}
Err(e) => {
eprintln!("[bluetooth-plugin] Error reading from D-Bus message stream: {:?}", e);
app.emit("bluetooth-change", BluetoothChange {
change_type: "dbus-error".to_string(),
data: serde_json::json!({ "message": format!("D-Bus stream error: {:?}", e) }),
}).unwrap_or_else(|err| eprintln!("[bluetooth-plugin] Failed to emit dbus-error: {}", err));
break;
}
}
}
}
impl BluetoothManager {
pub fn ping(
&self,
payload: crate::models::PingRequest,
) -> CrateResult<crate::models::PingResponse> {
Ok(crate::models::PingResponse {
value: payload.value,
})
}
}