mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-01-31 23:29:10 +00:00
linux-rust: add skeleton for other devices
This commit is contained in:
68
linux-rust/Cargo.lock
generated
68
linux-rust/Cargo.lock
generated
@@ -548,6 +548,12 @@ dependencies = [
|
||||
"syn 2.0.107",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder-lite"
|
||||
version = "0.1.0"
|
||||
@@ -1822,6 +1828,7 @@ dependencies = [
|
||||
"iced_renderer",
|
||||
"iced_widget",
|
||||
"iced_winit",
|
||||
"image 0.24.9",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
@@ -1885,6 +1892,8 @@ dependencies = [
|
||||
"half",
|
||||
"iced_core",
|
||||
"iced_futures",
|
||||
"image 0.24.9",
|
||||
"kamadak-exif",
|
||||
"log",
|
||||
"once_cell",
|
||||
"raw-window-handle",
|
||||
@@ -1996,6 +2005,24 @@ version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.24.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder",
|
||||
"color_quant",
|
||||
"exr",
|
||||
"gif",
|
||||
"jpeg-decoder",
|
||||
"num-traits",
|
||||
"png 0.17.16",
|
||||
"qoi",
|
||||
"tiff 0.9.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.25.8"
|
||||
@@ -2015,7 +2042,7 @@ dependencies = [
|
||||
"ravif",
|
||||
"rayon",
|
||||
"rgb",
|
||||
"tiff",
|
||||
"tiff 0.10.3",
|
||||
"zune-core",
|
||||
"zune-jpeg",
|
||||
]
|
||||
@@ -2039,7 +2066,7 @@ dependencies = [
|
||||
"ab_glyph",
|
||||
"approx",
|
||||
"getrandom 0.2.16",
|
||||
"image",
|
||||
"image 0.25.8",
|
||||
"itertools",
|
||||
"nalgebra",
|
||||
"num",
|
||||
@@ -2170,6 +2197,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jpeg-decoder"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07"
|
||||
dependencies = [
|
||||
"rayon",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.81"
|
||||
@@ -2180,6 +2216,15 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kamadak-exif"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef4fc70d0ab7e5b6bafa30216a6b48705ea964cdfc29c050f2412295eba58077"
|
||||
dependencies = [
|
||||
"mutate_once",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "khronos-egl"
|
||||
version = "6.0.0"
|
||||
@@ -2334,7 +2379,7 @@ dependencies = [
|
||||
"futures",
|
||||
"hex",
|
||||
"iced",
|
||||
"image",
|
||||
"image 0.25.8",
|
||||
"imageproc",
|
||||
"ksni",
|
||||
"libpulse-binding",
|
||||
@@ -2504,6 +2549,12 @@ dependencies = [
|
||||
"pxfm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mutate_once"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13d2233c9842d08cfe13f9eac96e207ca6a2ea10b80259ebe8ad0268be27d2af"
|
||||
|
||||
[[package]]
|
||||
name = "naga"
|
||||
version = "0.19.2"
|
||||
@@ -4116,6 +4167,17 @@ dependencies = [
|
||||
"syn 2.0.107",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiff"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"jpeg-decoder",
|
||||
"weezl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiff"
|
||||
version = "0.10.3"
|
||||
|
||||
@@ -11,7 +11,7 @@ uuid = "1.18.1"
|
||||
log = "0.4.28"
|
||||
dbus = "0.9.9"
|
||||
hex = "0.4.3"
|
||||
iced = {version = "0.13.1", features = ["tokio"]}
|
||||
iced = { version = "0.13.1", features = ["tokio", "image"] }
|
||||
libpulse-binding = "2.30.1"
|
||||
ksni = "0.3.1"
|
||||
image = "0.25.8"
|
||||
|
||||
BIN
linux-rust/assets/icon.png
Normal file
BIN
linux-rust/assets/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 266 KiB |
@@ -8,6 +8,8 @@ use tokio::time::{sleep, Instant};
|
||||
use std::collections::HashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json;
|
||||
use crate::devices::airpods::AirPodsInformation;
|
||||
use crate::devices::enums::{DeviceData, DeviceInformation, DeviceType};
|
||||
use crate::utils::get_devices_path;
|
||||
|
||||
const PSM: u16 = 0x1001;
|
||||
@@ -280,45 +282,11 @@ pub enum AACPEvent {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum DeviceType {
|
||||
AirPods,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LEData {
|
||||
pub struct AirPodsLEKeys {
|
||||
pub irk: String,
|
||||
pub enc_key: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AirPodsInformation {
|
||||
pub name: String,
|
||||
pub model_number: String,
|
||||
pub manufacturer: String,
|
||||
pub serial_number: String,
|
||||
pub version1: String,
|
||||
pub version2: String,
|
||||
pub hardware_revision: String,
|
||||
pub updater_identifier: String,
|
||||
pub left_serial_number: String,
|
||||
pub right_serial_number: String,
|
||||
pub version3: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind", content = "data")]
|
||||
pub enum DeviceInformation {
|
||||
AirPods(AirPodsInformation),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DeviceData {
|
||||
pub name: String,
|
||||
pub type_: DeviceType,
|
||||
pub le: LEData,
|
||||
pub information: Option<DeviceInformation>,
|
||||
}
|
||||
|
||||
pub struct AACPManagerState {
|
||||
pub sender: Option<mpsc::Sender<Vec<u8>>>,
|
||||
pub control_command_status_list: Vec<ControlCommandStatus>,
|
||||
@@ -647,7 +615,7 @@ impl AACPManager {
|
||||
strings.push(s.to_string());
|
||||
}
|
||||
}
|
||||
strings.remove(0); // Remove the first empty string as per comment
|
||||
strings.remove(0);
|
||||
let info = AirPodsInformation {
|
||||
name: strings.get(0).cloned().unwrap_or_default(),
|
||||
model_number: strings.get(1).cloned().unwrap_or_default(),
|
||||
@@ -660,6 +628,10 @@ impl AACPManager {
|
||||
left_serial_number: strings.get(8).cloned().unwrap_or_default(),
|
||||
right_serial_number: strings.get(9).cloned().unwrap_or_default(),
|
||||
version3: strings.get(10).cloned().unwrap_or_default(),
|
||||
le_keys: AirPodsLEKeys {
|
||||
irk: "".to_string(),
|
||||
enc_key: "".to_string(),
|
||||
},
|
||||
};
|
||||
let mut state = self.state.lock().await;
|
||||
if let Some(mac) = state.airpods_mac {
|
||||
@@ -715,12 +687,29 @@ impl AACPManager {
|
||||
let device_data = state.devices.entry(mac_str.clone()).or_insert(DeviceData {
|
||||
name: mac_str.clone(),
|
||||
type_: DeviceType::AirPods,
|
||||
le: LEData { irk: "".to_string(), enc_key: "".to_string() },
|
||||
information: None,
|
||||
});
|
||||
match kt {
|
||||
ProximityKeyType::Irk => device_data.le.irk = hex::encode(key_data),
|
||||
ProximityKeyType::EncKey => device_data.le.enc_key = hex::encode(key_data),
|
||||
ProximityKeyType::Irk => {
|
||||
match device_data.information.as_mut() {
|
||||
Some(DeviceInformation::AirPods(info)) => {
|
||||
info.le_keys.irk = hex::encode(key_data);
|
||||
}
|
||||
_ => {
|
||||
error!("Device information is not AirPods for adding LE IRK.");
|
||||
}
|
||||
}
|
||||
}
|
||||
ProximityKeyType::EncKey => {
|
||||
match device_data.information.as_mut() {
|
||||
Some(DeviceInformation::AirPods(info)) => {
|
||||
info.le_keys.enc_key = hex::encode(key_data);
|
||||
}
|
||||
_ => {
|
||||
error!("Device information is not AirPods for adding LE encryption key.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,29 +16,34 @@ const OPCODE_READ_REQUEST: u8 = 0x0A;
|
||||
const OPCODE_WRITE_REQUEST: u8 = 0x12;
|
||||
const OPCODE_HANDLE_VALUE_NTF: u8 = 0x1B;
|
||||
const OPCODE_WRITE_RESPONSE: u8 = 0x13;
|
||||
const RESPONSE_TIMEOUT: u64 = 5000;
|
||||
|
||||
#[repr(u16)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum ATTHandles {
|
||||
Transparency = 0x18,
|
||||
LoudSoundReduction = 0x1B,
|
||||
HearingAid = 0x2A,
|
||||
AirPodsTransparency = 0x18,
|
||||
AirPodsLoudSoundReduction = 0x1B,
|
||||
AirPodsHearingAid = 0x2A,
|
||||
NothingEverything = 0x8002,
|
||||
NothingEverythingRead = 0x8005 // for some reason, and not the same as the write handle
|
||||
}
|
||||
|
||||
#[repr(u16)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum ATTCCCDHandles {
|
||||
Transparency = ATTHandles::Transparency as u16 + 1,
|
||||
LoudSoundReduction = ATTHandles::LoudSoundReduction as u16 + 1,
|
||||
HearingAid = ATTHandles::HearingAid as u16 + 1,
|
||||
Transparency = ATTHandles::AirPodsTransparency as u16 + 1,
|
||||
LoudSoundReduction = ATTHandles::AirPodsLoudSoundReduction as u16 + 1,
|
||||
HearingAid = ATTHandles::AirPodsHearingAid as u16 + 1,
|
||||
}
|
||||
|
||||
impl From<ATTHandles> for ATTCCCDHandles {
|
||||
fn from(handle: ATTHandles) -> Self {
|
||||
match handle {
|
||||
ATTHandles::Transparency => ATTCCCDHandles::Transparency,
|
||||
ATTHandles::LoudSoundReduction => ATTCCCDHandles::LoudSoundReduction,
|
||||
ATTHandles::HearingAid => ATTCCCDHandles::HearingAid,
|
||||
ATTHandles::AirPodsTransparency => ATTCCCDHandles::Transparency,
|
||||
ATTHandles::AirPodsLoudSoundReduction => ATTCCCDHandles::LoudSoundReduction,
|
||||
ATTHandles::AirPodsHearingAid => ATTCCCDHandles::HearingAid,
|
||||
ATTHandles::NothingEverything => panic!("No CCCD for NothingEverything handle"), // we don't request it
|
||||
ATTHandles::NothingEverythingRead => panic!("No CCD for NothingEverythingRead handle") // it sends notifications without CCCD
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,18 +51,13 @@ impl From<ATTHandles> for ATTCCCDHandles {
|
||||
struct ATTManagerState {
|
||||
sender: Option<mpsc::Sender<Vec<u8>>>,
|
||||
listeners: HashMap<u16, Vec<mpsc::UnboundedSender<Vec<u8>>>>,
|
||||
responses: mpsc::UnboundedReceiver<Vec<u8>>,
|
||||
response_tx: mpsc::UnboundedSender<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl ATTManagerState {
|
||||
fn new() -> Self {
|
||||
let (tx, rx) = mpsc::unbounded_channel();
|
||||
ATTManagerState {
|
||||
sender: None,
|
||||
listeners: HashMap::new(),
|
||||
responses: rx,
|
||||
response_tx: tx,
|
||||
listeners: HashMap::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,13 +65,18 @@ impl ATTManagerState {
|
||||
#[derive(Clone)]
|
||||
pub struct ATTManager {
|
||||
state: Arc<Mutex<ATTManagerState>>,
|
||||
response_rx: Arc<Mutex<mpsc::UnboundedReceiver<Vec<u8>>>>,
|
||||
response_tx: mpsc::UnboundedSender<Vec<u8>>,
|
||||
tasks: Arc<Mutex<JoinSet<()>>>,
|
||||
}
|
||||
|
||||
impl ATTManager {
|
||||
pub fn new() -> Self {
|
||||
let (tx, rx) = mpsc::unbounded_channel();
|
||||
ATTManager {
|
||||
state: Arc::new(Mutex::new(ATTManagerState::new())),
|
||||
response_rx: Arc::new(Mutex::new(rx)),
|
||||
response_tx: tx,
|
||||
tasks: Arc::new(Mutex::new(JoinSet::new())),
|
||||
}
|
||||
}
|
||||
@@ -184,11 +189,18 @@ impl ATTManager {
|
||||
}
|
||||
|
||||
async fn read_response(&self) -> Result<Vec<u8>> {
|
||||
let mut state = self.state.lock().await;
|
||||
match tokio::time::timeout(Duration::from_millis(2000), state.responses.recv()).await {
|
||||
debug!("Waiting for response...");
|
||||
let mut rx = self.response_rx.lock().await;
|
||||
match tokio::time::timeout(Duration::from_millis(RESPONSE_TIMEOUT), rx.recv()).await {
|
||||
Ok(Some(resp)) => Ok(resp),
|
||||
Ok(None) => Err(Error::from(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "Response channel closed"))),
|
||||
Err(_) => Err(Error::from(std::io::Error::new(std::io::ErrorKind::TimedOut, "Response timeout"))),
|
||||
Ok(None) => Err(Error::from(std::io::Error::new(
|
||||
std::io::ErrorKind::UnexpectedEof,
|
||||
"Response channel closed"
|
||||
))),
|
||||
Err(_) => Err(Error::from(std::io::Error::new(
|
||||
std::io::ErrorKind::TimedOut,
|
||||
"Response timeout"
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -217,10 +229,11 @@ async fn recv_thread(manager: ATTManager, sp: Arc<SeqPacket>) {
|
||||
let _ = listener.send(value.clone());
|
||||
}
|
||||
}
|
||||
} else if data[0] == OPCODE_WRITE_RESPONSE {
|
||||
let _ = manager.response_tx.send(vec![]);
|
||||
} else {
|
||||
// Response
|
||||
let state = manager.state.lock().await;
|
||||
let _ = state.response_tx.send(data[1..].to_vec());
|
||||
let _ = manager.response_tx.send(data[1..].to_vec());
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use std::io::Error;
|
||||
use bluer::Adapter;
|
||||
use log::debug;
|
||||
|
||||
pub(crate) async fn find_connected_airpods(adapter: &bluer::Adapter) -> bluer::Result<bluer::Device> {
|
||||
pub(crate) async fn find_connected_airpods(adapter: &Adapter) -> bluer::Result<bluer::Device> {
|
||||
let target_uuid = uuid::Uuid::parse_str("74ec2172-0bad-4d01-8f77-997b2be0722a").unwrap();
|
||||
|
||||
let addrs = adapter.device_addresses().await?;
|
||||
@@ -17,4 +19,23 @@ pub(crate) async fn find_connected_airpods(adapter: &bluer::Adapter) -> bluer::R
|
||||
}
|
||||
}
|
||||
Err(bluer::Error::from(Error::new(std::io::ErrorKind::NotFound, "No connected AirPods found")))
|
||||
}
|
||||
|
||||
pub async fn find_other_managed_devices(adapter: &Adapter, managed_macs: Vec<String>) -> bluer::Result<Vec<bluer::Device>> {
|
||||
let addrs = adapter.device_addresses().await?;
|
||||
let mut devices = Vec::new();
|
||||
for addr in addrs {
|
||||
let device = adapter.device(addr)?;
|
||||
let device_mac = device.address().to_string();
|
||||
let connected = device.is_connected().await.unwrap_or(false);
|
||||
debug!("Checking device: {}, connected: {}", device_mac, connected);
|
||||
if connected && managed_macs.contains(&device_mac) {
|
||||
debug!("Found managed device: {}", device_mac);
|
||||
devices.push(device);
|
||||
}
|
||||
}
|
||||
if !devices.is_empty() {
|
||||
return Ok(devices);
|
||||
}
|
||||
Err(bluer::Error::from(Error::new(std::io::ErrorKind::NotFound, "No other managed devices found")))
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
use std::cmp::PartialEq;
|
||||
use bluer::monitor::{Monitor, MonitorEvent, Pattern};
|
||||
use bluer::{Address, Session};
|
||||
use aes::Aes128;
|
||||
@@ -14,7 +13,7 @@ use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use crate::bluetooth::aacp::BatteryStatus;
|
||||
use crate::ui::tray::MyTray;
|
||||
use crate::bluetooth::aacp::{DeviceData, DeviceType};
|
||||
use crate::devices::enums::{DeviceData, DeviceInformation, DeviceType};
|
||||
use crate::utils::{get_devices_path, get_preferences_path, ah};
|
||||
|
||||
fn decrypt(key: &[u8; 16], data: &[u8; 16]) -> [u8; 16] {
|
||||
@@ -43,14 +42,6 @@ fn verify_rpa(addr: &str, irk: &[u8; 16]) -> bool {
|
||||
hash == computed_hash
|
||||
}
|
||||
|
||||
impl PartialEq for DeviceType {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(DeviceType::AirPods, DeviceType::AirPods) => true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn start_le_monitor(tray_handle: Option<ksni::Handle<MyTray>>) -> bluer::Result<()> {
|
||||
let session = Session::new().await?;
|
||||
let adapter = session.default_adapter().await?;
|
||||
@@ -107,15 +98,17 @@ pub async fn start_le_monitor(tray_handle: Option<ksni::Handle<MyTray>>) -> blue
|
||||
let mut found_mac = None;
|
||||
for (airpods_mac, device_data) in &all_devices {
|
||||
if device_data.type_ == DeviceType::AirPods {
|
||||
if let Ok(irk_bytes) = hex::decode(&device_data.le.irk) {
|
||||
if irk_bytes.len() == 16 {
|
||||
let irk: [u8; 16] = irk_bytes.as_slice().try_into().unwrap();
|
||||
debug!("Verifying RPA {} for airpods MAC {} with IRK {}", addr_str, airpods_mac, device_data.le.irk);
|
||||
if verify_rpa(&addr_str, &irk) {
|
||||
info!("Matched our device ({}) with the irk for {}", addr, airpods_mac);
|
||||
verified_macs.insert(addr, airpods_mac.clone());
|
||||
found_mac = Some(airpods_mac.clone());
|
||||
break;
|
||||
if let Some(DeviceInformation::AirPods(info)) = &device_data.information {
|
||||
if let Ok(irk_bytes) = hex::decode(&info.le_keys.irk) {
|
||||
if irk_bytes.len() == 16 {
|
||||
let irk: [u8; 16] = irk_bytes.as_slice().try_into().unwrap();
|
||||
debug!("Verifying RPA {} for airpods MAC {} with IRK {}", addr_str, airpods_mac, info.le_keys.irk);
|
||||
if verify_rpa(&addr_str, &irk) {
|
||||
info!("Matched our device ({}) with the irk for {}", addr, airpods_mac);
|
||||
verified_macs.insert(addr, airpods_mac.clone());
|
||||
found_mac = Some(airpods_mac.clone());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -133,8 +126,8 @@ pub async fn start_le_monitor(tray_handle: Option<ksni::Handle<MyTray>>) -> blue
|
||||
|
||||
if let Some(ref mac) = matched_airpods_mac {
|
||||
if let Some(device_data) = all_devices.get(mac) {
|
||||
if !device_data.le.enc_key.is_empty() {
|
||||
if let Ok(enc_key_bytes) = hex::decode(&device_data.le.enc_key) {
|
||||
if let Some(DeviceInformation::AirPods(info)) = &device_data.information {
|
||||
if let Ok(enc_key_bytes) = hex::decode(&info.le_keys.enc_key) {
|
||||
if enc_key_bytes.len() == 16 {
|
||||
matched_enc_key = Some(enc_key_bytes.as_slice().try_into().unwrap());
|
||||
}
|
||||
|
||||
52
linux-rust/src/bluetooth/managers.rs
Normal file
52
linux-rust/src/bluetooth/managers.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use crate::bluetooth::aacp::AACPManager;
|
||||
use crate::bluetooth::att::ATTManager;
|
||||
|
||||
pub enum BluetoothManager {
|
||||
AACP(Arc<AACPManager>),
|
||||
ATT(Arc<ATTManager>),
|
||||
}
|
||||
|
||||
pub struct DeviceManagers {
|
||||
att: Option<Arc<ATTManager>>,
|
||||
aacp: Option<Arc<AACPManager>>,
|
||||
}
|
||||
|
||||
impl DeviceManagers {
|
||||
fn new() -> Self {
|
||||
Self { att: None, aacp: None }
|
||||
}
|
||||
|
||||
fn with_aacp(aacp: AACPManager) -> Self {
|
||||
Self { att: None, aacp: Some(Arc::new(aacp)) }
|
||||
}
|
||||
|
||||
fn with_att(att: ATTManager) -> Self {
|
||||
Self { att: Some(Arc::new(att)), aacp: None }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BluetoothDevices {
|
||||
devices: HashMap<String, DeviceManagers>,
|
||||
}
|
||||
|
||||
impl BluetoothDevices {
|
||||
fn new() -> Self {
|
||||
Self { devices: HashMap::new() }
|
||||
}
|
||||
|
||||
fn add_aacp(&mut self, mac: String, manager: AACPManager) {
|
||||
self.devices
|
||||
.entry(mac)
|
||||
.or_insert_with(DeviceManagers::new)
|
||||
.aacp = Some(Arc::new(manager));
|
||||
}
|
||||
|
||||
fn add_att(&mut self, mac: String, manager: ATTManager) {
|
||||
self.devices
|
||||
.entry(mac)
|
||||
.or_insert_with(DeviceManagers::new)
|
||||
.att = Some(Arc::new(manager));
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
pub(crate) mod discovery;
|
||||
pub mod aacp;
|
||||
pub mod att;
|
||||
pub mod le;
|
||||
pub mod le;
|
||||
pub mod managers;
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::bluetooth::aacp::{AACPManager, ProximityKeyType, AACPEvent};
|
||||
use crate::bluetooth::aacp::{AACPManager, ProximityKeyType, AACPEvent, AirPodsLEKeys};
|
||||
use crate::bluetooth::aacp::ControlCommandIdentifiers;
|
||||
// use crate::bluetooth::att::ATTManager;
|
||||
use crate::media_controller::MediaController;
|
||||
@@ -6,20 +6,26 @@ use bluer::Address;
|
||||
use log::{debug, info, error};
|
||||
use std::sync::Arc;
|
||||
use ksni::Handle;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::time::{sleep, Duration};
|
||||
use crate::ui::tray::MyTray;
|
||||
use crate::ui::messages::UIMessage;
|
||||
use crate::ui::messages::BluetoothUIMessage;
|
||||
|
||||
pub struct AirPodsDevice {
|
||||
pub mac_address: Address,
|
||||
pub aacp_manager: AACPManager,
|
||||
// pub att_manager: ATTManager,
|
||||
pub media_controller: Arc<Mutex<MediaController>>,
|
||||
// pub command_tx: Option<tokio::sync::mpsc::UnboundedSender<(ControlCommandIdentifiers, Vec<u8>)>>,
|
||||
}
|
||||
|
||||
impl AirPodsDevice {
|
||||
pub async fn new(mac_address: Address, tray_handle: Option<Handle<MyTray>>, ui_tx: tokio::sync::mpsc::UnboundedSender<UIMessage>) -> Self {
|
||||
pub async fn new(
|
||||
mac_address: Address,
|
||||
tray_handle: Option<Handle<MyTray>>,
|
||||
ui_tx: tokio::sync::mpsc::UnboundedSender<BluetoothUIMessage>,
|
||||
) -> Self {
|
||||
info!("Creating new AirPodsDevice for {}", mac_address);
|
||||
let mut aacp_manager = AACPManager::new();
|
||||
aacp_manager.connect(mac_address).await;
|
||||
@@ -146,8 +152,9 @@ impl AirPodsDevice {
|
||||
|
||||
let aacp_manager_clone_events = aacp_manager.clone();
|
||||
let local_mac_events = local_mac.clone();
|
||||
let ui_tx_clone = ui_tx.clone();
|
||||
let command_tx_clone = command_tx.clone();
|
||||
tokio::spawn(async move {
|
||||
let ui_tx_clone = ui_tx.clone();
|
||||
while let Some(event) = rx.recv().await {
|
||||
let event_clone = event.clone();
|
||||
match event {
|
||||
@@ -182,12 +189,12 @@ impl AirPodsDevice {
|
||||
}
|
||||
debug!("Updated tray with new battery info");
|
||||
|
||||
let _ = ui_tx_clone.send(UIMessage::AACPUIEvent(mac_address.to_string(), event_clone));
|
||||
let _ = ui_tx_clone.send(BluetoothUIMessage::AACPUIEvent(mac_address.to_string(), event_clone));
|
||||
debug!("Sent BatteryInfo event to UI");
|
||||
}
|
||||
AACPEvent::ControlCommand(status) => {
|
||||
debug!("Received ControlCommand event: {:?}", status);
|
||||
let _ = ui_tx_clone.send(UIMessage::AACPUIEvent(mac_address.to_string(), event_clone));
|
||||
let _ = ui_tx_clone.send(BluetoothUIMessage::AACPUIEvent(mac_address.to_string(), event_clone));
|
||||
debug!("Sent ControlCommand event to UI");
|
||||
}
|
||||
AACPEvent::ConversationalAwareness(status) => {
|
||||
@@ -221,14 +228,14 @@ impl AirPodsDevice {
|
||||
}
|
||||
AACPEvent::OwnershipToFalseRequest => {
|
||||
info!("Received ownership to false request. Setting ownership to false and pausing media.");
|
||||
let _ = command_tx.send((ControlCommandIdentifiers::OwnsConnection, vec![0x00]));
|
||||
let _ = command_tx_clone.send((ControlCommandIdentifiers::OwnsConnection, vec![0x00]));
|
||||
let controller = mc_clone.lock().await;
|
||||
controller.pause_all_media().await;
|
||||
controller.deactivate_a2dp_profile().await;
|
||||
}
|
||||
_ => {
|
||||
debug!("Received unhandled AACP event: {:?}", event);
|
||||
let _ = ui_tx_clone.send(UIMessage::AACPUIEvent(mac_address.to_string(), event_clone));
|
||||
let _ = ui_tx_clone.send(BluetoothUIMessage::AACPUIEvent(mac_address.to_string(), event_clone));
|
||||
debug!("Sent unhandled AACP event to UI");
|
||||
}
|
||||
}
|
||||
@@ -240,6 +247,23 @@ impl AirPodsDevice {
|
||||
aacp_manager,
|
||||
// att_manager,
|
||||
media_controller,
|
||||
// command_tx: Some(command_tx.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AirPodsInformation {
|
||||
pub name: String,
|
||||
pub model_number: String,
|
||||
pub manufacturer: String,
|
||||
pub serial_number: String,
|
||||
pub version1: String,
|
||||
pub version2: String,
|
||||
pub hardware_revision: String,
|
||||
pub updater_identifier: String,
|
||||
pub left_serial_number: String,
|
||||
pub right_serial_number: String,
|
||||
pub version3: String,
|
||||
pub le_keys: AirPodsLEKeys
|
||||
}
|
||||
107
linux-rust/src/devices/enums.rs
Normal file
107
linux-rust/src/devices/enums.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
use std::fmt::Display;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::devices::airpods::AirPodsInformation;
|
||||
use crate::devices::nothing::NothingInformation;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(PartialEq)]
|
||||
pub enum DeviceType {
|
||||
AirPods,
|
||||
Nothing
|
||||
}
|
||||
|
||||
impl Display for DeviceType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
DeviceType::AirPods => write!(f, "AirPods"),
|
||||
DeviceType::Nothing => write!(f, "Nothing"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind", content = "data")]
|
||||
pub enum DeviceInformation {
|
||||
AirPods(AirPodsInformation),
|
||||
Nothing(NothingInformation)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DeviceData {
|
||||
pub name: String,
|
||||
pub type_: DeviceType,
|
||||
pub information: Option<DeviceInformation>,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum DeviceState {
|
||||
AirPods(AirPodsState),
|
||||
Nothing(NothingState),
|
||||
}
|
||||
|
||||
impl Display for DeviceState {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
DeviceState::AirPods(_) => write!(f, "AirPods State"),
|
||||
DeviceState::Nothing(_) => write!(f, "Nothing State"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AirPodsState {
|
||||
pub conversation_awareness_enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct NothingState {
|
||||
pub anc_mode: NothingAncMode,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum NothingAncMode {
|
||||
Off,
|
||||
LowNoiseCancellation,
|
||||
MidNoiseCancellation,
|
||||
HighNoiseCancellation,
|
||||
AdaptiveNoiseCancellation,
|
||||
Transparency
|
||||
}
|
||||
|
||||
impl Display for NothingAncMode {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
NothingAncMode::Off => write!(f, "Off"),
|
||||
NothingAncMode::LowNoiseCancellation => write!(f, "Low Noise Cancellation"),
|
||||
NothingAncMode::MidNoiseCancellation => write!(f, "Mid Noise Cancellation"),
|
||||
NothingAncMode::HighNoiseCancellation => write!(f, "High Noise Cancellation"),
|
||||
NothingAncMode::AdaptiveNoiseCancellation => write!(f, "Adaptive Noise Cancellation"),
|
||||
NothingAncMode::Transparency => write!(f, "Transparency"),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl NothingAncMode {
|
||||
pub fn from_byte(value: u8) -> Self {
|
||||
match value {
|
||||
0x03 => NothingAncMode::LowNoiseCancellation,
|
||||
0x02 => NothingAncMode::MidNoiseCancellation,
|
||||
0x01 => NothingAncMode::HighNoiseCancellation,
|
||||
0x04 => NothingAncMode::AdaptiveNoiseCancellation,
|
||||
0x07 => NothingAncMode::Transparency,
|
||||
0x05 => NothingAncMode::Off,
|
||||
_ => NothingAncMode::Off,
|
||||
}
|
||||
}
|
||||
pub fn to_byte(&self) -> u8 {
|
||||
match self {
|
||||
NothingAncMode::LowNoiseCancellation => 0x03,
|
||||
NothingAncMode::MidNoiseCancellation => 0x02,
|
||||
NothingAncMode::HighNoiseCancellation => 0x01,
|
||||
NothingAncMode::AdaptiveNoiseCancellation => 0x04,
|
||||
NothingAncMode::Transparency => 0x07,
|
||||
NothingAncMode::Off => 0x05,
|
||||
}
|
||||
}
|
||||
}
|
||||
3
linux-rust/src/devices/mod.rs
Normal file
3
linux-rust/src/devices/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod airpods;
|
||||
pub mod enums;
|
||||
pub(crate) mod nothing;
|
||||
167
linux-rust/src/devices/nothing.rs
Normal file
167
linux-rust/src/devices/nothing.rs
Normal file
@@ -0,0 +1,167 @@
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
use bluer::Address;
|
||||
use log::{debug, info};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::time::sleep;
|
||||
use crate::bluetooth::att::{ATTHandles, ATTManager};
|
||||
use crate::devices::enums::{DeviceData, DeviceInformation, DeviceType};
|
||||
use crate::ui::messages::BluetoothUIMessage;
|
||||
use crate::utils::get_devices_path;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct NothingInformation{
|
||||
pub serial_number: String,
|
||||
pub firmware_version: String
|
||||
}
|
||||
|
||||
pub struct NothingDevice{
|
||||
pub att_manager: ATTManager,
|
||||
pub information: NothingInformation
|
||||
}
|
||||
|
||||
impl NothingDevice{
|
||||
pub async fn new(
|
||||
mac_address: Address,
|
||||
ui_tx: mpsc::UnboundedSender<BluetoothUIMessage>
|
||||
) -> Self {
|
||||
let mut att_manager = ATTManager::new();
|
||||
att_manager.connect(mac_address).await.expect("Failed to connect");
|
||||
|
||||
let (tx, mut rx) = mpsc::unbounded_channel::<Vec<u8>>();
|
||||
|
||||
att_manager.register_listener(
|
||||
ATTHandles::NothingEverythingRead,
|
||||
tx
|
||||
).await;
|
||||
|
||||
let devices: HashMap<String, DeviceData> =
|
||||
std::fs::read_to_string(get_devices_path())
|
||||
.ok()
|
||||
.and_then(|s| serde_json::from_str(&s).ok())
|
||||
.unwrap_or_default();
|
||||
let device_key = mac_address.to_string();
|
||||
let information = if let Some(device_data) = devices.get(&device_key) {
|
||||
let info = device_data.information.clone();
|
||||
if let Some(DeviceInformation::Nothing(ref nothing_info)) = info {
|
||||
nothing_info.clone()
|
||||
} else {
|
||||
NothingInformation{
|
||||
serial_number: String::new(),
|
||||
firmware_version: String::new()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
NothingInformation{
|
||||
serial_number: String::new(),
|
||||
firmware_version: String::new()
|
||||
}
|
||||
};
|
||||
|
||||
// Request version information
|
||||
att_manager.write(
|
||||
ATTHandles::NothingEverything,
|
||||
&[
|
||||
0x55, 0x20,
|
||||
0x01, 0x42,
|
||||
0xC0, 0x00,
|
||||
0x00, 0x00,
|
||||
0x00, 0x00 // something, idk
|
||||
]
|
||||
).await.expect("Failed to write");
|
||||
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
|
||||
// Request serial number
|
||||
att_manager.write(
|
||||
ATTHandles::NothingEverything,
|
||||
&[
|
||||
0x55, 0x20,
|
||||
0x01, 0x06,
|
||||
0xC0, 0x00,
|
||||
0x00, 0x13,
|
||||
0x00, 0x00
|
||||
]
|
||||
).await.expect("Failed to write");
|
||||
|
||||
// let ui_tx_clone = ui_tx.clone();
|
||||
let information_l = information.clone();
|
||||
tokio::spawn(async move {
|
||||
while let Some(data) = rx.recv().await {
|
||||
if data.starts_with(&[
|
||||
0x55, 0x20,
|
||||
0x01, 0x42, 0x40
|
||||
]) {
|
||||
let firmware_version = String::from_utf8_lossy(&data[8..]).to_string();
|
||||
info!("Received firmware version from Nothing device {}: {}", mac_address, firmware_version);
|
||||
let new_information = NothingInformation{
|
||||
serial_number: information_l.serial_number.clone(),
|
||||
firmware_version: firmware_version.clone()
|
||||
};
|
||||
let mut new_devices = devices.clone();
|
||||
new_devices.insert(
|
||||
device_key.clone(),
|
||||
DeviceData{
|
||||
name: devices.get(&device_key)
|
||||
.map(|d| d.name.clone())
|
||||
.unwrap_or("Nothing Device".to_string()),
|
||||
type_: devices.get(&device_key)
|
||||
.map(|d| d.type_.clone())
|
||||
.unwrap_or(DeviceType::Nothing),
|
||||
information: Some(DeviceInformation::Nothing(new_information)),
|
||||
}
|
||||
);
|
||||
let json = serde_json::to_string(&new_devices).unwrap();
|
||||
std::fs::write(get_devices_path(), json).expect("Failed to write devices file");
|
||||
} else if data.starts_with(
|
||||
&[
|
||||
0x55, 0x20,
|
||||
0x01, 0x06, 0x40
|
||||
]
|
||||
) {
|
||||
let serial_number_start_position = data.iter().position(|&b| b == "S".as_bytes()[0]).unwrap_or(8);
|
||||
let serial_number_end = data.iter()
|
||||
.skip(serial_number_start_position)
|
||||
.position(|&b| b == 0x0A)
|
||||
.map(|pos| pos + serial_number_start_position)
|
||||
.unwrap_or(data.len());
|
||||
if data.get(serial_number_start_position + 1) == Some(&"H".as_bytes()[0]) {
|
||||
let serial_number = String::from_utf8_lossy(
|
||||
&data[serial_number_start_position..serial_number_end]
|
||||
).to_string();
|
||||
info!("Received serial number from Nothing device {}: {}", mac_address, serial_number);
|
||||
let new_information = NothingInformation{
|
||||
serial_number: serial_number.clone(),
|
||||
firmware_version: information_l.firmware_version.clone()
|
||||
};
|
||||
let mut new_devices = devices.clone();
|
||||
new_devices.insert(
|
||||
device_key.clone(),
|
||||
DeviceData{
|
||||
name: devices.get(&device_key)
|
||||
.map(|d| d.name.clone())
|
||||
.unwrap_or("Nothing Device".to_string()),
|
||||
type_: devices.get(&device_key)
|
||||
.map(|d| d.type_.clone())
|
||||
.unwrap_or(DeviceType::Nothing),
|
||||
information: Some(DeviceInformation::Nothing(new_information)),
|
||||
}
|
||||
);
|
||||
let json = serde_json::to_string(&new_devices).unwrap();
|
||||
std::fs::write(get_devices_path(), json).expect("Failed to write devices file");
|
||||
} else {
|
||||
debug!("Serial number format unexpected from Nothing device {}: {:?}", mac_address, data);
|
||||
}
|
||||
}
|
||||
else {}
|
||||
debug!("Received data from (Nothing) device {}, data: {:?}", mac_address, data);
|
||||
}
|
||||
});
|
||||
|
||||
NothingDevice{
|
||||
att_manager,
|
||||
information
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
mod bluetooth;
|
||||
mod airpods;
|
||||
mod media_controller;
|
||||
mod ui;
|
||||
mod utils;
|
||||
mod devices;
|
||||
|
||||
use std::env;
|
||||
use log::info;
|
||||
@@ -11,15 +11,20 @@ use dbus::blocking::stdintf::org_freedesktop_dbus::Properties;
|
||||
use dbus::message::MatchRule;
|
||||
use dbus::arg::{RefArg, Variant};
|
||||
use std::collections::HashMap;
|
||||
use crate::bluetooth::discovery::find_connected_airpods;
|
||||
use crate::airpods::AirPodsDevice;
|
||||
use std::sync::Arc;
|
||||
use crate::bluetooth::discovery::{find_connected_airpods, find_other_managed_devices};
|
||||
use devices::airpods::AirPodsDevice;
|
||||
use bluer::Address;
|
||||
use ksni::TrayMethods;
|
||||
use crate::ui::tray::MyTray;
|
||||
use clap::Parser;
|
||||
use crate::bluetooth::le::start_le_monitor;
|
||||
use tokio::sync::mpsc::unbounded_channel;
|
||||
use crate::ui::messages::UIMessage;
|
||||
use crate::bluetooth::att::ATTHandles;
|
||||
use crate::bluetooth::managers::BluetoothManager;
|
||||
use crate::devices::enums::DeviceData;
|
||||
use crate::ui::messages::{AirPodsCommand, BluetoothUIMessage, NothingCommand, UICommand};
|
||||
use crate::utils::get_devices_path;
|
||||
|
||||
#[derive(Parser)]
|
||||
struct Args {
|
||||
@@ -35,23 +40,48 @@ fn main() -> iced::Result {
|
||||
let args = Args::parse();
|
||||
let log_level = if args.debug { "debug" } else { "info" };
|
||||
if env::var("RUST_LOG").is_err() {
|
||||
unsafe { env::set_var("RUST_LOG", log_level.to_owned() + ",wgpu_core=off,librepods_rust::bluetooth::le=off,cosmic_text=off,naga=off,iced_winit=off") };
|
||||
unsafe { env::set_var("RUST_LOG", log_level.to_owned() + ",iced_wgpu=off,wgpu_hal=off,wgpu_core=off,librepods_rust::bluetooth::le=off,cosmic_text=off,naga=off,iced_winit=off") };
|
||||
}
|
||||
env_logger::init();
|
||||
|
||||
let (ui_tx, ui_rx) = unbounded_channel::<UIMessage>();
|
||||
let (ui_tx, ui_rx) = unbounded_channel::<BluetoothUIMessage>();
|
||||
let (ui_command_tx, ui_command_rx) = unbounded_channel::<UICommand>();
|
||||
|
||||
std::thread::spawn(|| {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
rt.block_on(async_main(ui_tx)).unwrap();
|
||||
rt.block_on(async_main(ui_tx, ui_command_rx)).unwrap();
|
||||
});
|
||||
|
||||
ui::window::start_ui(ui_rx, args.start_minimized)
|
||||
ui::window::start_ui(ui_rx, args.start_minimized, ui_command_tx)
|
||||
}
|
||||
|
||||
|
||||
async fn async_main(ui_tx: tokio::sync::mpsc::UnboundedSender<UIMessage>) -> bluer::Result<()> {
|
||||
async fn async_main(ui_tx: tokio::sync::mpsc::UnboundedSender<BluetoothUIMessage>, mut ui_command_rx: tokio::sync::mpsc::UnboundedReceiver<UICommand>) -> bluer::Result<()> {
|
||||
let args = Args::parse();
|
||||
|
||||
// let mut device_command_txs: HashMap<String, tokio::sync::mpsc::UnboundedSender<(ControlCommandIdentifiers, Vec<u8>)>> = HashMap::new();
|
||||
let mut device_managers: HashMap<String, Arc<BluetoothManager>> = HashMap::new();
|
||||
|
||||
let mut managed_devices_mac: Vec<String> = Vec::new(); // includes ony non-AirPods. AirPods handled separately.
|
||||
|
||||
let devices_path = get_devices_path();
|
||||
let devices_json = std::fs::read_to_string(&devices_path).unwrap_or_else(|e| {
|
||||
log::error!("Failed to read devices file: {}", e);
|
||||
"{}".to_string()
|
||||
});
|
||||
let devices_list: HashMap<String, DeviceData> = serde_json::from_str(&devices_json).unwrap_or_else(|e| {
|
||||
log::error!("Deserialization failed: {}", e);
|
||||
HashMap::new()
|
||||
});
|
||||
for (mac, device_data) in devices_list.iter() {
|
||||
match device_data.type_ {
|
||||
devices::enums::DeviceType::Nothing => {
|
||||
managed_devices_mac.push(mac.clone());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let tray_handle = if args.no_tray {
|
||||
None
|
||||
} else {
|
||||
@@ -93,16 +123,51 @@ async fn async_main(ui_tx: tokio::sync::mpsc::UnboundedSender<UIMessage>) -> blu
|
||||
let name = device.name().await?.unwrap_or_else(|| "Unknown".to_string());
|
||||
info!("Found connected AirPods: {}, initializing.", name);
|
||||
let ui_tx_clone = ui_tx.clone();
|
||||
ui_tx_clone.send(UIMessage::DeviceConnected(device.address().to_string())).unwrap();
|
||||
let _airpods_device = AirPodsDevice::new(device.address(), tray_handle.clone(), ui_tx_clone).await;
|
||||
ui_tx_clone.send(BluetoothUIMessage::DeviceConnected(device.address().to_string())).unwrap();
|
||||
let airpods_device = AirPodsDevice::new(device.address(), tray_handle.clone(), ui_tx_clone).await;
|
||||
// device_command_txs.insert(device.address().to_string(), airpods_device.command_tx.unwrap());
|
||||
// device_managers.insert(device.address().to_string(), Arc::new(airpods_device.aacp_manager));
|
||||
device_managers.insert(
|
||||
device.address().to_string(),
|
||||
Arc::from(BluetoothManager::AACP(Arc::new(airpods_device.aacp_manager))),
|
||||
);
|
||||
}
|
||||
Err(_) => {
|
||||
info!("No connected AirPods found.");
|
||||
}
|
||||
}
|
||||
|
||||
match find_other_managed_devices(&adapter, managed_devices_mac.clone()).await {
|
||||
Ok(devices) => {
|
||||
for device in devices {
|
||||
let addr_str = device.address().to_string();
|
||||
info!("Found connected managed device: {}, initializing.", addr_str);
|
||||
let type_ = devices_list.get(&addr_str).unwrap().type_.clone();
|
||||
let ui_tx_clone = ui_tx.clone();
|
||||
let mut device_managers = device_managers.clone();
|
||||
tokio::spawn(async move {
|
||||
ui_tx_clone.send(BluetoothUIMessage::DeviceConnected(addr_str.clone())).unwrap();
|
||||
match type_ {
|
||||
devices::enums::DeviceType::Nothing => {
|
||||
let dev = devices::nothing::NothingDevice::new(device.address(), ui_tx_clone).await;
|
||||
device_managers.insert(
|
||||
addr_str,
|
||||
Arc::from(BluetoothManager::ATT(Arc::new(dev.att_manager))),
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Error finding connected managed devices: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
let conn = Connection::new_system()?;
|
||||
let rule = MatchRule::new_signal("org.freedesktop.DBus.Properties", "PropertiesChanged");
|
||||
let device_managers_clone = device_managers.clone();
|
||||
conn.add_match(rule, move |_: (), conn, msg| {
|
||||
let Some(path) = msg.path() else { return true; };
|
||||
if !path.contains("/org/bluez/hci") || !path.contains("/dev_") {
|
||||
@@ -123,21 +188,117 @@ async fn async_main(ui_tx: tokio::sync::mpsc::UnboundedSender<UIMessage>) -> blu
|
||||
let proxy = conn.with_proxy("org.bluez", path, std::time::Duration::from_millis(5000));
|
||||
let Ok(uuids) = proxy.get::<Vec<String>>("org.bluez.Device1", "UUIDs") else { return true; };
|
||||
let target_uuid = "74ec2172-0bad-4d01-8f77-997b2be0722a";
|
||||
|
||||
let Ok(addr_str) = proxy.get::<String>("org.bluez.Device1", "Address") else { return true; };
|
||||
let Ok(addr) = addr_str.parse::<Address>() else { return true; };
|
||||
|
||||
if managed_devices_mac.contains(&addr_str) {
|
||||
info!("Managed device connected: {}, initializing", addr_str);
|
||||
let type_ = devices_list.get(&addr_str).unwrap().type_.clone();
|
||||
match type_ {
|
||||
devices::enums::DeviceType::Nothing => {
|
||||
let ui_tx_clone = ui_tx.clone();
|
||||
let mut device_managers = device_managers.clone();
|
||||
tokio::spawn(async move {
|
||||
ui_tx_clone.send(BluetoothUIMessage::DeviceConnected(addr_str.clone())).unwrap();
|
||||
let dev = devices::nothing::NothingDevice::new(addr, ui_tx_clone).await;
|
||||
device_managers.insert(
|
||||
addr_str,
|
||||
Arc::from(BluetoothManager::ATT(Arc::new(dev.att_manager))),
|
||||
);
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if !uuids.iter().any(|u| u.to_lowercase() == target_uuid) {
|
||||
return true;
|
||||
}
|
||||
let name = proxy.get::<String>("org.bluez.Device1", "Name").unwrap_or_else(|_| "Unknown".to_string());
|
||||
let Ok(addr_str) = proxy.get::<String>("org.bluez.Device1", "Address") else { return true; };
|
||||
let Ok(addr) = addr_str.parse::<Address>() else { return true; };
|
||||
info!("AirPods connected: {}, initializing", name);
|
||||
let handle_clone = tray_handle.clone();
|
||||
let ui_tx_clone = ui_tx.clone();
|
||||
let mut device_managers = device_managers.clone();
|
||||
tokio::spawn(async move {
|
||||
ui_tx_clone.send(UIMessage::DeviceConnected(addr_str)).unwrap();
|
||||
let _airpods_device = AirPodsDevice::new(addr, handle_clone, ui_tx_clone).await;
|
||||
ui_tx_clone.send(BluetoothUIMessage::DeviceConnected(addr_str.clone())).unwrap();
|
||||
let airpods_device = AirPodsDevice::new(addr, handle_clone, ui_tx_clone).await;
|
||||
device_managers.insert(
|
||||
addr_str,
|
||||
Arc::from(BluetoothManager::AACP(Arc::new(airpods_device.aacp_manager))),
|
||||
);
|
||||
});
|
||||
true
|
||||
})?;
|
||||
tokio::spawn(async move {
|
||||
while let Some(command) = ui_command_rx.recv().await {
|
||||
match command {
|
||||
UICommand::AirPods(AirPodsCommand::SetControlCommandStatus(mac, identifier, value)) => {
|
||||
if let Some(manager) = device_managers_clone.get(&mac) {
|
||||
match manager.as_ref() {
|
||||
BluetoothManager::AACP(manager) => {
|
||||
log::debug!("Sending control command to device {}: {:?} = {:?}", mac, identifier, value);
|
||||
if let Err(e) = manager.send_control_command(identifier, value.as_ref()).await {
|
||||
log::error!("Failed to send control command to device {}: {}", mac, e);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
log::warn!("AACP not available for {}", mac);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log::warn!("No manager for device {}", mac);
|
||||
}
|
||||
}
|
||||
UICommand::AirPods(AirPodsCommand::RenameDevice(mac, new_name)) => {
|
||||
if let Some(manager) = device_managers_clone.get(&mac) {
|
||||
match manager.as_ref() {
|
||||
BluetoothManager::AACP(manager) => {
|
||||
log::debug!("Renaming device {} to {}", mac, new_name);
|
||||
if let Err(e) = manager.send_rename_packet(&new_name).await {
|
||||
log::error!("Failed to rename device {}: {}", mac, e);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
log::warn!("AACP not available for {}", mac);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log::warn!("No manager for device {}", mac);
|
||||
}
|
||||
}
|
||||
UICommand::Nothing(NothingCommand::SetNoiseCancellationMode(mac, mode)) => {
|
||||
if let Some(manager) = device_managers_clone.get(&mac) {
|
||||
match manager.as_ref() {
|
||||
BluetoothManager::ATT(manager) => {
|
||||
log::debug!("Setting noise cancellation mode for device {}: {:?}", mac, mode);
|
||||
if let Err(e) = manager.write(
|
||||
ATTHandles::NothingEverything,
|
||||
&[
|
||||
0x55,
|
||||
0x60, 0x01,
|
||||
0x0F, 0xF0,
|
||||
0x03, 0x00,
|
||||
0x00, 0x01, // the 0x00 is an incremental counter, but it works without it
|
||||
mode.to_byte(), 0x00,
|
||||
0x00, 0x00 // these both bytes were something random, 0 works too
|
||||
]
|
||||
).await {
|
||||
log::error!("Failed to set noise cancellation mode for device {}: {}", mac, e);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
log::warn!("Nothing manager not available for {}", mac);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log::warn!("No manager for device {}", mac);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
info!("Listening for Bluetooth connections via D-Bus...");
|
||||
loop {
|
||||
|
||||
197
linux-rust/src/ui/airpods.rs
Normal file
197
linux-rust/src/ui/airpods.rs
Normal file
@@ -0,0 +1,197 @@
|
||||
use std::collections::HashMap;
|
||||
use iced::widget::{button, column, container, row, text, toggler, Space};
|
||||
use iced::{Background, Border, Color, Length, Theme};
|
||||
use iced::widget::button::Style;
|
||||
use log::error;
|
||||
use crate::devices::enums::{AirPodsState, DeviceData, DeviceInformation};
|
||||
use crate::ui::window::{DeviceMessage, Message};
|
||||
|
||||
pub fn airpods_view<'a>(
|
||||
mac: &str,
|
||||
devices_list: &HashMap<String, DeviceData>,
|
||||
state: &AirPodsState,
|
||||
) -> iced::widget::Container<'a, Message> {
|
||||
let mut information_col = column![];
|
||||
let mac = mac.to_string();
|
||||
if let Some(device) = devices_list.get(mac.as_str()) {
|
||||
if let Some(DeviceInformation::AirPods(ref airpods_info)) = device.information {
|
||||
information_col = information_col
|
||||
.push(text("Device Information").size(18).style(
|
||||
|theme: &Theme| {
|
||||
let mut style = text::Style::default();
|
||||
style.color = Some(theme.palette().primary);
|
||||
style
|
||||
}
|
||||
))
|
||||
.push(Space::with_height(Length::from(10)))
|
||||
.push(
|
||||
row![
|
||||
text("Model Number").size(16).style(
|
||||
|theme: &Theme| {
|
||||
let mut style = text::Style::default();
|
||||
style.color = Some(theme.palette().text);
|
||||
style
|
||||
}
|
||||
),
|
||||
Space::with_width(Length::Fill),
|
||||
text(airpods_info.model_number.clone()).size(16)
|
||||
]
|
||||
)
|
||||
.push(
|
||||
row![
|
||||
text("Manufacturer").size(16).style(
|
||||
|theme: &Theme| {
|
||||
let mut style = text::Style::default();
|
||||
style.color = Some(theme.palette().text);
|
||||
style
|
||||
}
|
||||
),
|
||||
Space::with_width(Length::Fill),
|
||||
text(airpods_info.manufacturer.clone()).size(16)
|
||||
]
|
||||
)
|
||||
.push(
|
||||
row![
|
||||
text("Serial Number").size(16).style(
|
||||
|theme: &Theme| {
|
||||
let mut style = text::Style::default();
|
||||
style.color = Some(theme.palette().text);
|
||||
style
|
||||
}
|
||||
),
|
||||
Space::with_width(Length::Fill),
|
||||
button(
|
||||
text(airpods_info.serial_number.clone()).size(16)
|
||||
)
|
||||
.style(
|
||||
|theme: &Theme, _status| {
|
||||
let mut style = Style::default();
|
||||
style.text_color = theme.palette().text;
|
||||
style.background = Some(Background::Color(Color::TRANSPARENT));
|
||||
style
|
||||
}
|
||||
)
|
||||
.padding(0)
|
||||
.on_press(Message::CopyToClipboard(airpods_info.serial_number.clone()))
|
||||
]
|
||||
)
|
||||
.push(
|
||||
row![
|
||||
text("Left Serial Number").size(16).style(
|
||||
|theme: &Theme| {
|
||||
let mut style = text::Style::default();
|
||||
style.color = Some(theme.palette().text);
|
||||
style
|
||||
}
|
||||
),
|
||||
Space::with_width(Length::Fill),
|
||||
button(
|
||||
text(airpods_info.left_serial_number.clone()).size(16)
|
||||
)
|
||||
.style(
|
||||
|theme: &Theme, _status| {
|
||||
let mut style = Style::default();
|
||||
style.text_color = theme.palette().text;
|
||||
style.background = Some(Background::Color(Color::TRANSPARENT));
|
||||
style
|
||||
}
|
||||
)
|
||||
.padding(0)
|
||||
.on_press(Message::CopyToClipboard(airpods_info.left_serial_number.clone()))
|
||||
]
|
||||
)
|
||||
.push(
|
||||
row![
|
||||
text("Right Serial Number").size(16).style(
|
||||
|theme: &Theme| {
|
||||
let mut style = text::Style::default();
|
||||
style.color = Some(theme.palette().text);
|
||||
style
|
||||
}
|
||||
),
|
||||
Space::with_width(Length::Fill),
|
||||
button(
|
||||
text(airpods_info.right_serial_number.clone()).size(16)
|
||||
)
|
||||
.style(
|
||||
|theme: &Theme, _status| {
|
||||
let mut style = Style::default();
|
||||
style.text_color = theme.palette().text;
|
||||
style.background = Some(Background::Color(Color::TRANSPARENT));
|
||||
style
|
||||
}
|
||||
)
|
||||
.padding(0)
|
||||
.on_press(Message::CopyToClipboard(airpods_info.right_serial_number.clone()))
|
||||
]
|
||||
)
|
||||
.push(
|
||||
row![
|
||||
text("Version 1").size(16).style(
|
||||
|theme: &Theme| {
|
||||
let mut style = text::Style::default();
|
||||
style.color = Some(theme.palette().text);
|
||||
style
|
||||
}
|
||||
),
|
||||
Space::with_width(Length::Fill),
|
||||
text(airpods_info.version1.clone()).size(16)
|
||||
]
|
||||
)
|
||||
.push(
|
||||
row![
|
||||
text("Version 2").size(16).style(
|
||||
|theme: &Theme| {
|
||||
let mut style = text::Style::default();
|
||||
style.color = Some(theme.palette().text);
|
||||
style
|
||||
}
|
||||
),
|
||||
Space::with_width(Length::Fill),
|
||||
text(airpods_info.version2.clone()).size(16)
|
||||
]
|
||||
)
|
||||
.push(
|
||||
row![
|
||||
text("Version 3").size(16).style(
|
||||
|theme: &Theme| {
|
||||
let mut style = text::Style::default();
|
||||
style.color = Some(theme.palette().text);
|
||||
style
|
||||
}
|
||||
),
|
||||
Space::with_width(Length::Fill),
|
||||
text(airpods_info.version3.clone()).size(16)
|
||||
]
|
||||
);
|
||||
} else {
|
||||
error!("Expected AirPodsInformation for device {}, got something else", mac);
|
||||
}
|
||||
}
|
||||
|
||||
let toggler_widget = toggler(state.conversation_awareness_enabled)
|
||||
.label("Conversation Awareness")
|
||||
.on_toggle(move |is_enabled| Message::DeviceMessage(mac.to_string(), DeviceMessage::ConversationAwarenessToggled(is_enabled)));
|
||||
|
||||
container(
|
||||
column![
|
||||
toggler_widget,
|
||||
Space::with_height(Length::from(10)),
|
||||
container(information_col)
|
||||
.style(
|
||||
|theme: &Theme| {
|
||||
let mut style = container::Style::default();
|
||||
style.background = Some(Background::Color(theme.palette().primary.scale_alpha(0.1)));
|
||||
let mut border = Border::default();
|
||||
border.color = theme.palette().text;
|
||||
style.border = border.rounded(20);
|
||||
style
|
||||
}
|
||||
)
|
||||
.padding(20)
|
||||
]
|
||||
)
|
||||
.padding(20)
|
||||
.center_x(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
}
|
||||
@@ -1,15 +1,30 @@
|
||||
use crate::bluetooth::aacp::{AACPEvent, ControlCommandIdentifiers};
|
||||
use crate::devices::enums::NothingAncMode;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum UIMessage {
|
||||
pub enum BluetoothUIMessage {
|
||||
OpenWindow,
|
||||
DeviceConnected(String),
|
||||
DeviceDisconnected(String),
|
||||
AACPUIEvent(String, AACPEvent),
|
||||
NoOp,
|
||||
DeviceConnected(String), // mac
|
||||
DeviceDisconnected(String), // mac
|
||||
AACPUIEvent(String, AACPEvent), // mac, event
|
||||
ATTNotification(String, u16, Vec<u8>), // mac, handle, data
|
||||
NoOp
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum UICommand {
|
||||
AirPods(AirPodsCommand),
|
||||
Nothing(NothingCommand),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum AirPodsCommand {
|
||||
SetControlCommandStatus(String, ControlCommandIdentifiers, Vec<u8>),
|
||||
}
|
||||
RenameDevice(String, String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum NothingCommand {
|
||||
SetNoiseCancellationMode(String, NothingAncMode),
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
pub mod tray;
|
||||
pub mod window;
|
||||
pub mod messages;
|
||||
pub mod messages;
|
||||
mod airpods;
|
||||
mod nothing;
|
||||
77
linux-rust/src/ui/nothing.rs
Normal file
77
linux-rust/src/ui/nothing.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
use std::collections::HashMap;
|
||||
use iced::{Background, Border, Length, Theme};
|
||||
use iced::widget::{container, text, column, row, Space, combo_box};
|
||||
use crate::devices::enums::{DeviceData, DeviceInformation, NothingState};
|
||||
use crate::ui::window::Message;
|
||||
|
||||
pub fn nothing_view<'a>(
|
||||
mac: &str,
|
||||
devices_list: &HashMap<String, DeviceData>,
|
||||
state: &NothingState
|
||||
) -> iced::widget::Container<'a, Message> {
|
||||
let mut information_col = iced::widget::column![];
|
||||
let mac = mac.to_string();
|
||||
if let Some(device) = devices_list.get(mac.as_str()) {
|
||||
if let Some(DeviceInformation::Nothing(ref nothing_info)) = device.information {
|
||||
information_col = information_col
|
||||
.push(text("Device Information").size(18).style(
|
||||
|theme: &Theme| {
|
||||
let mut style = text::Style::default();
|
||||
style.color = Some(theme.palette().primary);
|
||||
style
|
||||
}
|
||||
))
|
||||
.push(iced::widget::Space::with_height(iced::Length::from(10)))
|
||||
.push(
|
||||
iced::widget::row![
|
||||
text("Serial Number").size(16).style(
|
||||
|theme: &Theme| {
|
||||
let mut style = text::Style::default();
|
||||
style.color = Some(theme.palette().text);
|
||||
style
|
||||
}
|
||||
),
|
||||
iced::widget::Space::with_width(iced::Length::Fill),
|
||||
text(nothing_info.serial_number.clone()).size(16)
|
||||
]
|
||||
)
|
||||
.push(
|
||||
iced::widget::row![
|
||||
text("Firmware Version").size(16).style(
|
||||
|theme: &Theme| {
|
||||
let mut style = text::Style::default();
|
||||
style.color = Some(theme.palette().text);
|
||||
style
|
||||
}
|
||||
),
|
||||
iced::widget::Space::with_width(iced::Length::Fill),
|
||||
text(nothing_info.firmware_version.clone()).size(16)
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
container(
|
||||
column![
|
||||
row![
|
||||
text("Noise Control Mode").size(18),
|
||||
Space::with_width(Length::Fill),
|
||||
// combobox here
|
||||
],
|
||||
container(information_col)
|
||||
.style(
|
||||
|theme: &Theme| {
|
||||
let mut style = container::Style::default();
|
||||
style.background = Some(Background::Color(theme.palette().primary.scale_alpha(0.1)));
|
||||
let mut border = Border::default();
|
||||
border.color = theme.palette().text;
|
||||
style.border = border.rounded(20);
|
||||
style
|
||||
}
|
||||
)
|
||||
.padding(20)
|
||||
]
|
||||
)
|
||||
.padding(20)
|
||||
.center_x(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
}
|
||||
@@ -5,7 +5,7 @@ use ksni::{Icon, ToolTip};
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
|
||||
use crate::bluetooth::aacp::ControlCommandIdentifiers;
|
||||
use crate::ui::messages::UIMessage;
|
||||
use crate::ui::messages::BluetoothUIMessage;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct MyTray {
|
||||
@@ -20,7 +20,7 @@ pub(crate) struct MyTray {
|
||||
pub(crate) listening_mode: Option<u8>,
|
||||
pub(crate) allow_off_option: Option<u8>,
|
||||
pub(crate) command_tx: Option<UnboundedSender<(ControlCommandIdentifiers, Vec<u8>)>>,
|
||||
pub(crate) ui_tx: Option<UnboundedSender<UIMessage>>,
|
||||
pub(crate) ui_tx: Option<UnboundedSender<BluetoothUIMessage>>,
|
||||
}
|
||||
|
||||
impl ksni::Tray for MyTray {
|
||||
@@ -114,7 +114,7 @@ impl ksni::Tray for MyTray {
|
||||
icon_name: "window-new".into(),
|
||||
activate: Box::new(|this: &mut Self| {
|
||||
if let Some(tx) = &this.ui_tx {
|
||||
let _ = tx.send(UIMessage::OpenWindow);
|
||||
let _ = tx.send(BluetoothUIMessage::OpenWindow);
|
||||
}
|
||||
}),
|
||||
..Default::default()
|
||||
|
||||
@@ -1,23 +1,30 @@
|
||||
|
||||
use std::collections::HashMap;
|
||||
use iced::widget::button::Style;
|
||||
use iced::widget::{button, column, container, pane_grid, text, Space, combo_box, row, text_input};
|
||||
use iced::widget::{button, column, container, pane_grid, text, Space, combo_box, row, text_input, scrollable};
|
||||
use iced::{daemon, window, Background, Border, Center, Color, Element, Length, Size, Subscription, Task, Theme};
|
||||
use std::sync::Arc;
|
||||
use bluer::{Address, Session};
|
||||
use iced::border::Radius;
|
||||
use iced::overlay::menu;
|
||||
use log::{debug, error};
|
||||
use tokio::sync::mpsc::UnboundedReceiver;
|
||||
use tokio::sync::Mutex;
|
||||
use crate::bluetooth::aacp::{DeviceData, DeviceInformation, DeviceType};
|
||||
use crate::ui::messages::UIMessage;
|
||||
use crate::bluetooth::aacp::{AACPEvent};
|
||||
use crate::devices::enums::{AirPodsState, DeviceData, DeviceState, DeviceType, NothingAncMode, NothingState};
|
||||
use crate::ui::messages::{AirPodsCommand, BluetoothUIMessage, NothingCommand, UICommand};
|
||||
use crate::utils::{get_devices_path, get_app_settings_path, MyTheme};
|
||||
use crate::ui::airpods::airpods_view;
|
||||
use crate::ui::nothing::nothing_view;
|
||||
|
||||
pub fn start_ui(ui_rx: UnboundedReceiver<UIMessage>, start_minimized: bool) -> iced::Result {
|
||||
pub fn start_ui(
|
||||
ui_rx: UnboundedReceiver<BluetoothUIMessage>,
|
||||
start_minimized: bool,
|
||||
ui_command_tx: tokio::sync::mpsc::UnboundedSender<UICommand>,
|
||||
) -> iced::Result {
|
||||
daemon(App::title, App::update, App::view)
|
||||
.subscription(App::subscription)
|
||||
.theme(App::theme)
|
||||
.run_with(move || App::new(ui_rx, start_minimized))
|
||||
.run_with(move || App::new(ui_rx, start_minimized, ui_command_tx))
|
||||
}
|
||||
|
||||
pub struct App {
|
||||
@@ -26,8 +33,14 @@ pub struct App {
|
||||
selected_tab: Tab,
|
||||
theme_state: combo_box::State<MyTheme>,
|
||||
selected_theme: MyTheme,
|
||||
ui_rx: Arc<Mutex<UnboundedReceiver<UIMessage>>>,
|
||||
bluetooth_state: BluetoothState
|
||||
ui_rx: Arc<Mutex<UnboundedReceiver<BluetoothUIMessage>>>,
|
||||
bluetooth_state: BluetoothState,
|
||||
ui_command_tx: tokio::sync::mpsc::UnboundedSender<UICommand>,
|
||||
paired_devices: HashMap<String, Address>,
|
||||
device_states: HashMap<String, DeviceState>,
|
||||
pending_add_device: Option<(String, Address)>,
|
||||
device_type_state: combo_box::State<DeviceType>,
|
||||
selected_device_type: Option<DeviceType>,
|
||||
}
|
||||
|
||||
pub struct BluetoothState {
|
||||
@@ -42,6 +55,12 @@ impl BluetoothState {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum DeviceMessage {
|
||||
ConversationAwarenessToggled(bool),
|
||||
NothingAncModeSelected(NothingAncMode)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Message {
|
||||
WindowOpened(window::Id),
|
||||
@@ -50,13 +69,21 @@ pub enum Message {
|
||||
SelectTab(Tab),
|
||||
ThemeSelected(MyTheme),
|
||||
CopyToClipboard(String),
|
||||
UIMessage(UIMessage),
|
||||
BluetoothMessage(BluetoothUIMessage),
|
||||
DeviceMessage(String, DeviceMessage),
|
||||
ShowNewDialogTab,
|
||||
GotPairedDevices(HashMap<String, Address>),
|
||||
StartAddDevice(String, Address),
|
||||
SelectDeviceType(DeviceType),
|
||||
ConfirmAddDevice,
|
||||
CancelAddDevice,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum Tab {
|
||||
Device(String),
|
||||
Settings,
|
||||
AddDevice
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
@@ -65,9 +92,12 @@ pub enum Pane {
|
||||
Content,
|
||||
}
|
||||
|
||||
|
||||
impl App {
|
||||
pub fn new(ui_rx: UnboundedReceiver<UIMessage>, start_minimized: bool) -> (Self, Task<Message>) {
|
||||
pub fn new(
|
||||
ui_rx: UnboundedReceiver<BluetoothUIMessage>,
|
||||
start_minimized: bool,
|
||||
ui_command_tx: tokio::sync::mpsc::UnboundedSender<UICommand>,
|
||||
) -> (Self, Task<Message>) {
|
||||
let (mut panes, first_pane) = pane_grid::State::new(Pane::Sidebar);
|
||||
let split = panes.split(pane_grid::Axis::Vertical, first_pane, Pane::Content);
|
||||
panes.resize(split.unwrap().1, 0.2);
|
||||
@@ -99,6 +129,14 @@ impl App {
|
||||
|
||||
let bluetooth_state = BluetoothState::new();
|
||||
|
||||
// let dummy_device_state = DeviceState::AirPods(AirPodsState {
|
||||
// conversation_awareness_enabled: false,
|
||||
// });
|
||||
// let device_states = HashMap::from([
|
||||
// ("28:2D:7F:C2:05:5B".to_string(), dummy_device_state),
|
||||
// ]);
|
||||
|
||||
let device_states = HashMap::new();
|
||||
(
|
||||
Self {
|
||||
window,
|
||||
@@ -131,6 +169,14 @@ impl App {
|
||||
selected_theme,
|
||||
ui_rx,
|
||||
bluetooth_state,
|
||||
ui_command_tx,
|
||||
paired_devices: HashMap::new(),
|
||||
device_states,
|
||||
pending_add_device: None,
|
||||
device_type_state: combo_box::State::new(vec![
|
||||
DeviceType::Nothing
|
||||
]),
|
||||
selected_device_type: None,
|
||||
},
|
||||
Task::batch(vec![open_task, wait_task])
|
||||
)
|
||||
@@ -171,9 +217,35 @@ impl App {
|
||||
Message::CopyToClipboard(data) => {
|
||||
iced::clipboard::write(data)
|
||||
}
|
||||
Message::UIMessage(ui_message) => {
|
||||
Message::DeviceMessage(mac, device_msg) => {
|
||||
match device_msg {
|
||||
DeviceMessage::ConversationAwarenessToggled(is_enabled) => {
|
||||
if let Some(DeviceState::AirPods(state)) = self.device_states.get_mut(&mac) {
|
||||
state.conversation_awareness_enabled = is_enabled;
|
||||
let value = if is_enabled { 0x01 } else { 0x02 };
|
||||
let _ = self.ui_command_tx.send(UICommand::AirPods(AirPodsCommand::SetControlCommandStatus(
|
||||
mac,
|
||||
crate::bluetooth::aacp::ControlCommandIdentifiers::ConversationDetectConfig,
|
||||
vec![value],
|
||||
)));
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
DeviceMessage::NothingAncModeSelected(mode) => {
|
||||
if let Some(DeviceState::Nothing(state)) = self.device_states.get_mut(&mac) {
|
||||
state.anc_mode = mode.clone();
|
||||
let _ = self.ui_command_tx.send(UICommand::Nothing(NothingCommand::SetNoiseCancellationMode(
|
||||
mac,
|
||||
mode,
|
||||
)));
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::BluetoothMessage(ui_message) => {
|
||||
match ui_message {
|
||||
UIMessage::NoOp => {
|
||||
BluetoothUIMessage::NoOp => {
|
||||
let ui_rx = Arc::clone(&self.ui_rx);
|
||||
let wait_task = Task::perform(
|
||||
wait_for_message(ui_rx),
|
||||
@@ -181,7 +253,7 @@ impl App {
|
||||
);
|
||||
wait_task
|
||||
}
|
||||
UIMessage::OpenWindow => {
|
||||
BluetoothUIMessage::OpenWindow => {
|
||||
let ui_rx = Arc::clone(&self.ui_rx);
|
||||
let wait_task = Task::perform(
|
||||
wait_for_message(ui_rx),
|
||||
@@ -205,7 +277,7 @@ impl App {
|
||||
])
|
||||
}
|
||||
}
|
||||
UIMessage::DeviceConnected(mac) => {
|
||||
BluetoothUIMessage::DeviceConnected(mac) => {
|
||||
let ui_rx = Arc::clone(&self.ui_rx);
|
||||
let wait_task = Task::perform(
|
||||
wait_for_message(ui_rx),
|
||||
@@ -223,34 +295,151 @@ impl App {
|
||||
self.bluetooth_state.connected_devices.push(mac.clone());
|
||||
}
|
||||
|
||||
// self.device_states.insert(mac.clone(), DeviceState::AirPods(AirPodsState {
|
||||
// conversation_awareness_enabled: false,
|
||||
// }));
|
||||
|
||||
let type_ = {
|
||||
let devices_json = std::fs::read_to_string(get_devices_path()).unwrap_or_else(|e| {
|
||||
error!("Failed to read devices file: {}", e);
|
||||
"{}".to_string()
|
||||
});
|
||||
let devices_list: HashMap<String, DeviceData> = serde_json::from_str(&devices_json).unwrap_or_else(|e| {
|
||||
error!("Deserialization failed: {}", e);
|
||||
HashMap::new()
|
||||
});
|
||||
devices_list.get(&mac).map(|d| d.type_.clone())
|
||||
};
|
||||
match type_ {
|
||||
Some(DeviceType::AirPods) => {
|
||||
self.device_states.insert(mac.clone(), DeviceState::AirPods(AirPodsState {
|
||||
conversation_awareness_enabled: false,
|
||||
}));
|
||||
}
|
||||
Some(DeviceType::Nothing) => {
|
||||
self.device_states.insert(mac.clone(), DeviceState::Nothing(NothingState {
|
||||
anc_mode: NothingAncMode::Off,
|
||||
}));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Task::batch(vec![
|
||||
wait_task,
|
||||
])
|
||||
}
|
||||
UIMessage::DeviceDisconnected(mac) => {
|
||||
BluetoothUIMessage::DeviceDisconnected(mac) => {
|
||||
let ui_rx = Arc::clone(&self.ui_rx);
|
||||
let wait_task = Task::perform(
|
||||
wait_for_message(ui_rx),
|
||||
|msg| msg,
|
||||
);
|
||||
debug!("Device disconnected: {}", mac);
|
||||
|
||||
self.device_states.remove(&mac);
|
||||
Task::batch(vec![
|
||||
wait_task,
|
||||
])
|
||||
}
|
||||
UIMessage::AACPUIEvent(mac, event) => {
|
||||
BluetoothUIMessage::AACPUIEvent(mac, event) => {
|
||||
let ui_rx = Arc::clone(&self.ui_rx);
|
||||
let wait_task = Task::perform(
|
||||
wait_for_message(ui_rx),
|
||||
|msg| msg,
|
||||
);
|
||||
debug!("AACP UI Event for {}: {:?}", mac, event);
|
||||
match event {
|
||||
AACPEvent::ControlCommand(status) => {
|
||||
match status.identifier {
|
||||
crate::bluetooth::aacp::ControlCommandIdentifiers::ConversationDetectConfig => {
|
||||
let is_enabled = match status.value.as_slice() {
|
||||
[0x01] => true,
|
||||
[0x02] => false,
|
||||
_ => {
|
||||
error!("Unknown Conversation Detect Config value: {:?}", status.value);
|
||||
false
|
||||
}
|
||||
};
|
||||
if let Some(DeviceState::AirPods(state)) = self.device_states.get_mut(&mac) {
|
||||
state.conversation_awareness_enabled = is_enabled;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
debug!("Unhandled Control Command Status: {:?}", status);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Task::batch(vec![
|
||||
wait_task,
|
||||
])
|
||||
}
|
||||
BluetoothUIMessage::ATTNotification(mac, handle, value) => {
|
||||
debug!("ATT Notification for {}: handle=0x{:04X}, value={:?}", mac, handle, value);
|
||||
let ui_rx = Arc::clone(&self.ui_rx);
|
||||
let wait_task = Task::perform(
|
||||
wait_for_message(ui_rx),
|
||||
|msg| msg,
|
||||
);
|
||||
Task::batch(vec![
|
||||
wait_task,
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::ShowNewDialogTab => {
|
||||
debug!("switching to Add Device tab");
|
||||
self.selected_tab = Tab::AddDevice;
|
||||
Task::perform(load_paired_devices(), Message::GotPairedDevices)
|
||||
}
|
||||
Message::GotPairedDevices(map) => {
|
||||
self.paired_devices = map;
|
||||
Task::none()
|
||||
}
|
||||
Message::StartAddDevice(name, addr) => {
|
||||
self.pending_add_device = Some((name, addr));
|
||||
self.selected_device_type = None;
|
||||
Task::none()
|
||||
}
|
||||
Message::SelectDeviceType(device_type) => {
|
||||
self.selected_device_type = Some(device_type);
|
||||
Task::none()
|
||||
}
|
||||
Message::ConfirmAddDevice => {
|
||||
if let Some((name, addr)) = self.pending_add_device.take() {
|
||||
if let Some(type_) = self.selected_device_type.take() {
|
||||
let devices_path = get_devices_path();
|
||||
let devices_json = std::fs::read_to_string(&devices_path).unwrap_or_else(|e| {
|
||||
error!("Failed to read devices file: {}", e);
|
||||
"{}".to_string()
|
||||
});
|
||||
let mut devices_list: HashMap<String, DeviceData> = serde_json::from_str(&devices_json).unwrap_or_else(|e| {
|
||||
error!("Deserialization failed: {}", e);
|
||||
HashMap::new()
|
||||
});
|
||||
devices_list.insert(addr.to_string(), DeviceData {
|
||||
name,
|
||||
type_: type_.clone(),
|
||||
information: None
|
||||
});
|
||||
let updated_json = serde_json::to_string(&devices_list).unwrap_or_else(|e| {
|
||||
error!("Serialization failed: {}", e);
|
||||
"{}".to_string()
|
||||
});
|
||||
if let Err(e) = std::fs::write(&devices_path, updated_json) {
|
||||
error!("Failed to write devices file: {}", e);
|
||||
}
|
||||
self.selected_tab = Tab::Device(addr.to_string());
|
||||
}
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::CancelAddDevice => {
|
||||
self.pending_add_device = None;
|
||||
self.selected_device_type = None;
|
||||
Task::none()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -356,6 +545,33 @@ impl App {
|
||||
let settings = create_settings_button();
|
||||
|
||||
let content = column![
|
||||
row![
|
||||
text("Devices").size(18),
|
||||
Space::with_width(Length::Fill),
|
||||
button(
|
||||
container(text("+").size(18)).center_x(Length::Fill).center_y(Length::Fill)
|
||||
)
|
||||
.style(
|
||||
|theme: &Theme, _status| {
|
||||
let mut style = Style::default();
|
||||
style.text_color = theme.palette().text;
|
||||
style.background = Some(Background::Color(theme.palette().primary.scale_alpha(0.1)));
|
||||
style.border = Border {
|
||||
width: 1.0,
|
||||
color: theme.palette().primary.scale_alpha(0.1),
|
||||
radius: Radius::from(8.0),
|
||||
};
|
||||
style
|
||||
}
|
||||
)
|
||||
.padding(0)
|
||||
.width(Length::from(28))
|
||||
.height(Length::from(28))
|
||||
.on_press(Message::ShowNewDialogTab)
|
||||
]
|
||||
.align_y(Center)
|
||||
.padding(4),
|
||||
Space::with_height(Length::from(8)),
|
||||
devices,
|
||||
Space::with_height(Length::Fill),
|
||||
settings
|
||||
@@ -375,200 +591,38 @@ impl App {
|
||||
.center_x(Length::Fill)
|
||||
.center_y(Length::Fill)
|
||||
} else {
|
||||
let mut information_col = column![];
|
||||
|
||||
let device_type = devices_list.get(id)
|
||||
.map(|d| d.type_.clone()).unwrap();
|
||||
|
||||
if device_type == DeviceType::AirPods {
|
||||
let device_information = devices_list.get(id)
|
||||
.and_then(|d| d.information.clone());
|
||||
match device_information {
|
||||
Some(DeviceInformation::AirPods(ref airpods_information)) => {
|
||||
information_col = information_col
|
||||
.push(text("Device Information").size(18).style(
|
||||
|theme: &Theme| {
|
||||
let mut style = text::Style::default();
|
||||
style.color = Some(theme.palette().primary);
|
||||
style
|
||||
}
|
||||
))
|
||||
.push(Space::with_height(Length::from(10)))
|
||||
.push(
|
||||
row![
|
||||
text("Model Number").size(16).style(
|
||||
|theme: &Theme| {
|
||||
let mut style = text::Style::default();
|
||||
style.color = Some(theme.palette().text);
|
||||
style
|
||||
}
|
||||
),
|
||||
Space::with_width(Length::Fill),
|
||||
text(airpods_information.model_number.clone()).size(16)
|
||||
]
|
||||
)
|
||||
.push(
|
||||
row![
|
||||
text("Manufacturer").size(16).style(
|
||||
|theme: &Theme| {
|
||||
let mut style = text::Style::default();
|
||||
style.color = Some(theme.palette().text);
|
||||
style
|
||||
}
|
||||
),
|
||||
Space::with_width(Length::Fill),
|
||||
text(airpods_information.manufacturer.clone()).size(16)
|
||||
]
|
||||
)
|
||||
.push(
|
||||
row![
|
||||
text("Serial Number").size(16).style(
|
||||
|theme: &Theme| {
|
||||
let mut style = text::Style::default();
|
||||
style.color = Some(theme.palette().text);
|
||||
style
|
||||
}
|
||||
),
|
||||
Space::with_width(Length::Fill),
|
||||
button(
|
||||
text(
|
||||
airpods_information.serial_number.clone()
|
||||
)
|
||||
.size(16)
|
||||
)
|
||||
.style(
|
||||
|theme: &Theme, _status| {
|
||||
let mut style = Style::default();
|
||||
style.text_color = theme.palette().text;
|
||||
style.background = Some(Background::Color(Color::TRANSPARENT));
|
||||
style
|
||||
}
|
||||
)
|
||||
.padding(0)
|
||||
.on_press(Message::CopyToClipboard(airpods_information.serial_number.clone()))
|
||||
]
|
||||
)
|
||||
.push(
|
||||
row![
|
||||
text("Left Serial Number").size(16).style(
|
||||
|theme: &Theme| {
|
||||
let mut style = text::Style::default();
|
||||
style.color = Some(theme.palette().text);
|
||||
style
|
||||
}
|
||||
),
|
||||
Space::with_width(Length::Fill),
|
||||
button(
|
||||
text(
|
||||
airpods_information.left_serial_number.clone()
|
||||
)
|
||||
.size(16)
|
||||
)
|
||||
.style(
|
||||
|theme: &Theme, _status| {
|
||||
let mut style = Style::default();
|
||||
style.text_color = theme.palette().text;
|
||||
style.background = Some(Background::Color(Color::TRANSPARENT));
|
||||
style
|
||||
}
|
||||
)
|
||||
.padding(0)
|
||||
.on_press(Message::CopyToClipboard(airpods_information.left_serial_number.clone()))
|
||||
]
|
||||
)
|
||||
.push(
|
||||
row![
|
||||
text("Right Serial Number").size(16).style(
|
||||
|theme: &Theme| {
|
||||
let mut style = text::Style::default();
|
||||
style.color = Some(theme.palette().text);
|
||||
style
|
||||
}
|
||||
),
|
||||
Space::with_width(Length::Fill),
|
||||
button(
|
||||
text(
|
||||
airpods_information.right_serial_number.clone()
|
||||
)
|
||||
.size(16)
|
||||
)
|
||||
.style(
|
||||
|theme: &Theme, _status| {
|
||||
let mut style = Style::default();
|
||||
style.text_color = theme.palette().text;
|
||||
style.background = Some(Background::Color(Color::TRANSPARENT));
|
||||
style
|
||||
}
|
||||
)
|
||||
.padding(0)
|
||||
.on_press(Message::CopyToClipboard(airpods_information.right_serial_number.clone()))
|
||||
]
|
||||
)
|
||||
.push(
|
||||
row![
|
||||
text("Version 1").size(16).style(
|
||||
|theme: &Theme| {
|
||||
let mut style = text::Style::default();
|
||||
style.color = Some(theme.palette().text);
|
||||
style
|
||||
}
|
||||
),
|
||||
Space::with_width(Length::Fill),
|
||||
text(airpods_information.version1.clone()).size(16)
|
||||
]
|
||||
)
|
||||
.push(
|
||||
row![
|
||||
text("Version 2").size(16).style(
|
||||
|theme: &Theme| {
|
||||
let mut style = text::Style::default();
|
||||
style.color = Some(theme.palette().text);
|
||||
style
|
||||
}
|
||||
),
|
||||
Space::with_width(Length::Fill),
|
||||
text(airpods_information.version2.clone()).size(16)
|
||||
]
|
||||
)
|
||||
.push(
|
||||
row![
|
||||
text("Version 3").size(16).style(
|
||||
|theme: &Theme| {
|
||||
let mut style = text::Style::default();
|
||||
style.color = Some(theme.palette().text);
|
||||
style
|
||||
}
|
||||
),
|
||||
Space::with_width(Length::Fill),
|
||||
text(airpods_information.version3.clone()).size(16)
|
||||
]
|
||||
);
|
||||
debug!("AirPods Information: {:?}", airpods_information);
|
||||
let device_type = devices_list.get(id).map(|d| d.type_.clone());
|
||||
let device_state = self.device_states.get(id);
|
||||
debug!("Rendering device view for {}: type={:?}, state={:?}", id, device_type, device_state);
|
||||
match device_type {
|
||||
Some(DeviceType::AirPods) => {
|
||||
if let Some(DeviceState::AirPods(state)) = device_state {
|
||||
airpods_view(id, &devices_list, state)
|
||||
} else {
|
||||
container(
|
||||
text("No state available for this AirPods device").size(16)
|
||||
)
|
||||
.center_x(Length::Fill)
|
||||
.center_y(Length::Fill)
|
||||
}
|
||||
_ => {
|
||||
error!("Expected AirPodsInformation, got something else: {:?}", device_information);
|
||||
},
|
||||
}
|
||||
Some(DeviceType::Nothing) => {
|
||||
if let Some(DeviceState::Nothing(state)) = device_state {
|
||||
nothing_view(id, &devices_list, state)
|
||||
} else {
|
||||
container(
|
||||
text("No state available for this Nothing device").size(16)
|
||||
)
|
||||
.center_x(Length::Fill)
|
||||
.center_y(Length::Fill)
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
container(text("Unsupported device").size(16))
|
||||
.center_x(Length::Fill)
|
||||
.center_y(Length::Fill)
|
||||
}
|
||||
}
|
||||
container(
|
||||
column![
|
||||
container(information_col)
|
||||
.style(
|
||||
|theme: &Theme| {
|
||||
let mut style = container::Style::default();
|
||||
style.background = Some(Background::Color(theme.palette().primary.scale_alpha(0.1)));
|
||||
let mut border = Border::default();
|
||||
border.color = theme.palette().text;
|
||||
style.border = border.rounded(20);
|
||||
style
|
||||
}
|
||||
)
|
||||
.padding(20)
|
||||
]
|
||||
)
|
||||
.padding(20)
|
||||
.center_x(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
}
|
||||
}
|
||||
Tab::Settings => {
|
||||
@@ -579,7 +633,7 @@ impl App {
|
||||
row![
|
||||
text("Theme:")
|
||||
.size(16),
|
||||
Space::with_width(Length::from(10)),
|
||||
Space::with_width(Length::Fill),
|
||||
combo_box(
|
||||
&self.theme_state,
|
||||
"Select theme",
|
||||
@@ -591,9 +645,9 @@ impl App {
|
||||
text_input::Style {
|
||||
background: Background::Color(Color::TRANSPARENT),
|
||||
border: Border {
|
||||
width: 0.5,
|
||||
width: 1.0,
|
||||
color: theme.palette().text,
|
||||
radius: Radius::from(10.0),
|
||||
radius: Radius::from(8.0),
|
||||
},
|
||||
icon: Default::default(),
|
||||
placeholder: theme.palette().text.scale_alpha(0.5),
|
||||
@@ -607,9 +661,9 @@ impl App {
|
||||
menu::Style {
|
||||
background: Background::Color(Color::TRANSPARENT),
|
||||
border: Border {
|
||||
width: 0.5,
|
||||
width: 1.0,
|
||||
color: theme.palette().text,
|
||||
radius: Radius::from(10.0)
|
||||
radius: Radius::from(8.0)
|
||||
},
|
||||
text_color: theme.palette().text,
|
||||
selected_text_color: theme.palette().text,
|
||||
@@ -617,7 +671,7 @@ impl App {
|
||||
}
|
||||
}
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.width(Length::from(350))
|
||||
]
|
||||
.align_y(Center)
|
||||
]
|
||||
@@ -626,6 +680,162 @@ impl App {
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
},
|
||||
Tab::AddDevice => {
|
||||
container(
|
||||
column![
|
||||
text("Pick a paired device to add:").size(18),
|
||||
Space::with_height(Length::from(10)),
|
||||
{
|
||||
let mut list_col = column![].spacing(12);
|
||||
for device in self.paired_devices.clone() {
|
||||
if !devices_list.contains_key(&device.1.to_string()) {
|
||||
let mut item_col = column![].spacing(8);
|
||||
let mut row_elements = vec![
|
||||
column![
|
||||
text(device.0.to_string()).size(16),
|
||||
text(device.1.to_string()).size(12)
|
||||
].into(),
|
||||
Space::with_width(Length::Fill).into(),
|
||||
];
|
||||
// Only show "Add" button if this device is not the pending one
|
||||
if !matches!(&self.pending_add_device, Some((_, addr)) if addr == &device.1) {
|
||||
row_elements.push(
|
||||
button(
|
||||
text("Add").size(14).width(120).align_y(Center).align_x(Center)
|
||||
)
|
||||
.style(
|
||||
|theme: &Theme, _status| {
|
||||
let mut style = Style::default();
|
||||
style.text_color = theme.palette().text;
|
||||
style.background = Some(Background::Color(theme.palette().primary.scale_alpha(0.5)));
|
||||
style.border = Border {
|
||||
width: 1.0,
|
||||
color: theme.palette().primary,
|
||||
radius: Radius::from(8.0),
|
||||
};
|
||||
style
|
||||
}
|
||||
)
|
||||
.padding(8)
|
||||
.on_press(Message::StartAddDevice(device.0.clone(), device.1.clone()))
|
||||
.into()
|
||||
);
|
||||
}
|
||||
item_col = item_col.push(row(row_elements).align_y(Center));
|
||||
|
||||
if let Some((_, pending_addr)) = &self.pending_add_device {
|
||||
if pending_addr == &device.1 {
|
||||
item_col = item_col.push(
|
||||
row![
|
||||
text("Device Type:").size(16),
|
||||
Space::with_width(Length::Fill),
|
||||
combo_box(
|
||||
&self.device_type_state,
|
||||
"Select device type",
|
||||
self.selected_device_type.as_ref(),
|
||||
Message::SelectDeviceType
|
||||
)
|
||||
.input_style(
|
||||
|theme: &Theme, _status| {
|
||||
text_input::Style {
|
||||
background: Background::Color(theme.palette().background),
|
||||
border: Border {
|
||||
width: 1.0,
|
||||
color: theme.palette().text,
|
||||
radius: Radius::from(8.0),
|
||||
},
|
||||
icon: Default::default(),
|
||||
placeholder: theme.palette().text.scale_alpha(0.5),
|
||||
value: theme.palette().text,
|
||||
selection: theme.palette().primary
|
||||
}
|
||||
}
|
||||
)
|
||||
.menu_style(
|
||||
|theme: &Theme| {
|
||||
menu::Style {
|
||||
background: Background::Color(theme.palette().background),
|
||||
border: Border {
|
||||
width: 1.0,
|
||||
color: theme.palette().text,
|
||||
radius: Radius::from(8.0)
|
||||
},
|
||||
text_color: theme.palette().text,
|
||||
selected_text_color: theme.palette().text,
|
||||
selected_background: Background::Color(theme.palette().primary.scale_alpha(0.3)),
|
||||
}
|
||||
}
|
||||
)
|
||||
.width(Length::from(200))
|
||||
]
|
||||
);
|
||||
item_col = item_col.push(
|
||||
row![
|
||||
Space::with_width(Length::Fill),
|
||||
button(text("Cancel").size(16).width(Length::Fill).center())
|
||||
.on_press(Message::CancelAddDevice)
|
||||
.style(|theme: &Theme, _status| {
|
||||
let mut style = Style::default();
|
||||
style.background = Some(Background::Color(theme.palette().primary.scale_alpha(0.1)));
|
||||
style.text_color = theme.palette().text;
|
||||
style.border = Border::default().rounded(8.0);
|
||||
style
|
||||
})
|
||||
.width(Length::from(120))
|
||||
.padding(4),
|
||||
Space::with_width(Length::from(20)),
|
||||
button(text("Add Device").size(16).width(Length::Fill).center())
|
||||
.on_press(Message::ConfirmAddDevice)
|
||||
.style(|theme: &Theme, _status| {
|
||||
let mut style = Style::default();
|
||||
style.background = Some(Background::Color(theme.palette().primary.scale_alpha(0.3)));
|
||||
style.text_color = theme.palette().text;
|
||||
style.border = Border::default().rounded(8.0);
|
||||
style
|
||||
})
|
||||
.width(Length::from(120))
|
||||
.padding(4),
|
||||
]
|
||||
.align_y(Center)
|
||||
.width(Length::Fill)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
list_col = list_col.push(
|
||||
container(item_col)
|
||||
.padding(8)
|
||||
.style(
|
||||
|theme: &Theme| {
|
||||
let mut style = container::Style::default();
|
||||
style.background = Some(Background::Color(theme.palette().primary.scale_alpha(0.1)));
|
||||
let mut border = Border::default();
|
||||
border.color = theme.palette().text;
|
||||
style.border = border.rounded(8);
|
||||
style
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
if self.paired_devices.iter().all(|device| devices_list.contains_key(&device.1.to_string())) && self.pending_add_device.is_none() {
|
||||
list_col = list_col.push(
|
||||
container(
|
||||
text("No new paired devices found. All paired devices are already added.").size(16)
|
||||
)
|
||||
.width(Length::Fill)
|
||||
);
|
||||
}
|
||||
scrollable(list_col)
|
||||
.height(Length::Fill)
|
||||
.width(Length::Fill)
|
||||
}
|
||||
]
|
||||
)
|
||||
.padding(20)
|
||||
.height(Length::Fill)
|
||||
.width(Length::Fill)
|
||||
}
|
||||
};
|
||||
|
||||
pane_grid::Content::new(content)
|
||||
@@ -649,14 +859,31 @@ impl App {
|
||||
}
|
||||
|
||||
async fn wait_for_message(
|
||||
ui_rx: Arc<Mutex<UnboundedReceiver<UIMessage>>>,
|
||||
ui_rx: Arc<Mutex<UnboundedReceiver<BluetoothUIMessage>>>,
|
||||
) -> Message {
|
||||
let mut rx = ui_rx.lock().await;
|
||||
match rx.recv().await {
|
||||
Some(msg) => Message::UIMessage(msg),
|
||||
Some(msg) => Message::BluetoothMessage(msg),
|
||||
None => {
|
||||
error!("UI message channel closed");
|
||||
Message::UIMessage(UIMessage::NoOp)
|
||||
Message::BluetoothMessage(BluetoothUIMessage::NoOp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
async fn load_paired_devices() -> HashMap<String, Address> {
|
||||
let mut devices = HashMap::new();
|
||||
|
||||
let session = Session::new().await.ok().unwrap();
|
||||
let adapter = session.default_adapter().await.ok().unwrap();
|
||||
let addresses = adapter.device_addresses().await.ok().unwrap();
|
||||
for addr in addresses {
|
||||
let device = adapter.device(addr.clone()).ok().unwrap();
|
||||
let paired = device.is_paired().await.ok().unwrap();
|
||||
if paired {
|
||||
let name = device.name().await.ok().flatten().unwrap_or_else(|| "Unknown".to_string());
|
||||
devices.insert(name, addr);
|
||||
}
|
||||
}
|
||||
|
||||
devices
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user