linux-rust: add skeleton for other devices

This commit is contained in:
Kavish Devar
2025-11-07 01:57:14 +05:30
parent 934df2419a
commit a2cda688d4
20 changed files with 1449 additions and 338 deletions

68
linux-rust/Cargo.lock generated
View File

@@ -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"

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 KiB

View File

@@ -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.");
}
}
}
}
}
}

View File

@@ -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) => {

View File

@@ -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")))
}

View File

@@ -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());
}

View 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));
}
}

View File

@@ -1,4 +1,5 @@
pub(crate) mod discovery;
pub mod aacp;
pub mod att;
pub mod le;
pub mod le;
pub mod managers;

View File

@@ -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
}

View 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,
}
}
}

View File

@@ -0,0 +1,3 @@
pub mod airpods;
pub mod enums;
pub(crate) mod nothing;

View 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
}
}
}

View File

@@ -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 {

View 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)
}

View File

@@ -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),
}

View File

@@ -1,3 +1,5 @@
pub mod tray;
pub mod window;
pub mod messages;
pub mod messages;
mod airpods;
mod nothing;

View 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)
}

View File

@@ -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()

View File

@@ -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
}