mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-01-29 14:20:48 +00:00
Compare commits
8 Commits
linux-v0.1
...
linux/rust
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c852b726de | ||
|
|
902b12a227 | ||
|
|
6ded8ff3ff | ||
|
|
376c54247b | ||
|
|
e2d17b8bae | ||
|
|
6f0323ee6b | ||
|
|
4737cbfc2c | ||
|
|
093554da07 |
7
.gitignore
vendored
7
.gitignore
vendored
@@ -659,3 +659,10 @@ obj/
|
|||||||
|
|
||||||
# End of https://www.toptal.com/developers/gitignore/api/qt,c++,clion,kotlin,python,android,pycharm,androidstudio,visualstudiocode,linux
|
# End of https://www.toptal.com/developers/gitignore/api/qt,c++,clion,kotlin,python,android,pycharm,androidstudio,visualstudiocode,linux
|
||||||
linux/.qmlls.ini
|
linux/.qmlls.ini
|
||||||
|
|
||||||
|
# Nix
|
||||||
|
result
|
||||||
|
result-*
|
||||||
|
|
||||||
|
# direnv
|
||||||
|
.direnv
|
||||||
|
|||||||
12
default.nix
Normal file
12
default.nix
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
(import (
|
||||||
|
let
|
||||||
|
lock = builtins.fromJSON (builtins.readFile ./flake.lock);
|
||||||
|
nodeName = lock.nodes.root.inputs.flake-compat;
|
||||||
|
in
|
||||||
|
fetchTarball {
|
||||||
|
url =
|
||||||
|
lock.nodes.${nodeName}.locked.url
|
||||||
|
or "https://github.com/edolstra/flake-compat/archive/${lock.nodes.${nodeName}.locked.rev}.tar.gz";
|
||||||
|
sha256 = lock.nodes.${nodeName}.locked.narHash;
|
||||||
|
}
|
||||||
|
) { src = ./.; }).defaultNix
|
||||||
143
flake.lock
generated
Normal file
143
flake.lock
generated
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"crane": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1765145449,
|
||||||
|
"narHash": "sha256-aBVHGWWRzSpfL++LubA0CwOOQ64WNLegrYHwsVuVN7A=",
|
||||||
|
"owner": "ipetkov",
|
||||||
|
"repo": "crane",
|
||||||
|
"rev": "69f538cdce5955fcd47abfed4395dc6d5194c1c5",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "ipetkov",
|
||||||
|
"repo": "crane",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-compat": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1733328505,
|
||||||
|
"narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=",
|
||||||
|
"rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec",
|
||||||
|
"revCount": 69,
|
||||||
|
"type": "tarball",
|
||||||
|
"url": "https://api.flakehub.com/f/pinned/edolstra/flake-compat/1.1.0/01948eb7-9cba-704f-bbf3-3fa956735b52/source.tar.gz"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"type": "tarball",
|
||||||
|
"url": "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-parts": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs-lib": "nixpkgs-lib"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1765495779,
|
||||||
|
"narHash": "sha256-MhA7wmo/7uogLxiewwRRmIax70g6q1U/YemqTGoFHlM=",
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "flake-parts",
|
||||||
|
"rev": "5635c32d666a59ec9a55cab87e898889869f7b71",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "flake-parts",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1765425892,
|
||||||
|
"narHash": "sha256-jlQpSkg2sK6IJVzTQBDyRxQZgKADC2HKMRfGCSgNMHo=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "5d6bdbddb4695a62f0d00a3620b37a15275a5093",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixpkgs-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs-lib": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1761765539,
|
||||||
|
"narHash": "sha256-b0yj6kfvO8ApcSE+QmA6mUfu8IYG6/uU28OFn4PaC8M=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "nixpkgs.lib",
|
||||||
|
"rev": "719359f4562934ae99f5443f20aa06c2ffff91fc",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "nixpkgs.lib",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs_2": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1761236834,
|
||||||
|
"narHash": "sha256-+pthv6hrL5VLW2UqPdISGuLiUZ6SnAXdd2DdUE+fV2Q=",
|
||||||
|
"owner": "nixos",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "d5faa84122bc0a1fd5d378492efce4e289f8eac1",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nixos",
|
||||||
|
"ref": "nixpkgs-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"crane": "crane",
|
||||||
|
"flake-compat": "flake-compat",
|
||||||
|
"flake-parts": "flake-parts",
|
||||||
|
"nixpkgs": "nixpkgs",
|
||||||
|
"systems": "systems",
|
||||||
|
"treefmt-nix": "treefmt-nix"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"treefmt-nix": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs_2"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1762938485,
|
||||||
|
"narHash": "sha256-AlEObg0syDl+Spi4LsZIBrjw+snSVU4T8MOeuZJUJjM=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "treefmt-nix",
|
||||||
|
"rev": "5b4ee75aeefd1e2d5a1cc43cf6ba65eba75e83e4",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "treefmt-nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
141
flake.nix
Normal file
141
flake.nix
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
{
|
||||||
|
description = "AirPods liberated from Apple's ecosystem";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||||
|
crane.url = "github:ipetkov/crane";
|
||||||
|
flake-parts.url = "github:hercules-ci/flake-parts";
|
||||||
|
flake-compat.url = "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz";
|
||||||
|
systems.url = "github:nix-systems/default";
|
||||||
|
treefmt-nix.url = "github:numtide/treefmt-nix";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs =
|
||||||
|
inputs@{
|
||||||
|
self,
|
||||||
|
crane,
|
||||||
|
flake-parts,
|
||||||
|
systems,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
flake-parts.lib.mkFlake { inherit inputs; } {
|
||||||
|
systems = import systems;
|
||||||
|
imports = [
|
||||||
|
inputs.treefmt-nix.flakeModule
|
||||||
|
];
|
||||||
|
|
||||||
|
perSystem =
|
||||||
|
{
|
||||||
|
self',
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
buildInputs =
|
||||||
|
with pkgs;
|
||||||
|
[
|
||||||
|
dbus
|
||||||
|
libpulseaudio
|
||||||
|
alsa-lib
|
||||||
|
bluez
|
||||||
|
|
||||||
|
# https://github.com/max-privatevoid/iced/blob/master/DEPENDENCIES.md
|
||||||
|
expat
|
||||||
|
fontconfig
|
||||||
|
freetype
|
||||||
|
freetype.dev
|
||||||
|
libGL
|
||||||
|
pkg-config
|
||||||
|
xorg.libX11
|
||||||
|
xorg.libXcursor
|
||||||
|
xorg.libXi
|
||||||
|
xorg.libXrandr
|
||||||
|
wayland
|
||||||
|
libxkbcommon
|
||||||
|
vulkan-loader
|
||||||
|
]
|
||||||
|
++ pkgs.lib.optionals pkgs.stdenv.isDarwin [
|
||||||
|
pkgs.libiconv
|
||||||
|
];
|
||||||
|
|
||||||
|
nativeBuildInputs = with pkgs; [
|
||||||
|
pkg-config
|
||||||
|
makeWrapper
|
||||||
|
];
|
||||||
|
|
||||||
|
craneLib = crane.mkLib pkgs;
|
||||||
|
unfilteredRoot = ./linux-rust/.;
|
||||||
|
src = lib.fileset.toSource {
|
||||||
|
root = unfilteredRoot;
|
||||||
|
fileset = lib.fileset.unions [
|
||||||
|
# Default files from crane (Rust and cargo files)
|
||||||
|
(craneLib.fileset.commonCargoSources unfilteredRoot)
|
||||||
|
(lib.fileset.maybeMissing ./linux-rust/assets/font)
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
commonArgs = {
|
||||||
|
inherit buildInputs nativeBuildInputs src;
|
||||||
|
strictDeps = true;
|
||||||
|
|
||||||
|
# RUST_BACKTRACE = "1";
|
||||||
|
};
|
||||||
|
|
||||||
|
librepods = craneLib.buildPackage (
|
||||||
|
commonArgs
|
||||||
|
// {
|
||||||
|
cargoArtifacts = craneLib.buildDepsOnly commonArgs;
|
||||||
|
|
||||||
|
doCheck = false;
|
||||||
|
|
||||||
|
# Wrap the binary after build to set runtime library path
|
||||||
|
postInstall = ''
|
||||||
|
wrapProgram $out/bin/librepods \
|
||||||
|
--prefix LD_LIBRARY_PATH : ${lib.makeLibraryPath buildInputs}
|
||||||
|
'';
|
||||||
|
|
||||||
|
meta = {
|
||||||
|
description = "AirPods liberated from Apple's ecosystem";
|
||||||
|
homepage = "https://github.com/kavishdevar/librepods";
|
||||||
|
license = pkgs.lib.licenses.gpl3Only;
|
||||||
|
maintainers = [ "kavishdevar" ];
|
||||||
|
platforms = pkgs.lib.platforms.unix;
|
||||||
|
mainProgram = "librepods";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
in
|
||||||
|
{
|
||||||
|
checks = {
|
||||||
|
inherit librepods;
|
||||||
|
};
|
||||||
|
|
||||||
|
packages.default = librepods;
|
||||||
|
apps.default = {
|
||||||
|
type = "app";
|
||||||
|
program = lib.getExe librepods;
|
||||||
|
};
|
||||||
|
|
||||||
|
devShells.default = craneLib.devShell {
|
||||||
|
name = "librepods-dev";
|
||||||
|
checks = self'.checks;
|
||||||
|
|
||||||
|
# NOTE: cargo and rustc are provided by default.
|
||||||
|
buildInputs =
|
||||||
|
with pkgs;
|
||||||
|
[
|
||||||
|
rust-analyzer
|
||||||
|
]
|
||||||
|
++ buildInputs;
|
||||||
|
|
||||||
|
LD_LIBRARY_PATH = lib.makeLibraryPath buildInputs;
|
||||||
|
};
|
||||||
|
|
||||||
|
treefmt = {
|
||||||
|
programs.nixfmt.enable = pkgs.lib.meta.availableOn pkgs.stdenv.buildPlatform pkgs.nixfmt-rfc-style.compiler;
|
||||||
|
programs.nixfmt.package = pkgs.nixfmt-rfc-style;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
BIN
linux-rust/assets/font/sf_pro.otf
Normal file
BIN
linux-rust/assets/font/sf_pro.otf
Normal file
Binary file not shown.
@@ -10,7 +10,6 @@ command: librepods
|
|||||||
finish-args:
|
finish-args:
|
||||||
- --socket=wayland
|
- --socket=wayland
|
||||||
- --socket=fallback-x11
|
- --socket=fallback-x11
|
||||||
- --share=ipc
|
|
||||||
- --socket=pulseaudio
|
- --socket=pulseaudio
|
||||||
- --system-talk-name=org.bluez
|
- --system-talk-name=org.bluez
|
||||||
- --allow=bluetooth
|
- --allow=bluetooth
|
||||||
@@ -31,14 +30,14 @@ modules:
|
|||||||
env:
|
env:
|
||||||
CARGO_NET_OFFLINE: 'true'
|
CARGO_NET_OFFLINE: 'true'
|
||||||
build-commands:
|
build-commands:
|
||||||
- cargo build --frozen --offline --verbose
|
- cargo build --release --frozen --offline --verbose
|
||||||
- install -Dm755 target/debug/librepods ${FLATPAK_DEST}/bin/librepods
|
- install -Dm755 target/release/librepods ${FLATPAK_DEST}/bin/librepods
|
||||||
- install -Dm644 assets/icon.png ${FLATPAK_DEST}/share/icons/hicolor/256x256/apps/me.kavishdevar.librepods.png
|
- install -Dm644 assets/icon.png ${FLATPAK_DEST}/share/icons/hicolor/256x256/apps/me.kavishdevar.librepods.png
|
||||||
- install -Dm644 assets/me.kavishdevar.librepods.desktop ${FLATPAK_DEST}/share/applications/${FLATPAK_ID}.desktop
|
- install -Dm644 assets/me.kavishdevar.librepods.desktop ${FLATPAK_DEST}/share/applications/${FLATPAK_ID}.desktop
|
||||||
- install -Dm644 flatpak/me.kavishdevar.librepods.metainfo.xml ${FLATPAK_DEST}/share/metainfo/${FLATPAK_ID}.metainfo.xml
|
- install -Dm644 flatpak/me.kavishdevar.librepods.metainfo.xml ${FLATPAK_DEST}/share/metainfo/${FLATPAK_ID}.metainfo.xml
|
||||||
sources:
|
sources:
|
||||||
- type: archive
|
- type: archive
|
||||||
path: ../dist/librepods-vlocal-source.tar.gz
|
# path: ../dist/librepods-vlocal-source.tar.gz
|
||||||
# url: https://github.com/kavishdevar/librepods/releases/download/linux-v0.1.0/librepods-v0.1.0-source.tar.gz
|
url: https://github.com/kavishdevar/librepods/releases/download/linux-v0.1.0/librepods-v0.1.0-source.tar.gz
|
||||||
# sha256: 287d31cbf6a1e1e8ab6a8ef02d4d9b31aa299652086b39c548a37b7fc8e31ba7
|
sha256: 78828d6113dcdc37be9aa006d7a437ec1705978669cddb9342824ec9546a7b4e
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
use bluer::{l2cap::{SocketAddr, Socket, SeqPacket}, Address, AddressType, Result, Error};
|
|
||||||
use std::time::Duration;
|
|
||||||
use log::{info, error, debug};
|
|
||||||
use std::sync::Arc;
|
|
||||||
use tokio::sync::{Mutex, mpsc};
|
|
||||||
use tokio::task::JoinSet;
|
|
||||||
use tokio::time::{sleep, Instant};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use serde_json;
|
|
||||||
use crate::devices::airpods::AirPodsInformation;
|
use crate::devices::airpods::AirPodsInformation;
|
||||||
use crate::devices::enums::{DeviceData, DeviceInformation, DeviceType};
|
use crate::devices::enums::{DeviceData, DeviceInformation, DeviceType};
|
||||||
use crate::utils::get_devices_path;
|
use crate::utils::get_devices_path;
|
||||||
|
use bluer::{
|
||||||
|
Address, AddressType, Error, Result,
|
||||||
|
l2cap::{SeqPacket, Socket, SocketAddr},
|
||||||
|
};
|
||||||
|
use log::{debug, error, info};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
use tokio::sync::{Mutex, mpsc};
|
||||||
|
use tokio::task::JoinSet;
|
||||||
|
use tokio::time::{Instant, sleep};
|
||||||
|
|
||||||
const PSM: u16 = 0x1001;
|
const PSM: u16 = 0x1001;
|
||||||
const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
|
const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||||
@@ -215,9 +218,10 @@ pub enum AudioSourceType {
|
|||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum BatteryComponent {
|
pub enum BatteryComponent {
|
||||||
|
Headphone = 1,
|
||||||
Left = 4,
|
Left = 4,
|
||||||
Right = 2,
|
Right = 2,
|
||||||
Case = 8
|
Case = 8,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
@@ -225,7 +229,7 @@ pub enum BatteryComponent {
|
|||||||
pub enum BatteryStatus {
|
pub enum BatteryStatus {
|
||||||
Charging = 1,
|
Charging = 1,
|
||||||
NotCharging = 2,
|
NotCharging = 2,
|
||||||
Disconnected = 4
|
Disconnected = 4,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
@@ -234,7 +238,7 @@ pub enum EarDetectionStatus {
|
|||||||
InEar = 0x00,
|
InEar = 0x00,
|
||||||
OutOfEar = 0x01,
|
OutOfEar = 0x01,
|
||||||
InCase = 0x02,
|
InCase = 0x02,
|
||||||
Disconnected = 0x03
|
Disconnected = 0x03,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AudioSourceType {
|
impl AudioSourceType {
|
||||||
@@ -290,7 +294,8 @@ pub struct AirPodsLEKeys {
|
|||||||
pub struct AACPManagerState {
|
pub struct AACPManagerState {
|
||||||
pub sender: Option<mpsc::Sender<Vec<u8>>>,
|
pub sender: Option<mpsc::Sender<Vec<u8>>>,
|
||||||
pub control_command_status_list: Vec<ControlCommandStatus>,
|
pub control_command_status_list: Vec<ControlCommandStatus>,
|
||||||
pub control_command_subscribers: HashMap<ControlCommandIdentifiers, Vec<mpsc::UnboundedSender<Vec<u8>>>>,
|
pub control_command_subscribers:
|
||||||
|
HashMap<ControlCommandIdentifiers, Vec<mpsc::UnboundedSender<Vec<u8>>>>,
|
||||||
pub owns: bool,
|
pub owns: bool,
|
||||||
pub old_connected_devices: Vec<ConnectedDevice>,
|
pub old_connected_devices: Vec<ConnectedDevice>,
|
||||||
pub connected_devices: Vec<ConnectedDevice>,
|
pub connected_devices: Vec<ConnectedDevice>,
|
||||||
@@ -306,11 +311,10 @@ pub struct AACPManagerState {
|
|||||||
|
|
||||||
impl AACPManagerState {
|
impl AACPManagerState {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
let devices: HashMap<String, DeviceData> =
|
let devices: HashMap<String, DeviceData> = std::fs::read_to_string(get_devices_path())
|
||||||
std::fs::read_to_string(get_devices_path())
|
.ok()
|
||||||
.ok()
|
.and_then(|s| serde_json::from_str(&s).ok())
|
||||||
.and_then(|s| serde_json::from_str(&s).ok())
|
.unwrap_or_default();
|
||||||
.unwrap_or_default();
|
|
||||||
AACPManagerState {
|
AACPManagerState {
|
||||||
sender: None,
|
sender: None,
|
||||||
control_command_status_list: Vec::new(),
|
control_command_status_list: Vec::new(),
|
||||||
@@ -361,17 +365,18 @@ impl AACPManager {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let seq_packet = match tokio::time::timeout(CONNECT_TIMEOUT, socket.connect(target_sa)).await {
|
let seq_packet =
|
||||||
Ok(Ok(s)) => Arc::new(s),
|
match tokio::time::timeout(CONNECT_TIMEOUT, socket.connect(target_sa)).await {
|
||||||
Ok(Err(e)) => {
|
Ok(Ok(s)) => Arc::new(s),
|
||||||
error!("L2CAP connect failed: {}", e);
|
Ok(Err(e)) => {
|
||||||
return;
|
error!("L2CAP connect failed: {}", e);
|
||||||
}
|
return;
|
||||||
Err(_) => {
|
}
|
||||||
error!("L2CAP connect timed out");
|
Err(_) => {
|
||||||
return;
|
error!("L2CAP connect timed out");
|
||||||
}
|
return;
|
||||||
};
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Wait for connection to be fully established
|
// Wait for connection to be fully established
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
@@ -380,7 +385,8 @@ impl AACPManager {
|
|||||||
Ok(peer) if peer.cid != 0 => break,
|
Ok(peer) if peer.cid != 0 => break,
|
||||||
Ok(_) => { /* still waiting */ }
|
Ok(_) => { /* still waiting */ }
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
if e.raw_os_error() == Some(107) { // ENOTCONN
|
if e.raw_os_error() == Some(107) {
|
||||||
|
// ENOTCONN
|
||||||
error!("Peer has disconnected during connection setup.");
|
error!("Peer has disconnected during connection setup.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -438,18 +444,39 @@ impl AACPManager {
|
|||||||
state.event_tx = Some(tx);
|
state.event_tx = Some(tx);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn subscribe_to_control_command(&self, identifier: ControlCommandIdentifiers, tx: mpsc::UnboundedSender<Vec<u8>>) {
|
pub async fn subscribe_to_control_command(
|
||||||
|
&self,
|
||||||
|
identifier: ControlCommandIdentifiers,
|
||||||
|
tx: mpsc::UnboundedSender<Vec<u8>>,
|
||||||
|
) {
|
||||||
let mut state = self.state.lock().await;
|
let mut state = self.state.lock().await;
|
||||||
state.control_command_subscribers.entry(identifier).or_default().push(tx);
|
state
|
||||||
|
.control_command_subscribers
|
||||||
|
.entry(identifier)
|
||||||
|
.or_default()
|
||||||
|
.push(tx);
|
||||||
// send initial value if available
|
// send initial value if available
|
||||||
if let Some(status) = state.control_command_status_list.iter().find(|s| s.identifier == identifier) {
|
if let Some(status) = state
|
||||||
let _ = state.control_command_subscribers.get(&identifier).unwrap().last().unwrap().send(status.value.clone());
|
.control_command_status_list
|
||||||
|
.iter()
|
||||||
|
.find(|s| s.identifier == identifier)
|
||||||
|
{
|
||||||
|
let _ = state
|
||||||
|
.control_command_subscribers
|
||||||
|
.get(&identifier)
|
||||||
|
.unwrap()
|
||||||
|
.last()
|
||||||
|
.unwrap()
|
||||||
|
.send(status.value.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn receive_packet(&self, packet: &[u8]) {
|
pub async fn receive_packet(&self, packet: &[u8]) {
|
||||||
if !packet.starts_with(&HEADER_BYTES) {
|
if !packet.starts_with(&HEADER_BYTES) {
|
||||||
debug!("Received packet does not start with expected header: {}", hex::encode(packet));
|
debug!(
|
||||||
|
"Received packet does not start with expected header: {}",
|
||||||
|
hex::encode(packet)
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if packet.len() < 5 {
|
if packet.len() < 5 {
|
||||||
@@ -468,7 +495,10 @@ impl AACPManager {
|
|||||||
}
|
}
|
||||||
let count = payload[2] as usize;
|
let count = payload[2] as usize;
|
||||||
if payload.len() < 3 + count * 5 {
|
if payload.len() < 3 + count * 5 {
|
||||||
error!("Battery Info packet length mismatch: {}", hex::encode(payload));
|
error!(
|
||||||
|
"Battery Info packet length mismatch: {}",
|
||||||
|
hex::encode(payload)
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let mut batteries = Vec::with_capacity(count);
|
let mut batteries = Vec::with_capacity(count);
|
||||||
@@ -476,6 +506,7 @@ impl AACPManager {
|
|||||||
let base_index = 3 + i * 5;
|
let base_index = 3 + i * 5;
|
||||||
batteries.push(BatteryInfo {
|
batteries.push(BatteryInfo {
|
||||||
component: match payload[base_index] {
|
component: match payload[base_index] {
|
||||||
|
0x01 => BatteryComponent::Headphone,
|
||||||
0x02 => BatteryComponent::Right,
|
0x02 => BatteryComponent::Right,
|
||||||
0x04 => BatteryComponent::Left,
|
0x04 => BatteryComponent::Left,
|
||||||
0x08 => BatteryComponent::Case,
|
0x08 => BatteryComponent::Case,
|
||||||
@@ -493,7 +524,7 @@ impl AACPManager {
|
|||||||
error!("Unknown battery status: {:#04x}", payload[base_index + 3]);
|
error!("Unknown battery status: {:#04x}", payload[base_index + 3]);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let mut state = self.state.lock().await;
|
let mut state = self.state.lock().await;
|
||||||
@@ -518,9 +549,16 @@ impl AACPManager {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if let Some(identifier) = ControlCommandIdentifiers::from_u8(identifier_byte) {
|
if let Some(identifier) = ControlCommandIdentifiers::from_u8(identifier_byte) {
|
||||||
let status = ControlCommandStatus { identifier, value: value.clone() };
|
let status = ControlCommandStatus {
|
||||||
|
identifier,
|
||||||
|
value: value.clone(),
|
||||||
|
};
|
||||||
let mut state = self.state.lock().await;
|
let mut state = self.state.lock().await;
|
||||||
if let Some(existing) = state.control_command_status_list.iter_mut().find(|s| s.identifier == identifier) {
|
if let Some(existing) = state
|
||||||
|
.control_command_status_list
|
||||||
|
.iter_mut()
|
||||||
|
.find(|s| s.identifier == identifier)
|
||||||
|
{
|
||||||
existing.value = value.clone();
|
existing.value = value.clone();
|
||||||
} else {
|
} else {
|
||||||
state.control_command_status_list.push(status.clone());
|
state.control_command_status_list.push(status.clone());
|
||||||
@@ -536,9 +574,16 @@ impl AACPManager {
|
|||||||
if let Some(ref tx) = state.event_tx {
|
if let Some(ref tx) = state.event_tx {
|
||||||
let _ = tx.send(AACPEvent::ControlCommand(status));
|
let _ = tx.send(AACPEvent::ControlCommand(status));
|
||||||
}
|
}
|
||||||
info!("Received Control Command: {:?}, value: {}", identifier, hex::encode(&value));
|
info!(
|
||||||
|
"Received Control Command: {:?}, value: {}",
|
||||||
|
identifier,
|
||||||
|
hex::encode(&value)
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
error!("Unknown Control Command identifier: {:#04x}", identifier_byte);
|
error!(
|
||||||
|
"Unknown Control Command identifier: {:#04x}",
|
||||||
|
identifier_byte
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
opcodes::EAR_DETECTION => {
|
opcodes::EAR_DETECTION => {
|
||||||
@@ -570,10 +615,19 @@ impl AACPManager {
|
|||||||
state.ear_detection_status = statuses.clone();
|
state.ear_detection_status = statuses.clone();
|
||||||
|
|
||||||
if let Some(ref tx) = state.event_tx {
|
if let Some(ref tx) = state.event_tx {
|
||||||
debug!("Sending Ear Detection event: old: {:?}, new: {:?}", state.old_ear_detection_status, statuses);
|
debug!(
|
||||||
let _ = tx.send(AACPEvent::EarDetection(state.old_ear_detection_status.clone(), statuses));
|
"Sending Ear Detection event: old: {:?}, new: {:?}",
|
||||||
|
state.old_ear_detection_status, statuses
|
||||||
|
);
|
||||||
|
let _ = tx.send(AACPEvent::EarDetection(
|
||||||
|
state.old_ear_detection_status.clone(),
|
||||||
|
statuses,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
info!("Received Ear Detection Status: {:?}", state.ear_detection_status);
|
info!(
|
||||||
|
"Received Ear Detection Status: {:?}",
|
||||||
|
state.ear_detection_status
|
||||||
|
);
|
||||||
}
|
}
|
||||||
opcodes::CONVERSATION_AWARENESS => {
|
opcodes::CONVERSATION_AWARENESS => {
|
||||||
if packet.len() == 10 {
|
if packet.len() == 10 {
|
||||||
@@ -585,7 +639,10 @@ impl AACPManager {
|
|||||||
}
|
}
|
||||||
info!("Received Conversation Awareness: {}", status);
|
info!("Received Conversation Awareness: {}", status);
|
||||||
} else {
|
} else {
|
||||||
info!("Received Conversation Awareness packet with unexpected length: {}", packet.len());
|
info!(
|
||||||
|
"Received Conversation Awareness packet with unexpected length: {}",
|
||||||
|
packet.len()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
opcodes::INFORMATION => {
|
opcodes::INFORMATION => {
|
||||||
@@ -617,7 +674,7 @@ impl AACPManager {
|
|||||||
}
|
}
|
||||||
strings.remove(0);
|
strings.remove(0);
|
||||||
let info = AirPodsInformation {
|
let info = AirPodsInformation {
|
||||||
name: strings.get(0).cloned().unwrap_or_default(),
|
name: strings.first().cloned().unwrap_or_default(),
|
||||||
model_number: strings.get(1).cloned().unwrap_or_default(),
|
model_number: strings.get(1).cloned().unwrap_or_default(),
|
||||||
manufacturer: strings.get(2).cloned().unwrap_or_default(),
|
manufacturer: strings.get(2).cloned().unwrap_or_default(),
|
||||||
serial_number: strings.get(3).cloned().unwrap_or_default(),
|
serial_number: strings.get(3).cloned().unwrap_or_default(),
|
||||||
@@ -634,28 +691,31 @@ impl AACPManager {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
let mut state = self.state.lock().await;
|
let mut state = self.state.lock().await;
|
||||||
if let Some(mac) = state.airpods_mac {
|
if let Some(mac) = state.airpods_mac
|
||||||
if let Some(device_data) = state.devices.get_mut(&mac.to_string()) {
|
&& let Some(device_data) = state.devices.get_mut(&mac.to_string())
|
||||||
device_data.name = info.name.clone();
|
{
|
||||||
device_data.information = Some(DeviceInformation::AirPods(info.clone()));
|
device_data.name = info.name.clone();
|
||||||
}
|
device_data.information = Some(DeviceInformation::AirPods(info.clone()));
|
||||||
}
|
}
|
||||||
let json = serde_json::to_string(&state.devices).unwrap();
|
let json = serde_json::to_string(&state.devices).unwrap();
|
||||||
if let Some(parent) = get_devices_path().parent() {
|
if let Some(parent) = get_devices_path().parent()
|
||||||
if let Err(e) = tokio::fs::create_dir_all(&parent).await {
|
&& let Err(e) = tokio::fs::create_dir_all(&parent).await
|
||||||
error!("Failed to create directory for devices: {}", e);
|
{
|
||||||
return;
|
error!("Failed to create directory for devices: {}", e);
|
||||||
}
|
return;
|
||||||
}
|
}
|
||||||
if let Err(e) = tokio::fs::write(&get_devices_path(), json).await {
|
if let Err(e) = tokio::fs::write(&get_devices_path(), json).await {
|
||||||
error!("Failed to save devices: {}", e);
|
error!("Failed to save devices: {}", e);
|
||||||
}
|
}
|
||||||
info!("Received Information: {:?}", info);
|
info!("Received Information: {:?}", info);
|
||||||
},
|
}
|
||||||
|
|
||||||
opcodes::PROXIMITY_KEYS_RSP => {
|
opcodes::PROXIMITY_KEYS_RSP => {
|
||||||
if payload.len() < 4 {
|
if payload.len() < 4 {
|
||||||
error!("Proximity Keys Response packet too short: {}", hex::encode(payload));
|
error!(
|
||||||
|
"Proximity Keys Response packet too short: {}",
|
||||||
|
hex::encode(payload)
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let key_count = payload[2] as usize;
|
let key_count = payload[2] as usize;
|
||||||
@@ -664,67 +724,77 @@ impl AACPManager {
|
|||||||
let mut keys = Vec::new();
|
let mut keys = Vec::new();
|
||||||
for _ in 0..key_count {
|
for _ in 0..key_count {
|
||||||
if offset + 3 >= payload.len() {
|
if offset + 3 >= payload.len() {
|
||||||
error!("Proximity Keys Response packet too short while parsing keys: {}", hex::encode(payload));
|
error!(
|
||||||
|
"Proximity Keys Response packet too short while parsing keys: {}",
|
||||||
|
hex::encode(payload)
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let key_type = payload[offset];
|
let key_type = payload[offset];
|
||||||
let key_length = payload[offset + 2] as usize;
|
let key_length = payload[offset + 2] as usize;
|
||||||
offset += 4;
|
offset += 4;
|
||||||
if offset + key_length > payload.len() {
|
if offset + key_length > payload.len() {
|
||||||
error!("Proximity Keys Response packet too short for key data: {}", hex::encode(payload));
|
error!(
|
||||||
|
"Proximity Keys Response packet too short for key data: {}",
|
||||||
|
hex::encode(payload)
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let key_data = payload[offset..offset + key_length].to_vec();
|
let key_data = payload[offset..offset + key_length].to_vec();
|
||||||
keys.push((key_type, key_data));
|
keys.push((key_type, key_data));
|
||||||
offset += key_length;
|
offset += key_length;
|
||||||
}
|
}
|
||||||
info!("Received Proximity Keys Response: {:?}", keys.iter().map(|(kt, kd)| (kt, hex::encode(kd))).collect::<Vec<_>>());
|
info!(
|
||||||
|
"Received Proximity Keys Response: {:?}",
|
||||||
|
keys.iter()
|
||||||
|
.map(|(kt, kd)| (kt, hex::encode(kd)))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
);
|
||||||
let mut state = self.state.lock().await;
|
let mut state = self.state.lock().await;
|
||||||
for (key_type, key_data) in &keys {
|
for (key_type, key_data) in &keys {
|
||||||
if let Some(kt) = ProximityKeyType::from_u8(*key_type) {
|
if let Some(kt) = ProximityKeyType::from_u8(*key_type)
|
||||||
if let Some(mac) = state.airpods_mac {
|
&& let Some(mac) = state.airpods_mac
|
||||||
let mac_str = mac.to_string();
|
{
|
||||||
let device_data = state.devices.entry(mac_str.clone()).or_insert(DeviceData {
|
let mac_str = mac.to_string();
|
||||||
|
let device_data =
|
||||||
|
state.devices.entry(mac_str.clone()).or_insert(DeviceData {
|
||||||
name: mac_str.clone(),
|
name: mac_str.clone(),
|
||||||
type_: DeviceType::AirPods,
|
type_: DeviceType::AirPods,
|
||||||
information: None,
|
information: None,
|
||||||
});
|
});
|
||||||
match kt {
|
match kt {
|
||||||
ProximityKeyType::Irk => {
|
ProximityKeyType::Irk => match device_data.information.as_mut() {
|
||||||
match device_data.information.as_mut() {
|
Some(DeviceInformation::AirPods(info)) => {
|
||||||
Some(DeviceInformation::AirPods(info)) => {
|
info.le_keys.irk = hex::encode(key_data);
|
||||||
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() {
|
error!("Device information is not AirPods for adding LE IRK.");
|
||||||
Some(DeviceInformation::AirPods(info)) => {
|
|
||||||
info.le_keys.enc_key = hex::encode(key_data);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
error!("Device information is not AirPods for adding LE encryption key.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
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."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let json = serde_json::to_string(&state.devices).unwrap();
|
let json = serde_json::to_string(&state.devices).unwrap();
|
||||||
if let Some(parent) = get_devices_path().parent() {
|
if let Some(parent) = get_devices_path().parent()
|
||||||
if let Err(e) = tokio::fs::create_dir_all(&parent).await {
|
&& let Err(e) = tokio::fs::create_dir_all(&parent).await
|
||||||
error!("Failed to create directory for devices: {}", e);
|
{
|
||||||
return;
|
error!("Failed to create directory for devices: {}", e);
|
||||||
}
|
return;
|
||||||
}
|
}
|
||||||
if let Err(e) = tokio::fs::write(&get_devices_path(), json).await {
|
if let Err(e) = tokio::fs::write(&get_devices_path(), json).await {
|
||||||
error!("Failed to save devices: {}", e);
|
error!("Failed to save devices: {}", e);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
opcodes::STEM_PRESS => info!("Received Stem Press packet."),
|
opcodes::STEM_PRESS => info!("Received Stem Press packet."),
|
||||||
opcodes::AUDIO_SOURCE => {
|
opcodes::AUDIO_SOURCE => {
|
||||||
if payload.len() < 9 {
|
if payload.len() < 9 {
|
||||||
@@ -746,12 +816,18 @@ impl AACPManager {
|
|||||||
}
|
}
|
||||||
opcodes::CONNECTED_DEVICES => {
|
opcodes::CONNECTED_DEVICES => {
|
||||||
if payload.len() < 3 {
|
if payload.len() < 3 {
|
||||||
error!("Connected Devices packet too short: {}", hex::encode(payload));
|
error!(
|
||||||
|
"Connected Devices packet too short: {}",
|
||||||
|
hex::encode(payload)
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let count = payload[2] as usize;
|
let count = payload[2] as usize;
|
||||||
if payload.len() < 3 + count * 8 {
|
if payload.len() < 3 + count * 8 {
|
||||||
error!("Connected Devices packet length mismatch: {}", hex::encode(payload));
|
error!(
|
||||||
|
"Connected Devices packet length mismatch: {}",
|
||||||
|
hex::encode(payload)
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let mut devices = Vec::with_capacity(count);
|
let mut devices = Vec::with_capacity(count);
|
||||||
@@ -759,17 +835,30 @@ impl AACPManager {
|
|||||||
let base = 5 + i * 8;
|
let base = 5 + i * 8;
|
||||||
let mac = format!(
|
let mac = format!(
|
||||||
"{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}",
|
"{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}",
|
||||||
payload[base], payload[base + 1], payload[base + 2], payload[base + 3], payload[base + 4], payload[base + 5]
|
payload[base],
|
||||||
|
payload[base + 1],
|
||||||
|
payload[base + 2],
|
||||||
|
payload[base + 3],
|
||||||
|
payload[base + 4],
|
||||||
|
payload[base + 5]
|
||||||
);
|
);
|
||||||
let info1 = payload[base + 6];
|
let info1 = payload[base + 6];
|
||||||
let info2 = payload[base + 7];
|
let info2 = payload[base + 7];
|
||||||
devices.push(ConnectedDevice { mac, info1, info2, r#type: None });
|
devices.push(ConnectedDevice {
|
||||||
|
mac,
|
||||||
|
info1,
|
||||||
|
info2,
|
||||||
|
r#type: None,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
let mut state = self.state.lock().await;
|
let mut state = self.state.lock().await;
|
||||||
state.old_connected_devices = state.connected_devices.clone();
|
state.old_connected_devices = state.connected_devices.clone();
|
||||||
state.connected_devices = devices.clone();
|
state.connected_devices = devices.clone();
|
||||||
if let Some(ref tx) = state.event_tx {
|
if let Some(ref tx) = state.event_tx {
|
||||||
let _ = tx.send(AACPEvent::ConnectedDevices(state.old_connected_devices.clone(), devices));
|
let _ = tx.send(AACPEvent::ConnectedDevices(
|
||||||
|
state.old_connected_devices.clone(),
|
||||||
|
devices,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
info!("Received Connected Devices: {:?}", state.connected_devices);
|
info!("Received Connected Devices: {:?}", state.connected_devices);
|
||||||
}
|
}
|
||||||
@@ -784,7 +873,7 @@ impl AACPManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
opcodes::EQ_DATA => {
|
opcodes::EQ_DATA => {
|
||||||
debug!("Received EQ Data");
|
debug!("Received EQ Data");
|
||||||
}
|
}
|
||||||
_ => debug!("Received unknown packet with opcode {:#04x}", opcode),
|
_ => debug!("Received unknown packet with opcode {:#04x}", opcode),
|
||||||
}
|
}
|
||||||
@@ -807,17 +896,18 @@ impl AACPManager {
|
|||||||
|
|
||||||
pub async fn send_handshake(&self) -> Result<()> {
|
pub async fn send_handshake(&self) -> Result<()> {
|
||||||
let packet = [
|
let packet = [
|
||||||
0x00, 0x00, 0x04, 0x00,
|
0x00, 0x00, 0x04, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
0x01, 0x00, 0x02, 0x00,
|
0x00, 0x00,
|
||||||
0x00, 0x00, 0x00, 0x00,
|
|
||||||
0x00, 0x00, 0x00, 0x00
|
|
||||||
];
|
];
|
||||||
self.send_packet(&packet).await
|
self.send_packet(&packet).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn send_proximity_keys_request(&self, key_types: Vec<ProximityKeyType>) -> Result<()> {
|
pub async fn send_proximity_keys_request(
|
||||||
|
&self,
|
||||||
|
key_types: Vec<ProximityKeyType>,
|
||||||
|
) -> Result<()> {
|
||||||
let opcode = [opcodes::PROXIMITY_KEYS_REQ, 0x00];
|
let opcode = [opcodes::PROXIMITY_KEYS_REQ, 0x00];
|
||||||
let mut data = Vec::with_capacity( 2);
|
let mut data = Vec::with_capacity(2);
|
||||||
data.push(key_types.iter().fold(0u8, |acc, kt| acc | (*kt as u8)));
|
data.push(key_types.iter().fold(0u8, |acc, kt| acc | (*kt as u8)));
|
||||||
data.push(0x00);
|
data.push(0x00);
|
||||||
let packet = [opcode.as_slice(), data.as_slice()].concat();
|
let packet = [opcode.as_slice(), data.as_slice()].concat();
|
||||||
@@ -836,7 +926,11 @@ impl AACPManager {
|
|||||||
self.send_data_packet(&packet).await
|
self.send_data_packet(&packet).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn send_control_command(&self, identifier: ControlCommandIdentifiers, value: &[u8]) -> Result<()> {
|
pub async fn send_control_command(
|
||||||
|
&self,
|
||||||
|
identifier: ControlCommandIdentifiers,
|
||||||
|
value: &[u8],
|
||||||
|
) -> Result<()> {
|
||||||
let opcode = [opcodes::CONTROL_COMMAND, 0x00];
|
let opcode = [opcodes::CONTROL_COMMAND, 0x00];
|
||||||
let mut data = vec![identifier as u8];
|
let mut data = vec![identifier as u8];
|
||||||
for i in 0..4 {
|
for i in 0..4 {
|
||||||
@@ -846,10 +940,17 @@ impl AACPManager {
|
|||||||
self.send_data_packet(&packet).await
|
self.send_data_packet(&packet).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn send_media_information_new_device(&self, self_mac_address: &str, target_mac_address: &str) -> Result<()> {
|
pub async fn send_media_information_new_device(
|
||||||
|
&self,
|
||||||
|
self_mac_address: &str,
|
||||||
|
target_mac_address: &str,
|
||||||
|
) -> Result<()> {
|
||||||
let opcode = [opcodes::SMART_ROUTING, 0x00];
|
let opcode = [opcodes::SMART_ROUTING, 0x00];
|
||||||
let mut buffer = Vec::with_capacity(112);
|
let mut buffer = Vec::with_capacity(112);
|
||||||
let target_mac_bytes: Vec<u8> = target_mac_address.split(':').map(|s| u8::from_str_radix(s, 16).unwrap()).collect();
|
let target_mac_bytes: Vec<u8> = target_mac_address
|
||||||
|
.split(':')
|
||||||
|
.map(|s| u8::from_str_radix(s, 16).unwrap())
|
||||||
|
.collect();
|
||||||
buffer.extend_from_slice(&target_mac_bytes.iter().rev().cloned().collect::<Vec<u8>>());
|
buffer.extend_from_slice(&target_mac_bytes.iter().rev().cloned().collect::<Vec<u8>>());
|
||||||
|
|
||||||
buffer.extend_from_slice(&[0x68, 0x00]);
|
buffer.extend_from_slice(&[0x68, 0x00]);
|
||||||
@@ -881,7 +982,10 @@ impl AACPManager {
|
|||||||
pub async fn send_hijack_request(&self, target_mac_address: &str) -> Result<()> {
|
pub async fn send_hijack_request(&self, target_mac_address: &str) -> Result<()> {
|
||||||
let opcode = [opcodes::SMART_ROUTING, 0x00];
|
let opcode = [opcodes::SMART_ROUTING, 0x00];
|
||||||
let mut buffer = Vec::with_capacity(106);
|
let mut buffer = Vec::with_capacity(106);
|
||||||
let target_mac_bytes: Vec<u8> = target_mac_address.split(':').map(|s| u8::from_str_radix(s, 16).unwrap()).collect();
|
let target_mac_bytes: Vec<u8> = target_mac_address
|
||||||
|
.split(':')
|
||||||
|
.map(|s| u8::from_str_radix(s, 16).unwrap())
|
||||||
|
.collect();
|
||||||
buffer.extend_from_slice(&target_mac_bytes.iter().rev().cloned().collect::<Vec<u8>>());
|
buffer.extend_from_slice(&target_mac_bytes.iter().rev().cloned().collect::<Vec<u8>>());
|
||||||
buffer.extend_from_slice(&[0x62, 0x00]);
|
buffer.extend_from_slice(&[0x62, 0x00]);
|
||||||
buffer.extend_from_slice(&[0x01, 0xE5]);
|
buffer.extend_from_slice(&[0x01, 0xE5]);
|
||||||
@@ -909,10 +1013,18 @@ impl AACPManager {
|
|||||||
self.send_data_packet(&packet).await
|
self.send_data_packet(&packet).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn send_media_information(&self, self_mac_address: &str, target_mac_address: &str, streaming_state: bool) -> Result<()> {
|
pub async fn send_media_information(
|
||||||
|
&self,
|
||||||
|
self_mac_address: &str,
|
||||||
|
target_mac_address: &str,
|
||||||
|
streaming_state: bool,
|
||||||
|
) -> Result<()> {
|
||||||
let opcode = [opcodes::SMART_ROUTING, 0x00];
|
let opcode = [opcodes::SMART_ROUTING, 0x00];
|
||||||
let mut buffer = Vec::with_capacity(138);
|
let mut buffer = Vec::with_capacity(138);
|
||||||
let target_mac_bytes: Vec<u8> = target_mac_address.split(':').map(|s| u8::from_str_radix(s, 16).unwrap()).collect();
|
let target_mac_bytes: Vec<u8> = target_mac_address
|
||||||
|
.split(':')
|
||||||
|
.map(|s| u8::from_str_radix(s, 16).unwrap())
|
||||||
|
.collect();
|
||||||
buffer.extend_from_slice(&target_mac_bytes.iter().rev().cloned().collect::<Vec<u8>>());
|
buffer.extend_from_slice(&target_mac_bytes.iter().rev().cloned().collect::<Vec<u8>>());
|
||||||
buffer.extend_from_slice(&[0x82, 0x00]);
|
buffer.extend_from_slice(&[0x82, 0x00]);
|
||||||
buffer.extend_from_slice(&[0x01, 0xE5, 0x4A]);
|
buffer.extend_from_slice(&[0x01, 0xE5, 0x4A]);
|
||||||
@@ -945,7 +1057,10 @@ impl AACPManager {
|
|||||||
pub async fn send_smart_routing_show_ui(&self, target_mac_address: &str) -> Result<()> {
|
pub async fn send_smart_routing_show_ui(&self, target_mac_address: &str) -> Result<()> {
|
||||||
let opcode = [opcodes::SMART_ROUTING, 0x00];
|
let opcode = [opcodes::SMART_ROUTING, 0x00];
|
||||||
let mut buffer = Vec::with_capacity(134);
|
let mut buffer = Vec::with_capacity(134);
|
||||||
let target_mac_bytes: Vec<u8> = target_mac_address.split(':').map(|s| u8::from_str_radix(s, 16).unwrap()).collect();
|
let target_mac_bytes: Vec<u8> = target_mac_address
|
||||||
|
.split(':')
|
||||||
|
.map(|s| u8::from_str_radix(s, 16).unwrap())
|
||||||
|
.collect();
|
||||||
buffer.extend_from_slice(&target_mac_bytes.iter().rev().cloned().collect::<Vec<u8>>());
|
buffer.extend_from_slice(&target_mac_bytes.iter().rev().cloned().collect::<Vec<u8>>());
|
||||||
buffer.extend_from_slice(&[0x7E, 0x00]);
|
buffer.extend_from_slice(&[0x7E, 0x00]);
|
||||||
buffer.extend_from_slice(&[0x01, 0xE6, 0x5B]);
|
buffer.extend_from_slice(&[0x01, 0xE6, 0x5B]);
|
||||||
@@ -978,7 +1093,10 @@ impl AACPManager {
|
|||||||
pub async fn send_hijack_reversed(&self, target_mac_address: &str) -> Result<()> {
|
pub async fn send_hijack_reversed(&self, target_mac_address: &str) -> Result<()> {
|
||||||
let opcode = [opcodes::SMART_ROUTING, 0x00];
|
let opcode = [opcodes::SMART_ROUTING, 0x00];
|
||||||
let mut buffer = Vec::with_capacity(97);
|
let mut buffer = Vec::with_capacity(97);
|
||||||
let target_mac_bytes: Vec<u8> = target_mac_address.split(':').map(|s| u8::from_str_radix(s, 16).unwrap()).collect();
|
let target_mac_bytes: Vec<u8> = target_mac_address
|
||||||
|
.split(':')
|
||||||
|
.map(|s| u8::from_str_radix(s, 16).unwrap())
|
||||||
|
.collect();
|
||||||
buffer.extend_from_slice(&target_mac_bytes.iter().rev().cloned().collect::<Vec<u8>>());
|
buffer.extend_from_slice(&target_mac_bytes.iter().rev().cloned().collect::<Vec<u8>>());
|
||||||
buffer.extend_from_slice(&[0x59, 0x00]);
|
buffer.extend_from_slice(&[0x59, 0x00]);
|
||||||
buffer.extend_from_slice(&[0x01, 0xE3]);
|
buffer.extend_from_slice(&[0x01, 0xE3]);
|
||||||
@@ -1001,10 +1119,17 @@ impl AACPManager {
|
|||||||
self.send_data_packet(&packet).await
|
self.send_data_packet(&packet).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn send_add_tipi_device(&self, self_mac_address: &str, target_mac_address: &str) -> Result<()> {
|
pub async fn send_add_tipi_device(
|
||||||
|
&self,
|
||||||
|
self_mac_address: &str,
|
||||||
|
target_mac_address: &str,
|
||||||
|
) -> Result<()> {
|
||||||
let opcode = [opcodes::SMART_ROUTING, 0x00];
|
let opcode = [opcodes::SMART_ROUTING, 0x00];
|
||||||
let mut buffer = Vec::with_capacity(86);
|
let mut buffer = Vec::with_capacity(86);
|
||||||
let target_mac_bytes: Vec<u8> = target_mac_address.split(':').map(|s| u8::from_str_radix(s, 16).unwrap()).collect();
|
let target_mac_bytes: Vec<u8> = target_mac_address
|
||||||
|
.split(':')
|
||||||
|
.map(|s| u8::from_str_radix(s, 16).unwrap())
|
||||||
|
.collect();
|
||||||
buffer.extend_from_slice(&target_mac_bytes.iter().rev().cloned().collect::<Vec<u8>>());
|
buffer.extend_from_slice(&target_mac_bytes.iter().rev().cloned().collect::<Vec<u8>>());
|
||||||
buffer.extend_from_slice(&[0x4E, 0x00]);
|
buffer.extend_from_slice(&[0x4E, 0x00]);
|
||||||
buffer.extend_from_slice(&[0x01, 0xE5]);
|
buffer.extend_from_slice(&[0x01, 0xE5]);
|
||||||
@@ -1029,10 +1154,8 @@ impl AACPManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn send_some_packet(&self) -> Result<()> {
|
pub async fn send_some_packet(&self) -> Result<()> {
|
||||||
self.send_data_packet(&[
|
self.send_data_packet(&[0x29, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF])
|
||||||
0x29, 0x00,
|
.await
|
||||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
|
|
||||||
]).await
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1051,7 +1174,9 @@ async fn recv_thread(manager: AACPManager, sp: Arc<SeqPacket>) {
|
|||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Read error: {}", e);
|
error!("Read error: {}", e);
|
||||||
debug!("We have probably disconnected, clearing state variables (owns=false, connected_devices=empty, control_command_status_list=empty).");
|
debug!(
|
||||||
|
"We have probably disconnected, clearing state variables (owns=false, connected_devices=empty, control_command_status_list=empty)."
|
||||||
|
);
|
||||||
let mut state = manager.state.lock().await;
|
let mut state = manager.state.lock().await;
|
||||||
state.owns = false;
|
state.owns = false;
|
||||||
state.connected_devices.clear();
|
state.connected_devices.clear();
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
use bluer::l2cap::{SocketAddr, Socket, SeqPacket};
|
use bluer::l2cap::{SeqPacket, Socket, SocketAddr};
|
||||||
use bluer::{Address, AddressType, Result, Error};
|
use bluer::{Address, AddressType, Error, Result};
|
||||||
use log::{info, error, debug};
|
use hex;
|
||||||
|
use log::{debug, error, info};
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::{Mutex, mpsc};
|
use tokio::sync::{Mutex, mpsc};
|
||||||
use tokio::task::JoinSet;
|
use tokio::task::JoinSet;
|
||||||
use tokio::time::{sleep, Duration, Instant};
|
use tokio::time::{Duration, Instant, sleep};
|
||||||
use std::collections::HashMap;
|
|
||||||
use hex;
|
|
||||||
|
|
||||||
const PSM_ATT: u16 = 0x001F;
|
const PSM_ATT: u16 = 0x001F;
|
||||||
const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
|
const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||||
@@ -25,7 +25,7 @@ pub enum ATTHandles {
|
|||||||
AirPodsLoudSoundReduction = 0x1B,
|
AirPodsLoudSoundReduction = 0x1B,
|
||||||
AirPodsHearingAid = 0x2A,
|
AirPodsHearingAid = 0x2A,
|
||||||
NothingEverything = 0x8002,
|
NothingEverything = 0x8002,
|
||||||
NothingEverythingRead = 0x8005 // for some reason, and not the same as the write handle
|
NothingEverythingRead = 0x8005, // for some reason, and not the same as the write handle
|
||||||
}
|
}
|
||||||
|
|
||||||
#[repr(u16)]
|
#[repr(u16)]
|
||||||
@@ -43,7 +43,7 @@ impl From<ATTHandles> for ATTCCCDHandles {
|
|||||||
ATTHandles::AirPodsLoudSoundReduction => ATTCCCDHandles::LoudSoundReduction,
|
ATTHandles::AirPodsLoudSoundReduction => ATTCCCDHandles::LoudSoundReduction,
|
||||||
ATTHandles::AirPodsHearingAid => ATTCCCDHandles::HearingAid,
|
ATTHandles::AirPodsHearingAid => ATTCCCDHandles::HearingAid,
|
||||||
ATTHandles::NothingEverything => panic!("No CCCD for NothingEverything handle"), // we don't request it
|
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
|
ATTHandles::NothingEverythingRead => panic!("No CCD for NothingEverythingRead handle"), // it sends notifications without CCCD
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -57,7 +57,7 @@ impl ATTManagerState {
|
|||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
ATTManagerState {
|
ATTManagerState {
|
||||||
sender: None,
|
sender: None,
|
||||||
listeners: HashMap::new()
|
listeners: HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,11 +82,15 @@ impl ATTManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn connect(&mut self, addr: Address) -> Result<()> {
|
pub async fn connect(&mut self, addr: Address) -> Result<()> {
|
||||||
info!("ATTManager connecting to {} on PSM {:#06X}...", addr, PSM_ATT);
|
info!(
|
||||||
|
"ATTManager connecting to {} on PSM {:#06X}...",
|
||||||
|
addr, PSM_ATT
|
||||||
|
);
|
||||||
let target_sa = SocketAddr::new(addr, AddressType::BrEdr, PSM_ATT);
|
let target_sa = SocketAddr::new(addr, AddressType::BrEdr, PSM_ATT);
|
||||||
|
|
||||||
let socket = Socket::new_seq_packet()?;
|
let socket = Socket::new_seq_packet()?;
|
||||||
let seq_packet_result = tokio::time::timeout(CONNECT_TIMEOUT, socket.connect(target_sa)).await;
|
let seq_packet_result =
|
||||||
|
tokio::time::timeout(CONNECT_TIMEOUT, socket.connect(target_sa)).await;
|
||||||
let seq_packet = match seq_packet_result {
|
let seq_packet = match seq_packet_result {
|
||||||
Ok(Ok(s)) => Arc::new(s),
|
Ok(Ok(s)) => Arc::new(s),
|
||||||
Ok(Err(e)) => {
|
Ok(Err(e)) => {
|
||||||
@@ -95,7 +99,10 @@ impl ATTManager {
|
|||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
error!("L2CAP connect timed out");
|
error!("L2CAP connect timed out");
|
||||||
return Err(Error::from(std::io::Error::new(std::io::ErrorKind::TimedOut, "Connection timeout")));
|
return Err(Error::from(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::TimedOut,
|
||||||
|
"Connection timeout",
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -106,7 +113,8 @@ impl ATTManager {
|
|||||||
Ok(peer) if peer.cid != 0 => break,
|
Ok(peer) if peer.cid != 0 => break,
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
if e.raw_os_error() == Some(107) { // ENOTCONN
|
if e.raw_os_error() == Some(107) {
|
||||||
|
// ENOTCONN
|
||||||
error!("Peer has disconnected during connection setup.");
|
error!("Peer has disconnected during connection setup.");
|
||||||
return Err(e.into());
|
return Err(e.into());
|
||||||
}
|
}
|
||||||
@@ -115,7 +123,10 @@ impl ATTManager {
|
|||||||
}
|
}
|
||||||
if start.elapsed() >= CONNECT_TIMEOUT {
|
if start.elapsed() >= CONNECT_TIMEOUT {
|
||||||
error!("Timed out waiting for L2CAP connection to be fully established.");
|
error!("Timed out waiting for L2CAP connection to be fully established.");
|
||||||
return Err(Error::from(std::io::Error::new(std::io::ErrorKind::TimedOut, "Connection timeout")));
|
return Err(Error::from(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::TimedOut,
|
||||||
|
"Connection timeout",
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
sleep(POLL_INTERVAL).await;
|
sleep(POLL_INTERVAL).await;
|
||||||
}
|
}
|
||||||
@@ -180,11 +191,17 @@ impl ATTManager {
|
|||||||
if let Some(sender) = &state.sender {
|
if let Some(sender) = &state.sender {
|
||||||
sender.send(data.to_vec()).await.map_err(|e| {
|
sender.send(data.to_vec()).await.map_err(|e| {
|
||||||
error!("Failed to send packet to channel: {}", e);
|
error!("Failed to send packet to channel: {}", e);
|
||||||
Error::from(std::io::Error::new(std::io::ErrorKind::NotConnected, "L2CAP send channel closed"))
|
Error::from(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::NotConnected,
|
||||||
|
"L2CAP send channel closed",
|
||||||
|
))
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
error!("Cannot send packet, sender is not available.");
|
error!("Cannot send packet, sender is not available.");
|
||||||
Err(Error::from(std::io::Error::new(std::io::ErrorKind::NotConnected, "L2CAP stream not connected")))
|
Err(Error::from(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::NotConnected,
|
||||||
|
"L2CAP stream not connected",
|
||||||
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,11 +212,11 @@ impl ATTManager {
|
|||||||
Ok(Some(resp)) => Ok(resp),
|
Ok(Some(resp)) => Ok(resp),
|
||||||
Ok(None) => Err(Error::from(std::io::Error::new(
|
Ok(None) => Err(Error::from(std::io::Error::new(
|
||||||
std::io::ErrorKind::UnexpectedEof,
|
std::io::ErrorKind::UnexpectedEof,
|
||||||
"Response channel closed"
|
"Response channel closed",
|
||||||
))),
|
))),
|
||||||
Err(_) => Err(Error::from(std::io::Error::new(
|
Err(_) => Err(Error::from(std::io::Error::new(
|
||||||
std::io::ErrorKind::TimedOut,
|
std::io::ErrorKind::TimedOut,
|
||||||
"Response timeout"
|
"Response timeout",
|
||||||
))),
|
))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::io::Error;
|
|
||||||
use bluer::Adapter;
|
use bluer::Adapter;
|
||||||
use log::debug;
|
use log::debug;
|
||||||
|
use std::io::Error;
|
||||||
|
|
||||||
pub(crate) async fn find_connected_airpods(adapter: &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 target_uuid = uuid::Uuid::parse_str("74ec2172-0bad-4d01-8f77-997b2be0722a").unwrap();
|
||||||
@@ -8,20 +8,24 @@ pub(crate) async fn find_connected_airpods(adapter: &Adapter) -> bluer::Result<b
|
|||||||
let addrs = adapter.device_addresses().await?;
|
let addrs = adapter.device_addresses().await?;
|
||||||
for addr in addrs {
|
for addr in addrs {
|
||||||
let device = adapter.device(addr)?;
|
let device = adapter.device(addr)?;
|
||||||
if device.is_connected().await.unwrap_or(false) {
|
if device.is_connected().await.unwrap_or(false)
|
||||||
if let Ok(uuids) = device.uuids().await {
|
&& let Ok(uuids) = device.uuids().await
|
||||||
if let Some(uuids) = uuids {
|
&& let Some(uuids) = uuids
|
||||||
if uuids.iter().any(|u| *u == target_uuid) {
|
&& uuids.iter().any(|u| *u == target_uuid)
|
||||||
return Ok(device);
|
{
|
||||||
}
|
return Ok(device);
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(bluer::Error::from(Error::new(std::io::ErrorKind::NotFound, "No connected AirPods found")))
|
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>> {
|
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 addrs = adapter.device_addresses().await?;
|
||||||
let mut devices = Vec::new();
|
let mut devices = Vec::new();
|
||||||
for addr in addrs {
|
for addr in addrs {
|
||||||
@@ -38,5 +42,8 @@ pub async fn find_other_managed_devices(adapter: &Adapter, managed_macs: Vec<Str
|
|||||||
return Ok(devices);
|
return Ok(devices);
|
||||||
}
|
}
|
||||||
debug!("No other managed devices found");
|
debug!("No other managed devices found");
|
||||||
Err(bluer::Error::from(Error::new(std::io::ErrorKind::NotFound, "No other managed devices found")))
|
Err(bluer::Error::from(Error::new(
|
||||||
|
std::io::ErrorKind::NotFound,
|
||||||
|
"No other managed devices found",
|
||||||
|
)))
|
||||||
}
|
}
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
|
use crate::bluetooth::aacp::BatteryStatus;
|
||||||
|
use crate::devices::enums::{DeviceData, DeviceInformation, DeviceType};
|
||||||
|
use crate::ui::tray::MyTray;
|
||||||
|
use crate::utils::{ah, get_devices_path, get_preferences_path};
|
||||||
|
use aes::Aes128;
|
||||||
|
use aes::cipher::generic_array::GenericArray;
|
||||||
|
use aes::cipher::{BlockDecrypt, KeyInit};
|
||||||
use bluer::monitor::{Monitor, MonitorEvent, Pattern};
|
use bluer::monitor::{Monitor, MonitorEvent, Pattern};
|
||||||
use bluer::{Address, Session};
|
use bluer::{Address, Session};
|
||||||
use aes::Aes128;
|
|
||||||
use aes::cipher::{KeyInit, BlockDecrypt};
|
|
||||||
use aes::cipher::generic_array::GenericArray;
|
|
||||||
use std::collections::{HashMap, HashSet};
|
|
||||||
use log::{info, debug};
|
|
||||||
use serde_json;
|
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use hex;
|
use hex;
|
||||||
|
use log::{debug, info};
|
||||||
|
use serde_json;
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use crate::bluetooth::aacp::BatteryStatus;
|
|
||||||
use crate::ui::tray::MyTray;
|
|
||||||
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] {
|
fn decrypt(key: &[u8; 16], data: &[u8; 16]) -> [u8; 16] {
|
||||||
let cipher = Aes128::new(&GenericArray::from(*key));
|
let cipher = Aes128::new(&GenericArray::from(*key));
|
||||||
@@ -24,7 +24,8 @@ fn decrypt(key: &[u8; 16], data: &[u8; 16]) -> [u8; 16] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn verify_rpa(addr: &str, irk: &[u8; 16]) -> bool {
|
fn verify_rpa(addr: &str, irk: &[u8; 16]) -> bool {
|
||||||
let rpa: Vec<u8> = addr.split(':')
|
let rpa: Vec<u8> = addr
|
||||||
|
.split(':')
|
||||||
.map(|s| u8::from_str_radix(s, 16).unwrap())
|
.map(|s| u8::from_str_radix(s, 16).unwrap())
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -38,7 +39,10 @@ fn verify_rpa(addr: &str, irk: &[u8; 16]) -> bool {
|
|||||||
let hash_slice = &rpa[0..3];
|
let hash_slice = &rpa[0..3];
|
||||||
let hash: [u8; 3] = hash_slice.try_into().unwrap();
|
let hash: [u8; 3] = hash_slice.try_into().unwrap();
|
||||||
let computed_hash = ah(irk, &prand);
|
let computed_hash = ah(irk, &prand);
|
||||||
debug!("Verifying RPA: addr={}, hash={:?}, computed_hash={:?}", addr, hash, computed_hash);
|
debug!(
|
||||||
|
"Verifying RPA: addr={}, hash={:?}, computed_hash={:?}",
|
||||||
|
addr, hash, computed_hash
|
||||||
|
);
|
||||||
hash == computed_hash
|
hash == computed_hash
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,20 +51,19 @@ pub async fn start_le_monitor(tray_handle: Option<ksni::Handle<MyTray>>) -> blue
|
|||||||
let adapter = session.default_adapter().await?;
|
let adapter = session.default_adapter().await?;
|
||||||
adapter.set_powered(true).await?;
|
adapter.set_powered(true).await?;
|
||||||
|
|
||||||
let all_devices: HashMap<String, DeviceData> =
|
let all_devices: HashMap<String, DeviceData> = std::fs::read_to_string(get_devices_path())
|
||||||
std::fs::read_to_string(get_devices_path())
|
.ok()
|
||||||
.ok()
|
.and_then(|s| serde_json::from_str(&s).ok())
|
||||||
.and_then(|s| serde_json::from_str(&s).ok())
|
.unwrap_or_default();
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
let mut verified_macs: HashMap<Address, String> = HashMap::new();
|
let mut verified_macs: HashMap<Address, String> = HashMap::new();
|
||||||
let mut failed_macs: HashSet<Address> = HashSet::new();
|
let mut failed_macs: HashSet<Address> = HashSet::new();
|
||||||
let connecting_macs = Arc::new(Mutex::new(HashSet::<Address>::new()));
|
let connecting_macs = Arc::new(Mutex::new(HashSet::<Address>::new()));
|
||||||
|
|
||||||
let pattern = Pattern {
|
let pattern = Pattern {
|
||||||
data_type: 0xFF, // Manufacturer specific data
|
data_type: 0xFF, // Manufacturer specific data
|
||||||
start_position: 0,
|
start_position: 0,
|
||||||
content: vec![0x4C, 0x00], // Apple manufacturer ID (76) in LE
|
content: vec![0x4C, 0x00], // Apple manufacturer ID (76) in LE
|
||||||
};
|
};
|
||||||
|
|
||||||
let mm = adapter.monitor().await?;
|
let mm = adapter.monitor().await?;
|
||||||
@@ -97,20 +100,24 @@ pub async fn start_le_monitor(tray_handle: Option<ksni::Handle<MyTray>>) -> blue
|
|||||||
debug!("Checking RPA for device: {}", addr_str);
|
debug!("Checking RPA for device: {}", addr_str);
|
||||||
let mut found_mac = None;
|
let mut found_mac = None;
|
||||||
for (airpods_mac, device_data) in &all_devices {
|
for (airpods_mac, device_data) in &all_devices {
|
||||||
if device_data.type_ == DeviceType::AirPods {
|
if device_data.type_ == DeviceType::AirPods
|
||||||
if let Some(DeviceInformation::AirPods(info)) = &device_data.information {
|
&& let Some(DeviceInformation::AirPods(info)) = &device_data.information
|
||||||
if let Ok(irk_bytes) = hex::decode(&info.le_keys.irk) {
|
&& let Ok(irk_bytes) = hex::decode(&info.le_keys.irk)
|
||||||
if irk_bytes.len() == 16 {
|
&& 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);
|
let irk: [u8; 16] = irk_bytes.as_slice().try_into().unwrap();
|
||||||
if verify_rpa(&addr_str, &irk) {
|
debug!(
|
||||||
info!("Matched our device ({}) with the irk for {}", addr, airpods_mac);
|
"Verifying RPA {} for airpods MAC {} with IRK {}",
|
||||||
verified_macs.insert(addr, airpods_mac.clone());
|
addr_str, airpods_mac, info.le_keys.irk
|
||||||
found_mac = Some(airpods_mac.clone());
|
);
|
||||||
break;
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -124,16 +131,13 @@ pub async fn start_le_monitor(tray_handle: Option<ksni::Handle<MyTray>>) -> blue
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(ref mac) = matched_airpods_mac {
|
if let Some(ref mac) = matched_airpods_mac
|
||||||
if let Some(device_data) = all_devices.get(mac) {
|
&& let Some(device_data) = all_devices.get(mac)
|
||||||
if let Some(DeviceInformation::AirPods(info)) = &device_data.information {
|
&& let Some(DeviceInformation::AirPods(info)) = &device_data.information
|
||||||
if let Ok(enc_key_bytes) = hex::decode(&info.le_keys.enc_key) {
|
&& let Ok(enc_key_bytes) = hex::decode(&info.le_keys.enc_key)
|
||||||
if enc_key_bytes.len() == 16 {
|
&& enc_key_bytes.len() == 16
|
||||||
matched_enc_key = Some(enc_key_bytes.as_slice().try_into().unwrap());
|
{
|
||||||
}
|
matched_enc_key = Some(enc_key_bytes.as_slice().try_into().unwrap());
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if matched_airpods_mac.is_some() {
|
if matched_airpods_mac.is_some() {
|
||||||
@@ -144,123 +148,224 @@ pub async fn start_le_monitor(tray_handle: Option<ksni::Handle<MyTray>>) -> blue
|
|||||||
while let Some(ev) = events.next().await {
|
while let Some(ev) = events.next().await {
|
||||||
match ev {
|
match ev {
|
||||||
bluer::DeviceEvent::PropertyChanged(prop) => {
|
bluer::DeviceEvent::PropertyChanged(prop) => {
|
||||||
match prop {
|
if let bluer::DeviceProperty::ManufacturerData(data) = prop {
|
||||||
bluer::DeviceProperty::ManufacturerData(data) => {
|
if let Some(enc_key) = &matched_enc_key
|
||||||
if let Some(enc_key) = &matched_enc_key {
|
&& let Some(apple_data) = data.get(&76)
|
||||||
if let Some(apple_data) = data.get(&76) {
|
&& apple_data.len() > 20
|
||||||
if apple_data.len() > 20 {
|
{
|
||||||
let last_16: [u8; 16] = apple_data[apple_data.len() - 16..].try_into().unwrap();
|
let last_16: [u8; 16] =
|
||||||
let decrypted = decrypt(enc_key, &last_16);
|
apple_data[apple_data.len() - 16..].try_into().unwrap();
|
||||||
debug!("Decrypted data from airpods_mac {}: {}",
|
let decrypted = decrypt(enc_key, &last_16);
|
||||||
matched_airpods_mac.as_ref().unwrap_or(&"unknown".to_string()),
|
debug!(
|
||||||
hex::encode(&decrypted));
|
"Decrypted data from airpods_mac {}: {}",
|
||||||
|
matched_airpods_mac
|
||||||
|
.as_ref()
|
||||||
|
.unwrap_or(&"unknown".to_string()),
|
||||||
|
hex::encode(decrypted)
|
||||||
|
);
|
||||||
|
|
||||||
let connection_state = apple_data[10] as usize;
|
let connection_state = apple_data[10] as usize;
|
||||||
debug!("Connection state: {}", connection_state);
|
debug!("Connection state: {}", connection_state);
|
||||||
if connection_state == 0x00 {
|
if connection_state == 0x00 {
|
||||||
let pref_path = get_preferences_path();
|
let pref_path = get_preferences_path();
|
||||||
let preferences: HashMap<String, HashMap<String, bool>> =
|
let preferences: HashMap<
|
||||||
std::fs::read_to_string(&pref_path)
|
String,
|
||||||
.ok()
|
HashMap<String, bool>,
|
||||||
.and_then(|s| serde_json::from_str(&s).ok())
|
> = std::fs::read_to_string(&pref_path)
|
||||||
.unwrap_or_default();
|
.ok()
|
||||||
let auto_connect = preferences.get(matched_airpods_mac.as_ref().unwrap())
|
.and_then(|s| serde_json::from_str(&s).ok())
|
||||||
.and_then(|prefs| prefs.get("autoConnect"))
|
.unwrap_or_default();
|
||||||
.copied()
|
let auto_connect = preferences
|
||||||
.unwrap_or(true);
|
.get(matched_airpods_mac.as_ref().unwrap())
|
||||||
debug!("Auto-connect preference for {}: {}", matched_airpods_mac.as_ref().unwrap(), auto_connect);
|
.and_then(|prefs| prefs.get("autoConnect"))
|
||||||
if auto_connect {
|
.copied()
|
||||||
let real_address = Address::from_str(&addr_str).unwrap();
|
.unwrap_or(true);
|
||||||
let mut cm = connecting_macs_clone.lock().await;
|
debug!(
|
||||||
if cm.contains(&real_address) {
|
"Auto-connect preference for {}: {}",
|
||||||
info!("Already connecting to {}, skipping duplicate attempt.", matched_airpods_mac.as_ref().unwrap());
|
matched_airpods_mac.as_ref().unwrap(),
|
||||||
return;
|
auto_connect
|
||||||
}
|
);
|
||||||
cm.insert(real_address);
|
if auto_connect {
|
||||||
// let adapter_clone = adapter_monitor_clone.clone();
|
let real_address =
|
||||||
// let real_device = adapter_clone.device(real_address).unwrap();
|
Address::from_str(&addr_str).unwrap();
|
||||||
info!("AirPods are disconnected, attempting to connect to {}", matched_airpods_mac.as_ref().unwrap());
|
let mut cm = connecting_macs_clone.lock().await;
|
||||||
// if let Err(e) = real_device.connect().await {
|
if cm.contains(&real_address) {
|
||||||
// info!("Failed to connect to AirPods {}: {}", matched_airpods_mac.as_ref().unwrap(), e);
|
info!(
|
||||||
// } else {
|
"Already connecting to {}, skipping duplicate attempt.",
|
||||||
// info!("Successfully connected to AirPods {}", matched_airpods_mac.as_ref().unwrap());
|
matched_airpods_mac.as_ref().unwrap()
|
||||||
// }
|
);
|
||||||
// call bluetoothctl connect <mac> for now, I don't know why bluer connect isn't working
|
return;
|
||||||
let output = tokio::process::Command::new("bluetoothctl")
|
}
|
||||||
.arg("connect")
|
cm.insert(real_address);
|
||||||
.arg(matched_airpods_mac.as_ref().unwrap())
|
// let adapter_clone = adapter_monitor_clone.clone();
|
||||||
.output()
|
// let real_device = adapter_clone.device(real_address).unwrap();
|
||||||
.await;
|
info!(
|
||||||
match output {
|
"AirPods are disconnected, attempting to connect to {}",
|
||||||
Ok(output) => {
|
matched_airpods_mac.as_ref().unwrap()
|
||||||
if output.status.success() {
|
);
|
||||||
info!("Successfully connected to AirPods {}", matched_airpods_mac.as_ref().unwrap());
|
// if let Err(e) = real_device.connect().await {
|
||||||
cm.remove(&real_address);
|
// info!("Failed to connect to AirPods {}: {}", matched_airpods_mac.as_ref().unwrap(), e);
|
||||||
} else {
|
// } else {
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
// info!("Successfully connected to AirPods {}", matched_airpods_mac.as_ref().unwrap());
|
||||||
info!("Failed to connect to AirPods {}: {}", matched_airpods_mac.as_ref().unwrap(), stderr);
|
// }
|
||||||
}
|
// call bluetoothctl connect <mac> for now, I don't know why bluer connect isn't working
|
||||||
}
|
let output =
|
||||||
Err(e) => {
|
tokio::process::Command::new("bluetoothctl")
|
||||||
info!("Failed to execute bluetoothctl to connect to AirPods {}: {}", matched_airpods_mac.as_ref().unwrap(), e);
|
.arg("connect")
|
||||||
}
|
.arg(matched_airpods_mac.as_ref().unwrap())
|
||||||
}
|
.output()
|
||||||
info!("Auto-connect is disabled for {}, not attempting to connect.", matched_airpods_mac.as_ref().unwrap());
|
.await;
|
||||||
|
match output {
|
||||||
|
Ok(output) => {
|
||||||
|
if output.status.success() {
|
||||||
|
info!(
|
||||||
|
"Successfully connected to AirPods {}",
|
||||||
|
matched_airpods_mac
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
cm.remove(&real_address);
|
||||||
|
} else {
|
||||||
|
let stderr = String::from_utf8_lossy(
|
||||||
|
&output.stderr,
|
||||||
|
);
|
||||||
|
info!(
|
||||||
|
"Failed to connect to AirPods {}: {}",
|
||||||
|
matched_airpods_mac
|
||||||
|
.as_ref()
|
||||||
|
.unwrap(),
|
||||||
|
stderr
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Err(e) => {
|
||||||
let status = apple_data[5] as usize;
|
info!(
|
||||||
let primary_left = (status >> 5) & 0x01 == 1;
|
"Failed to execute bluetoothctl to connect to AirPods {}: {}",
|
||||||
let this_in_case = (status >> 6) & 0x01 == 1;
|
matched_airpods_mac.as_ref().unwrap(),
|
||||||
let xor_factor = primary_left ^ this_in_case;
|
e
|
||||||
let is_left_in_ear = if xor_factor { (status & 0x02) != 0 } else { (status & 0x08) != 0 };
|
);
|
||||||
let is_right_in_ear = if xor_factor { (status & 0x08) != 0 } else { (status & 0x02) != 0 };
|
|
||||||
let is_flipped = !primary_left;
|
|
||||||
|
|
||||||
let left_byte_index = if is_flipped { 2 } else { 1 };
|
|
||||||
let right_byte_index = if is_flipped { 1 } else { 2 };
|
|
||||||
|
|
||||||
let left_byte = decrypted[left_byte_index] as i32;
|
|
||||||
let right_byte = decrypted[right_byte_index] as i32;
|
|
||||||
let case_byte = decrypted[3] as i32;
|
|
||||||
|
|
||||||
let (left_battery, left_charging) = if left_byte == 0xff {
|
|
||||||
(0, false)
|
|
||||||
} else {
|
|
||||||
(left_byte & 0x7F, (left_byte & 0x80) != 0)
|
|
||||||
};
|
|
||||||
let (right_battery, right_charging) = if right_byte == 0xff {
|
|
||||||
(0, false)
|
|
||||||
} else {
|
|
||||||
(right_byte & 0x7F, (right_byte & 0x80) != 0)
|
|
||||||
};
|
|
||||||
let (case_battery, case_charging) = if case_byte == 0xff {
|
|
||||||
(0, false)
|
|
||||||
} else {
|
|
||||||
(case_byte & 0x7F, (case_byte & 0x80) != 0)
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(handle) = &tray_handle_clone {
|
|
||||||
handle.update(|tray: &mut MyTray| {
|
|
||||||
tray.battery_l = if left_byte == 0xff { None } else { Some(left_battery as u8) };
|
|
||||||
tray.battery_l_status = if left_byte == 0xff { Some(BatteryStatus::Disconnected) } else if left_charging { Some(BatteryStatus::Charging) } else { Some(BatteryStatus::NotCharging) };
|
|
||||||
tray.battery_r = if right_byte == 0xff { None } else { Some(right_battery as u8) };
|
|
||||||
tray.battery_r_status = if right_byte == 0xff { Some(BatteryStatus::Disconnected) } else if right_charging { Some(BatteryStatus::Charging) } else { Some(BatteryStatus::NotCharging) };
|
|
||||||
tray.battery_c = if case_byte == 0xff { None } else { Some(case_battery as u8) };
|
|
||||||
tray.battery_c_status = if case_byte == 0xff { Some(BatteryStatus::Disconnected) } else if case_charging { Some(BatteryStatus::Charging) } else { Some(BatteryStatus::NotCharging) };
|
|
||||||
}).await;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
debug!("Battery status: Left: {}, Right: {}, Case: {}, InEar: L:{} R:{}",
|
|
||||||
if left_byte == 0xff { "disconnected".to_string() } else { format!("{}% (charging: {})", left_battery, left_charging) },
|
|
||||||
if right_byte == 0xff { "disconnected".to_string() } else { format!("{}% (charging: {})", right_battery, right_charging) },
|
|
||||||
if case_byte == 0xff { "disconnected".to_string() } else { format!("{}% (charging: {})", case_battery, case_charging) },
|
|
||||||
is_left_in_ear, is_right_in_ear);
|
|
||||||
}
|
}
|
||||||
|
info!(
|
||||||
|
"Auto-connect is disabled for {}, not attempting to connect.",
|
||||||
|
matched_airpods_mac.as_ref().unwrap()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let status = apple_data[5] as usize;
|
||||||
|
let primary_left = (status >> 5) & 0x01 == 1;
|
||||||
|
let this_in_case = (status >> 6) & 0x01 == 1;
|
||||||
|
let xor_factor = primary_left ^ this_in_case;
|
||||||
|
let is_left_in_ear = if xor_factor {
|
||||||
|
(status & 0x02) != 0
|
||||||
|
} else {
|
||||||
|
(status & 0x08) != 0
|
||||||
|
};
|
||||||
|
let is_right_in_ear = if xor_factor {
|
||||||
|
(status & 0x08) != 0
|
||||||
|
} else {
|
||||||
|
(status & 0x02) != 0
|
||||||
|
};
|
||||||
|
let is_flipped = !primary_left;
|
||||||
|
|
||||||
|
let left_byte_index = if is_flipped { 2 } else { 1 };
|
||||||
|
let right_byte_index = if is_flipped { 1 } else { 2 };
|
||||||
|
|
||||||
|
let left_byte = decrypted[left_byte_index] as i32;
|
||||||
|
let right_byte = decrypted[right_byte_index] as i32;
|
||||||
|
let case_byte = decrypted[3] as i32;
|
||||||
|
|
||||||
|
let (left_battery, left_charging) = if left_byte == 0xff {
|
||||||
|
(0, false)
|
||||||
|
} else {
|
||||||
|
(left_byte & 0x7F, (left_byte & 0x80) != 0)
|
||||||
|
};
|
||||||
|
let (right_battery, right_charging) = if right_byte == 0xff
|
||||||
|
{
|
||||||
|
(0, false)
|
||||||
|
} else {
|
||||||
|
(right_byte & 0x7F, (right_byte & 0x80) != 0)
|
||||||
|
};
|
||||||
|
let (case_battery, case_charging) = if case_byte == 0xff {
|
||||||
|
(0, false)
|
||||||
|
} else {
|
||||||
|
(case_byte & 0x7F, (case_byte & 0x80) != 0)
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(handle) = &tray_handle_clone {
|
||||||
|
handle
|
||||||
|
.update(|tray: &mut MyTray| {
|
||||||
|
tray.battery_l = if left_byte == 0xff {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(left_battery as u8)
|
||||||
|
};
|
||||||
|
tray.battery_l_status = if left_byte == 0xff {
|
||||||
|
Some(BatteryStatus::Disconnected)
|
||||||
|
} else if left_charging {
|
||||||
|
Some(BatteryStatus::Charging)
|
||||||
|
} else {
|
||||||
|
Some(BatteryStatus::NotCharging)
|
||||||
|
};
|
||||||
|
tray.battery_r = if right_byte == 0xff {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(right_battery as u8)
|
||||||
|
};
|
||||||
|
tray.battery_r_status = if right_byte == 0xff {
|
||||||
|
Some(BatteryStatus::Disconnected)
|
||||||
|
} else if right_charging {
|
||||||
|
Some(BatteryStatus::Charging)
|
||||||
|
} else {
|
||||||
|
Some(BatteryStatus::NotCharging)
|
||||||
|
};
|
||||||
|
tray.battery_c = if case_byte == 0xff {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(case_battery as u8)
|
||||||
|
};
|
||||||
|
tray.battery_c_status = if case_byte == 0xff {
|
||||||
|
Some(BatteryStatus::Disconnected)
|
||||||
|
} else if case_charging {
|
||||||
|
Some(BatteryStatus::Charging)
|
||||||
|
} else {
|
||||||
|
Some(BatteryStatus::NotCharging)
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
"Battery status: Left: {}, Right: {}, Case: {}, InEar: L:{} R:{}",
|
||||||
|
if left_byte == 0xff {
|
||||||
|
"disconnected".to_string()
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"{}% (charging: {})",
|
||||||
|
left_battery, left_charging
|
||||||
|
)
|
||||||
|
},
|
||||||
|
if right_byte == 0xff {
|
||||||
|
"disconnected".to_string()
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"{}% (charging: {})",
|
||||||
|
right_battery, right_charging
|
||||||
|
)
|
||||||
|
},
|
||||||
|
if case_byte == 0xff {
|
||||||
|
"disconnected".to_string()
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"{}% (charging: {})",
|
||||||
|
case_battery, case_charging
|
||||||
|
)
|
||||||
|
},
|
||||||
|
is_left_in_ear,
|
||||||
|
is_right_in_ear
|
||||||
|
);
|
||||||
}
|
}
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
use crate::bluetooth::aacp::AACPManager;
|
use crate::bluetooth::aacp::AACPManager;
|
||||||
use crate::bluetooth::att::ATTManager;
|
use crate::bluetooth::att::ATTManager;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub struct DeviceManagers {
|
pub struct DeviceManagers {
|
||||||
att: Option<Arc<ATTManager>>,
|
att: Option<Arc<ATTManager>>,
|
||||||
@@ -9,16 +9,25 @@ pub struct DeviceManagers {
|
|||||||
|
|
||||||
impl DeviceManagers {
|
impl DeviceManagers {
|
||||||
pub fn with_aacp(aacp: AACPManager) -> Self {
|
pub fn with_aacp(aacp: AACPManager) -> Self {
|
||||||
Self { att: None, aacp: Some(Arc::new(aacp)) }
|
Self {
|
||||||
|
att: None,
|
||||||
|
aacp: Some(Arc::new(aacp)),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_att(att: ATTManager) -> Self {
|
pub fn with_att(att: ATTManager) -> Self {
|
||||||
Self { att: Some(Arc::new(att)), aacp: None }
|
Self {
|
||||||
|
att: Some(Arc::new(att)),
|
||||||
|
aacp: None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// keeping the att for airpods optional as it requires changes in system bluez config
|
// keeping the att for airpods optional as it requires changes in system bluez config
|
||||||
pub fn with_both(aacp: AACPManager, att: ATTManager) -> Self {
|
pub fn with_both(aacp: AACPManager, att: ATTManager) -> Self {
|
||||||
Self { att: Some(Arc::new(att)), aacp: Some(Arc::new(aacp)) }
|
Self {
|
||||||
|
att: Some(Arc::new(att)),
|
||||||
|
aacp: Some(Arc::new(aacp)),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_aacp(&mut self, manager: AACPManager) {
|
pub fn set_aacp(&mut self, manager: AACPManager) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
pub(crate) mod discovery;
|
|
||||||
pub mod aacp;
|
pub mod aacp;
|
||||||
pub mod att;
|
pub mod att;
|
||||||
|
pub(crate) mod discovery;
|
||||||
pub mod le;
|
pub mod le;
|
||||||
pub mod managers;
|
pub mod managers;
|
||||||
@@ -1,16 +1,15 @@
|
|||||||
use crate::bluetooth::aacp::{AACPManager, ProximityKeyType, AACPEvent, AirPodsLEKeys};
|
|
||||||
use crate::bluetooth::aacp::ControlCommandIdentifiers;
|
use crate::bluetooth::aacp::ControlCommandIdentifiers;
|
||||||
use crate::bluetooth::att::ATTManager;
|
use crate::bluetooth::aacp::{AACPEvent, AACPManager, AirPodsLEKeys, ProximityKeyType};
|
||||||
use crate::media_controller::MediaController;
|
use crate::media_controller::MediaController;
|
||||||
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::BluetoothUIMessage;
|
use crate::ui::messages::BluetoothUIMessage;
|
||||||
|
use crate::ui::tray::MyTray;
|
||||||
|
use bluer::Address;
|
||||||
|
use ksni::Handle;
|
||||||
|
use log::{debug, error, info};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
use tokio::time::{Duration, sleep};
|
||||||
|
|
||||||
pub struct AirPodsDevice {
|
pub struct AirPodsDevice {
|
||||||
pub mac_address: Address,
|
pub mac_address: Address,
|
||||||
@@ -34,7 +33,9 @@ impl AirPodsDevice {
|
|||||||
// att_manager.connect(mac_address).await.expect("Failed to connect ATT");
|
// att_manager.connect(mac_address).await.expect("Failed to connect ATT");
|
||||||
|
|
||||||
if let Some(handle) = &tray_handle {
|
if let Some(handle) = &tray_handle {
|
||||||
handle.update(|tray: &mut MyTray| tray.connected = true).await;
|
handle
|
||||||
|
.update(|tray: &mut MyTray| tray.connected = true)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("Sending handshake");
|
info!("Sending handshake");
|
||||||
@@ -62,24 +63,39 @@ impl AirPodsDevice {
|
|||||||
}
|
}
|
||||||
|
|
||||||
info!("Requesting Proximity Keys: IRK and ENC_KEY");
|
info!("Requesting Proximity Keys: IRK and ENC_KEY");
|
||||||
if let Err(e) = aacp_manager.send_proximity_keys_request(
|
if let Err(e) = aacp_manager
|
||||||
vec![ProximityKeyType::Irk, ProximityKeyType::EncKey],
|
.send_proximity_keys_request(vec![ProximityKeyType::Irk, ProximityKeyType::EncKey])
|
||||||
).await {
|
.await
|
||||||
|
{
|
||||||
error!("Failed to request proximity keys: {}", e);
|
error!("Failed to request proximity keys: {}", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
let session = bluer::Session::new().await.expect("Failed to get bluer session");
|
let session = bluer::Session::new()
|
||||||
let adapter = session.default_adapter().await.expect("Failed to get default adapter");
|
.await
|
||||||
let local_mac = adapter.address().await.expect("Failed to get adapter address").to_string();
|
.expect("Failed to get bluer session");
|
||||||
|
let adapter = session
|
||||||
|
.default_adapter()
|
||||||
|
.await
|
||||||
|
.expect("Failed to get default adapter");
|
||||||
|
let local_mac = adapter
|
||||||
|
.address()
|
||||||
|
.await
|
||||||
|
.expect("Failed to get adapter address")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
let media_controller = Arc::new(Mutex::new(MediaController::new(mac_address.to_string(), local_mac.clone())));
|
let media_controller = Arc::new(Mutex::new(MediaController::new(
|
||||||
|
mac_address.to_string(),
|
||||||
|
local_mac.clone(),
|
||||||
|
)));
|
||||||
let mc_clone = media_controller.clone();
|
let mc_clone = media_controller.clone();
|
||||||
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
|
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
|
||||||
let (command_tx, mut command_rx) = tokio::sync::mpsc::unbounded_channel();
|
let (command_tx, mut command_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||||
|
|
||||||
aacp_manager.set_event_channel(tx).await;
|
aacp_manager.set_event_channel(tx).await;
|
||||||
if let Some(handle) = &tray_handle {
|
if let Some(handle) = &tray_handle {
|
||||||
handle.update(|tray: &mut MyTray| tray.command_tx = Some(command_tx.clone())).await;
|
handle
|
||||||
|
.update(|tray: &mut MyTray| tray.command_tx = Some(command_tx.clone()))
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
let aacp_manager_clone = aacp_manager.clone();
|
let aacp_manager_clone = aacp_manager.clone();
|
||||||
@@ -93,54 +109,80 @@ impl AirPodsDevice {
|
|||||||
|
|
||||||
let mc_listener = media_controller.lock().await;
|
let mc_listener = media_controller.lock().await;
|
||||||
let aacp_manager_clone_listener = aacp_manager.clone();
|
let aacp_manager_clone_listener = aacp_manager.clone();
|
||||||
mc_listener.start_playback_listener(aacp_manager_clone_listener, command_tx.clone()).await;
|
mc_listener
|
||||||
|
.start_playback_listener(aacp_manager_clone_listener, command_tx.clone())
|
||||||
|
.await;
|
||||||
drop(mc_listener);
|
drop(mc_listener);
|
||||||
|
|
||||||
let (listening_mode_tx, mut listening_mode_rx) = tokio::sync::mpsc::unbounded_channel();
|
let (listening_mode_tx, mut listening_mode_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||||
aacp_manager.subscribe_to_control_command(ControlCommandIdentifiers::ListeningMode, listening_mode_tx).await;
|
aacp_manager
|
||||||
|
.subscribe_to_control_command(
|
||||||
|
ControlCommandIdentifiers::ListeningMode,
|
||||||
|
listening_mode_tx,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
let tray_handle_clone = tray_handle.clone();
|
let tray_handle_clone = tray_handle.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
while let Some(value) = listening_mode_rx.recv().await {
|
while let Some(value) = listening_mode_rx.recv().await {
|
||||||
if let Some(handle) = &tray_handle_clone {
|
if let Some(handle) = &tray_handle_clone {
|
||||||
handle.update(|tray: &mut MyTray| {
|
handle
|
||||||
tray.listening_mode = Some(value[0]);
|
.update(|tray: &mut MyTray| {
|
||||||
}).await;
|
tray.listening_mode = Some(value[0]);
|
||||||
|
})
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let (allow_off_tx, mut allow_off_rx) = tokio::sync::mpsc::unbounded_channel();
|
let (allow_off_tx, mut allow_off_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||||
aacp_manager.subscribe_to_control_command(ControlCommandIdentifiers::AllowOffOption, allow_off_tx).await;
|
aacp_manager
|
||||||
|
.subscribe_to_control_command(ControlCommandIdentifiers::AllowOffOption, allow_off_tx)
|
||||||
|
.await;
|
||||||
let tray_handle_clone = tray_handle.clone();
|
let tray_handle_clone = tray_handle.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
while let Some(value) = allow_off_rx.recv().await {
|
while let Some(value) = allow_off_rx.recv().await {
|
||||||
if let Some(handle) = &tray_handle_clone {
|
if let Some(handle) = &tray_handle_clone {
|
||||||
handle.update(|tray: &mut MyTray| {
|
handle
|
||||||
tray.allow_off_option = Some(value[0]);
|
.update(|tray: &mut MyTray| {
|
||||||
}).await;
|
tray.allow_off_option = Some(value[0]);
|
||||||
|
})
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let (conversation_detect_tx, mut conversation_detect_rx) = tokio::sync::mpsc::unbounded_channel();
|
let (conversation_detect_tx, mut conversation_detect_rx) =
|
||||||
aacp_manager.subscribe_to_control_command(ControlCommandIdentifiers::ConversationDetectConfig, conversation_detect_tx).await;
|
tokio::sync::mpsc::unbounded_channel();
|
||||||
|
aacp_manager
|
||||||
|
.subscribe_to_control_command(
|
||||||
|
ControlCommandIdentifiers::ConversationDetectConfig,
|
||||||
|
conversation_detect_tx,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
let tray_handle_clone = tray_handle.clone();
|
let tray_handle_clone = tray_handle.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
while let Some(value) = conversation_detect_rx.recv().await {
|
while let Some(value) = conversation_detect_rx.recv().await {
|
||||||
if let Some(handle) = &tray_handle_clone {
|
if let Some(handle) = &tray_handle_clone {
|
||||||
handle.update(|tray: &mut MyTray| {
|
handle
|
||||||
tray.conversation_detect_enabled = Some(value[0] == 0x01);
|
.update(|tray: &mut MyTray| {
|
||||||
}).await;
|
tray.conversation_detect_enabled = Some(value[0] == 0x01);
|
||||||
|
})
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let (owns_connection_tx, mut owns_connection_rx) = tokio::sync::mpsc::unbounded_channel();
|
let (owns_connection_tx, mut owns_connection_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||||
aacp_manager.subscribe_to_control_command(ControlCommandIdentifiers::OwnsConnection, owns_connection_tx).await;
|
aacp_manager
|
||||||
|
.subscribe_to_control_command(
|
||||||
|
ControlCommandIdentifiers::OwnsConnection,
|
||||||
|
owns_connection_tx,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
let mc_clone_owns = media_controller.clone();
|
let mc_clone_owns = media_controller.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
while let Some(value) = owns_connection_rx.recv().await {
|
while let Some(value) = owns_connection_rx.recv().await {
|
||||||
let owns = value.get(0).copied().unwrap_or(0) != 0;
|
let owns = value.first().copied().unwrap_or(0) != 0;
|
||||||
if !owns {
|
if !owns {
|
||||||
info!("Lost ownership, pausing media and disconnecting audio");
|
info!("Lost ownership, pausing media and disconnecting audio");
|
||||||
let controller = mc_clone_owns.lock().await;
|
let controller = mc_clone_owns.lock().await;
|
||||||
@@ -159,42 +201,62 @@ impl AirPodsDevice {
|
|||||||
let event_clone = event.clone();
|
let event_clone = event.clone();
|
||||||
match event {
|
match event {
|
||||||
AACPEvent::EarDetection(old_status, new_status) => {
|
AACPEvent::EarDetection(old_status, new_status) => {
|
||||||
debug!("Received EarDetection event: old_status={:?}, new_status={:?}", old_status, new_status);
|
debug!(
|
||||||
|
"Received EarDetection event: old_status={:?}, new_status={:?}",
|
||||||
|
old_status, new_status
|
||||||
|
);
|
||||||
let controller = mc_clone.lock().await;
|
let controller = mc_clone.lock().await;
|
||||||
debug!("Calling handle_ear_detection with old_status: {:?}, new_status: {:?}", old_status, new_status);
|
debug!(
|
||||||
controller.handle_ear_detection(old_status, new_status).await;
|
"Calling handle_ear_detection with old_status: {:?}, new_status: {:?}",
|
||||||
|
old_status, new_status
|
||||||
|
);
|
||||||
|
controller
|
||||||
|
.handle_ear_detection(old_status, new_status)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
AACPEvent::BatteryInfo(battery_info) => {
|
AACPEvent::BatteryInfo(battery_info) => {
|
||||||
debug!("Received BatteryInfo event: {:?}", battery_info);
|
debug!("Received BatteryInfo event: {:?}", battery_info);
|
||||||
if let Some(handle) = &tray_handle {
|
if let Some(handle) = &tray_handle {
|
||||||
handle.update(|tray: &mut MyTray| {
|
handle
|
||||||
for b in &battery_info {
|
.update(|tray: &mut MyTray| {
|
||||||
match b.component as u8 {
|
for b in &battery_info {
|
||||||
0x02 => {
|
match b.component as u8 {
|
||||||
tray.battery_r = Some(b.level);
|
0x01 => {
|
||||||
tray.battery_r_status = Some(b.status);
|
tray.battery_headphone = Some(b.level);
|
||||||
|
tray.battery_headphone_status = Some(b.status);
|
||||||
|
}
|
||||||
|
0x02 => {
|
||||||
|
tray.battery_r = Some(b.level);
|
||||||
|
tray.battery_r_status = Some(b.status);
|
||||||
|
}
|
||||||
|
0x04 => {
|
||||||
|
tray.battery_l = Some(b.level);
|
||||||
|
tray.battery_l_status = Some(b.status);
|
||||||
|
}
|
||||||
|
0x08 => {
|
||||||
|
tray.battery_c = Some(b.level);
|
||||||
|
tray.battery_c_status = Some(b.status);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
0x04 => {
|
|
||||||
tray.battery_l = Some(b.level);
|
|
||||||
tray.battery_l_status = Some(b.status);
|
|
||||||
}
|
|
||||||
0x08 => {
|
|
||||||
tray.battery_c = Some(b.level);
|
|
||||||
tray.battery_c_status = Some(b.status);
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}).await;
|
.await;
|
||||||
}
|
}
|
||||||
debug!("Updated tray with new battery info");
|
debug!("Updated tray with new battery info");
|
||||||
|
|
||||||
let _ = ui_tx_clone.send(BluetoothUIMessage::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");
|
debug!("Sent BatteryInfo event to UI");
|
||||||
}
|
}
|
||||||
AACPEvent::ControlCommand(status) => {
|
AACPEvent::ControlCommand(status) => {
|
||||||
debug!("Received ControlCommand event: {:?}", status);
|
debug!("Received ControlCommand event: {:?}", status);
|
||||||
let _ = ui_tx_clone.send(BluetoothUIMessage::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");
|
debug!("Sent ControlCommand event to UI");
|
||||||
}
|
}
|
||||||
AACPEvent::ConversationalAwareness(status) => {
|
AACPEvent::ConversationalAwareness(status) => {
|
||||||
@@ -205,37 +267,60 @@ impl AirPodsDevice {
|
|||||||
AACPEvent::ConnectedDevices(old_devices, new_devices) => {
|
AACPEvent::ConnectedDevices(old_devices, new_devices) => {
|
||||||
let local_mac = local_mac_events.clone();
|
let local_mac = local_mac_events.clone();
|
||||||
let new_devices_filtered = new_devices.iter().filter(|new_device| {
|
let new_devices_filtered = new_devices.iter().filter(|new_device| {
|
||||||
let not_in_old = old_devices.iter().all(|old_device| old_device.mac != new_device.mac);
|
let not_in_old = old_devices
|
||||||
|
.iter()
|
||||||
|
.all(|old_device| old_device.mac != new_device.mac);
|
||||||
let not_local = new_device.mac != local_mac;
|
let not_local = new_device.mac != local_mac;
|
||||||
not_in_old && not_local
|
not_in_old && not_local
|
||||||
});
|
});
|
||||||
|
|
||||||
for device in new_devices_filtered {
|
for device in new_devices_filtered {
|
||||||
info!("New connected device: {}, info1: {}, info2: {}", device.mac, device.info1, device.info2);
|
info!(
|
||||||
info!("Sending new Tipi packet for device {}, and sending media info to the device", device.mac);
|
"New connected device: {}, info1: {}, info2: {}",
|
||||||
|
device.mac, device.info1, device.info2
|
||||||
|
);
|
||||||
|
info!(
|
||||||
|
"Sending new Tipi packet for device {}, and sending media info to the device",
|
||||||
|
device.mac
|
||||||
|
);
|
||||||
let aacp_manager_clone = aacp_manager_clone_events.clone();
|
let aacp_manager_clone = aacp_manager_clone_events.clone();
|
||||||
let local_mac_clone = local_mac.clone();
|
let local_mac_clone = local_mac.clone();
|
||||||
let device_mac_clone = device.mac.clone();
|
let device_mac_clone = device.mac.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
if let Err(e) = aacp_manager_clone.send_media_information_new_device(&local_mac_clone, &device_mac_clone).await {
|
if let Err(e) = aacp_manager_clone
|
||||||
|
.send_media_information_new_device(
|
||||||
|
&local_mac_clone,
|
||||||
|
&device_mac_clone,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
error!("Failed to send media info new device: {}", e);
|
error!("Failed to send media info new device: {}", e);
|
||||||
}
|
}
|
||||||
if let Err(e) = aacp_manager_clone.send_add_tipi_device(&local_mac_clone, &device_mac_clone).await {
|
if let Err(e) = aacp_manager_clone
|
||||||
|
.send_add_tipi_device(&local_mac_clone, &device_mac_clone)
|
||||||
|
.await
|
||||||
|
{
|
||||||
error!("Failed to send add tipi device: {}", e);
|
error!("Failed to send add tipi device: {}", e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AACPEvent::OwnershipToFalseRequest => {
|
AACPEvent::OwnershipToFalseRequest => {
|
||||||
info!("Received ownership to false request. Setting ownership to false and pausing media.");
|
info!(
|
||||||
let _ = command_tx_clone.send((ControlCommandIdentifiers::OwnsConnection, vec![0x00]));
|
"Received ownership to false request. Setting ownership to false and pausing media."
|
||||||
|
);
|
||||||
|
let _ = command_tx_clone
|
||||||
|
.send((ControlCommandIdentifiers::OwnsConnection, vec![0x00]));
|
||||||
let controller = mc_clone.lock().await;
|
let controller = mc_clone.lock().await;
|
||||||
controller.pause_all_media().await;
|
controller.pause_all_media().await;
|
||||||
controller.deactivate_a2dp_profile().await;
|
controller.deactivate_a2dp_profile().await;
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
debug!("Received unhandled AACP event: {:?}", event);
|
debug!("Received unhandled AACP event: {:?}", event);
|
||||||
let _ = ui_tx_clone.send(BluetoothUIMessage::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");
|
debug!("Sent unhandled AACP event to UI");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -265,5 +350,5 @@ pub struct AirPodsInformation {
|
|||||||
pub left_serial_number: String,
|
pub left_serial_number: String,
|
||||||
pub right_serial_number: String,
|
pub right_serial_number: String,
|
||||||
pub version3: String,
|
pub version3: String,
|
||||||
pub le_keys: AirPodsLEKeys
|
pub le_keys: AirPodsLEKeys,
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
use std::fmt::Display;
|
use crate::bluetooth::aacp::BatteryInfo;
|
||||||
use iced::widget::{combo_box, ComboBox};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use crate::devices::airpods::AirPodsInformation;
|
use crate::devices::airpods::AirPodsInformation;
|
||||||
use crate::devices::nothing::NothingInformation;
|
use crate::devices::nothing::NothingInformation;
|
||||||
|
use iced::widget::combo_box;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
#[derive(PartialEq)]
|
|
||||||
pub enum DeviceType {
|
pub enum DeviceType {
|
||||||
AirPods,
|
AirPods,
|
||||||
Nothing
|
Nothing,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for DeviceType {
|
impl Display for DeviceType {
|
||||||
@@ -20,12 +20,11 @@ impl Display for DeviceType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[serde(tag = "kind", content = "data")]
|
#[serde(tag = "kind", content = "data")]
|
||||||
pub enum DeviceInformation {
|
pub enum DeviceInformation {
|
||||||
AirPods(AirPodsInformation),
|
AirPods(AirPodsInformation),
|
||||||
Nothing(NothingInformation)
|
Nothing(NothingInformation),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -35,7 +34,6 @@ pub struct DeviceData {
|
|||||||
pub information: Option<DeviceInformation>,
|
pub information: Option<DeviceInformation>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum DeviceState {
|
pub enum DeviceState {
|
||||||
AirPods(AirPodsState),
|
AirPods(AirPodsState),
|
||||||
@@ -58,7 +56,8 @@ pub struct AirPodsState {
|
|||||||
pub noise_control_state: combo_box::State<AirPodsNoiseControlMode>,
|
pub noise_control_state: combo_box::State<AirPodsNoiseControlMode>,
|
||||||
pub conversation_awareness_enabled: bool,
|
pub conversation_awareness_enabled: bool,
|
||||||
pub personalized_volume_enabled: bool,
|
pub personalized_volume_enabled: bool,
|
||||||
pub allow_off_mode: bool
|
pub allow_off_mode: bool,
|
||||||
|
pub battery: Vec<BatteryInfo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@@ -66,7 +65,7 @@ pub enum AirPodsNoiseControlMode {
|
|||||||
Off,
|
Off,
|
||||||
NoiseCancellation,
|
NoiseCancellation,
|
||||||
Transparency,
|
Transparency,
|
||||||
Adaptive
|
Adaptive,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for AirPodsNoiseControlMode {
|
impl Display for AirPodsNoiseControlMode {
|
||||||
@@ -113,7 +112,7 @@ pub enum NothingAncMode {
|
|||||||
MidNoiseCancellation,
|
MidNoiseCancellation,
|
||||||
HighNoiseCancellation,
|
HighNoiseCancellation,
|
||||||
AdaptiveNoiseCancellation,
|
AdaptiveNoiseCancellation,
|
||||||
Transparency
|
Transparency,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for NothingAncMode {
|
impl Display for NothingAncMode {
|
||||||
|
|||||||
@@ -1,167 +1,179 @@
|
|||||||
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::bluetooth::att::{ATTHandles, ATTManager};
|
||||||
use crate::devices::enums::{DeviceData, DeviceInformation, DeviceType};
|
use crate::devices::enums::{DeviceData, DeviceInformation, DeviceType};
|
||||||
use crate::ui::messages::BluetoothUIMessage;
|
use crate::ui::messages::BluetoothUIMessage;
|
||||||
use crate::utils::get_devices_path;
|
use crate::utils::get_devices_path;
|
||||||
|
use bluer::Address;
|
||||||
|
use log::{debug, info};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::time::Duration;
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
use tokio::time::sleep;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct NothingInformation{
|
pub struct NothingInformation {
|
||||||
pub serial_number: String,
|
pub serial_number: String,
|
||||||
pub firmware_version: String
|
pub firmware_version: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct NothingDevice{
|
pub struct NothingDevice {
|
||||||
pub att_manager: ATTManager,
|
pub att_manager: ATTManager,
|
||||||
pub information: NothingInformation
|
pub information: NothingInformation,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NothingDevice{
|
impl NothingDevice {
|
||||||
pub async fn new(
|
pub async fn new(
|
||||||
mac_address: Address,
|
mac_address: Address,
|
||||||
ui_tx: mpsc::UnboundedSender<BluetoothUIMessage>
|
ui_tx: mpsc::UnboundedSender<BluetoothUIMessage>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let mut att_manager = ATTManager::new();
|
let mut att_manager = ATTManager::new();
|
||||||
att_manager.connect(mac_address).await.expect("Failed to connect");
|
att_manager
|
||||||
|
.connect(mac_address)
|
||||||
|
.await
|
||||||
|
.expect("Failed to connect");
|
||||||
|
|
||||||
let (tx, mut rx) = mpsc::unbounded_channel::<Vec<u8>>();
|
let (tx, mut rx) = mpsc::unbounded_channel::<Vec<u8>>();
|
||||||
|
|
||||||
att_manager.register_listener(
|
att_manager
|
||||||
ATTHandles::NothingEverythingRead,
|
.register_listener(ATTHandles::NothingEverythingRead, tx)
|
||||||
tx
|
.await;
|
||||||
).await;
|
|
||||||
|
|
||||||
let devices: HashMap<String, DeviceData> =
|
let devices: HashMap<String, DeviceData> = std::fs::read_to_string(get_devices_path())
|
||||||
std::fs::read_to_string(get_devices_path())
|
.ok()
|
||||||
.ok()
|
.and_then(|s| serde_json::from_str(&s).ok())
|
||||||
.and_then(|s| serde_json::from_str(&s).ok())
|
.unwrap_or_default();
|
||||||
.unwrap_or_default();
|
|
||||||
let device_key = mac_address.to_string();
|
let device_key = mac_address.to_string();
|
||||||
let information = if let Some(device_data) = devices.get(&device_key) {
|
let information = if let Some(device_data) = devices.get(&device_key) {
|
||||||
let info = device_data.information.clone();
|
let info = device_data.information.clone();
|
||||||
if let Some(DeviceInformation::Nothing(ref nothing_info)) = info {
|
if let Some(DeviceInformation::Nothing(ref nothing_info)) = info {
|
||||||
nothing_info.clone()
|
nothing_info.clone()
|
||||||
} else {
|
} else {
|
||||||
NothingInformation{
|
NothingInformation {
|
||||||
serial_number: String::new(),
|
serial_number: String::new(),
|
||||||
firmware_version: String::new()
|
firmware_version: String::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
NothingInformation{
|
NothingInformation {
|
||||||
serial_number: String::new(),
|
serial_number: String::new(),
|
||||||
firmware_version: String::new()
|
firmware_version: String::new(),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Request version information
|
// Request version information
|
||||||
att_manager.write(
|
att_manager
|
||||||
ATTHandles::NothingEverything,
|
.write(
|
||||||
&[
|
ATTHandles::NothingEverything,
|
||||||
0x55, 0x20,
|
&[
|
||||||
0x01, 0x42,
|
0x55, 0x20, 0x01, 0x42, 0xC0, 0x00, 0x00, 0x00, 0x00,
|
||||||
0xC0, 0x00,
|
0x00, // something, idk
|
||||||
0x00, 0x00,
|
],
|
||||||
0x00, 0x00 // something, idk
|
)
|
||||||
]
|
.await
|
||||||
).await.expect("Failed to write");
|
.expect("Failed to write");
|
||||||
|
|
||||||
sleep(Duration::from_millis(100)).await;
|
sleep(Duration::from_millis(100)).await;
|
||||||
|
|
||||||
// Request serial number
|
// Request serial number
|
||||||
att_manager.write(
|
att_manager
|
||||||
ATTHandles::NothingEverything,
|
.write(
|
||||||
&[
|
ATTHandles::NothingEverything,
|
||||||
0x55, 0x20,
|
&[0x55, 0x20, 0x01, 0x06, 0xC0, 0x00, 0x00, 0x13, 0x00, 0x00],
|
||||||
0x01, 0x06,
|
)
|
||||||
0xC0, 0x00,
|
.await
|
||||||
0x00, 0x13,
|
.expect("Failed to write");
|
||||||
0x00, 0x00
|
|
||||||
]
|
|
||||||
).await.expect("Failed to write");
|
|
||||||
|
|
||||||
// let ui_tx_clone = ui_tx.clone();
|
// let ui_tx_clone = ui_tx.clone();
|
||||||
let information_l = information.clone();
|
let information_l = information.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
while let Some(data) = rx.recv().await {
|
while let Some(data) = rx.recv().await {
|
||||||
if data.starts_with(&[
|
if data.starts_with(&[0x55, 0x20, 0x01, 0x42, 0x40]) {
|
||||||
0x55, 0x20,
|
|
||||||
0x01, 0x42, 0x40
|
|
||||||
]) {
|
|
||||||
let firmware_version = String::from_utf8_lossy(&data[8..]).to_string();
|
let firmware_version = String::from_utf8_lossy(&data[8..]).to_string();
|
||||||
info!("Received firmware version from Nothing device {}: {}", mac_address, firmware_version);
|
info!(
|
||||||
let new_information = NothingInformation{
|
"Received firmware version from Nothing device {}: {}",
|
||||||
|
mac_address, firmware_version
|
||||||
|
);
|
||||||
|
let new_information = NothingInformation {
|
||||||
serial_number: information_l.serial_number.clone(),
|
serial_number: information_l.serial_number.clone(),
|
||||||
firmware_version: firmware_version.clone()
|
firmware_version: firmware_version.clone(),
|
||||||
};
|
};
|
||||||
let mut new_devices = devices.clone();
|
let mut new_devices = devices.clone();
|
||||||
new_devices.insert(
|
new_devices.insert(
|
||||||
device_key.clone(),
|
device_key.clone(),
|
||||||
DeviceData{
|
DeviceData {
|
||||||
name: devices.get(&device_key)
|
name: devices
|
||||||
|
.get(&device_key)
|
||||||
.map(|d| d.name.clone())
|
.map(|d| d.name.clone())
|
||||||
.unwrap_or("Nothing Device".to_string()),
|
.unwrap_or("Nothing Device".to_string()),
|
||||||
type_: devices.get(&device_key)
|
type_: devices
|
||||||
|
.get(&device_key)
|
||||||
.map(|d| d.type_.clone())
|
.map(|d| d.type_.clone())
|
||||||
.unwrap_or(DeviceType::Nothing),
|
.unwrap_or(DeviceType::Nothing),
|
||||||
information: Some(DeviceInformation::Nothing(new_information)),
|
information: Some(DeviceInformation::Nothing(new_information)),
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
let json = serde_json::to_string(&new_devices).unwrap();
|
let json = serde_json::to_string(&new_devices).unwrap();
|
||||||
std::fs::write(get_devices_path(), json).expect("Failed to write devices file");
|
std::fs::write(get_devices_path(), json).expect("Failed to write devices file");
|
||||||
} else if data.starts_with(
|
} else if data.starts_with(&[0x55, 0x20, 0x01, 0x06, 0x40]) {
|
||||||
&[
|
let serial_number_start_position = data
|
||||||
0x55, 0x20,
|
.iter()
|
||||||
0x01, 0x06, 0x40
|
.position(|&b| b == "S".as_bytes()[0])
|
||||||
]
|
.unwrap_or(8);
|
||||||
) {
|
let serial_number_end = data
|
||||||
let serial_number_start_position = data.iter().position(|&b| b == "S".as_bytes()[0]).unwrap_or(8);
|
.iter()
|
||||||
let serial_number_end = data.iter()
|
|
||||||
.skip(serial_number_start_position)
|
.skip(serial_number_start_position)
|
||||||
.position(|&b| b == 0x0A)
|
.position(|&b| b == 0x0A)
|
||||||
.map(|pos| pos + serial_number_start_position)
|
.map(|pos| pos + serial_number_start_position)
|
||||||
.unwrap_or(data.len());
|
.unwrap_or(data.len());
|
||||||
if data.get(serial_number_start_position + 1) == Some(&"H".as_bytes()[0]) {
|
if data.get(serial_number_start_position + 1) == Some(&"H".as_bytes()[0]) {
|
||||||
let serial_number = String::from_utf8_lossy(
|
let serial_number = String::from_utf8_lossy(
|
||||||
&data[serial_number_start_position..serial_number_end]
|
&data[serial_number_start_position..serial_number_end],
|
||||||
).to_string();
|
)
|
||||||
info!("Received serial number from Nothing device {}: {}", mac_address, serial_number);
|
.to_string();
|
||||||
let new_information = NothingInformation{
|
info!(
|
||||||
|
"Received serial number from Nothing device {}: {}",
|
||||||
|
mac_address, serial_number
|
||||||
|
);
|
||||||
|
let new_information = NothingInformation {
|
||||||
serial_number: serial_number.clone(),
|
serial_number: serial_number.clone(),
|
||||||
firmware_version: information_l.firmware_version.clone()
|
firmware_version: information_l.firmware_version.clone(),
|
||||||
};
|
};
|
||||||
let mut new_devices = devices.clone();
|
let mut new_devices = devices.clone();
|
||||||
new_devices.insert(
|
new_devices.insert(
|
||||||
device_key.clone(),
|
device_key.clone(),
|
||||||
DeviceData{
|
DeviceData {
|
||||||
name: devices.get(&device_key)
|
name: devices
|
||||||
|
.get(&device_key)
|
||||||
.map(|d| d.name.clone())
|
.map(|d| d.name.clone())
|
||||||
.unwrap_or("Nothing Device".to_string()),
|
.unwrap_or("Nothing Device".to_string()),
|
||||||
type_: devices.get(&device_key)
|
type_: devices
|
||||||
|
.get(&device_key)
|
||||||
.map(|d| d.type_.clone())
|
.map(|d| d.type_.clone())
|
||||||
.unwrap_or(DeviceType::Nothing),
|
.unwrap_or(DeviceType::Nothing),
|
||||||
information: Some(DeviceInformation::Nothing(new_information)),
|
information: Some(DeviceInformation::Nothing(new_information)),
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
let json = serde_json::to_string(&new_devices).unwrap();
|
let json = serde_json::to_string(&new_devices).unwrap();
|
||||||
std::fs::write(get_devices_path(), json).expect("Failed to write devices file");
|
std::fs::write(get_devices_path(), json)
|
||||||
|
.expect("Failed to write devices file");
|
||||||
} else {
|
} else {
|
||||||
debug!("Serial number format unexpected from Nothing device {}: {:?}", mac_address, data);
|
debug!(
|
||||||
|
"Serial number format unexpected from Nothing device {}: {:?}",
|
||||||
|
mac_address, data
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {}
|
|
||||||
debug!("Received data from (Nothing) device {}, data: {:?}", mac_address, data);
|
debug!(
|
||||||
|
"Received data from (Nothing) device {}, data: {:?}",
|
||||||
|
mac_address, data
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
NothingDevice{
|
NothingDevice {
|
||||||
att_manager,
|
att_manager,
|
||||||
information
|
information,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,62 +1,95 @@
|
|||||||
mod bluetooth;
|
mod bluetooth;
|
||||||
|
mod devices;
|
||||||
mod media_controller;
|
mod media_controller;
|
||||||
mod ui;
|
mod ui;
|
||||||
mod utils;
|
mod utils;
|
||||||
mod devices;
|
|
||||||
|
|
||||||
use std::env;
|
|
||||||
use log::info;
|
|
||||||
use dbus::blocking::Connection;
|
|
||||||
use dbus::blocking::stdintf::org_freedesktop_dbus::Properties;
|
|
||||||
use dbus::message::MatchRule;
|
|
||||||
use dbus::arg::{RefArg, Variant};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use crate::bluetooth::discovery::{find_connected_airpods, find_other_managed_devices};
|
use crate::bluetooth::discovery::{find_connected_airpods, find_other_managed_devices};
|
||||||
use devices::airpods::AirPodsDevice;
|
|
||||||
use bluer::{Address, InternalErrorKind};
|
|
||||||
use ksni::TrayMethods;
|
|
||||||
use crate::ui::tray::MyTray;
|
|
||||||
use clap::Parser;
|
|
||||||
use crate::bluetooth::le::start_le_monitor;
|
use crate::bluetooth::le::start_le_monitor;
|
||||||
use tokio::sync::mpsc::unbounded_channel;
|
|
||||||
use tokio::sync::RwLock;
|
|
||||||
use crate::bluetooth::managers::DeviceManagers;
|
use crate::bluetooth::managers::DeviceManagers;
|
||||||
use crate::devices::enums::DeviceData;
|
use crate::devices::enums::DeviceData;
|
||||||
use crate::ui::messages::BluetoothUIMessage;
|
use crate::ui::messages::BluetoothUIMessage;
|
||||||
|
use crate::ui::tray::MyTray;
|
||||||
use crate::utils::get_devices_path;
|
use crate::utils::get_devices_path;
|
||||||
|
use bluer::{Address, InternalErrorKind};
|
||||||
|
use clap::Parser;
|
||||||
|
use dbus::arg::{RefArg, Variant};
|
||||||
|
use dbus::blocking::Connection;
|
||||||
|
use dbus::blocking::stdintf::org_freedesktop_dbus::Properties;
|
||||||
|
use dbus::message::MatchRule;
|
||||||
|
use devices::airpods::AirPodsDevice;
|
||||||
|
use ksni::TrayMethods;
|
||||||
|
use log::info;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::env;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
use tokio::sync::mpsc::unbounded_channel;
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
struct Args {
|
struct Args {
|
||||||
#[arg(long)]
|
#[arg(long, short = 'd', help = "Enable debug logging")]
|
||||||
debug: bool,
|
debug: bool,
|
||||||
#[arg(long)]
|
#[arg(
|
||||||
|
long,
|
||||||
|
help = "Disable system tray, useful if your environment doesn't support AppIndicator or StatusNotifier"
|
||||||
|
)]
|
||||||
no_tray: bool,
|
no_tray: bool,
|
||||||
#[arg(long)]
|
#[arg(long, help = "Start the application minimized to tray")]
|
||||||
start_minimized: bool,
|
start_minimized: bool,
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
help = "Enable Bluetooth LE debug logging. Only use when absolutely necessary; this produces a lot of logs."
|
||||||
|
)]
|
||||||
|
le_debug: bool,
|
||||||
|
#[arg(long, short = 'v', help = "Show application version and exit")]
|
||||||
|
version: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> iced::Result {
|
fn main() -> iced::Result {
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
|
|
||||||
|
if args.version {
|
||||||
|
println!(
|
||||||
|
"You are running LibrePods version {}",
|
||||||
|
env!("CARGO_PKG_VERSION")
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
let log_level = if args.debug { "debug" } else { "info" };
|
let log_level = if args.debug { "debug" } else { "info" };
|
||||||
|
let wayland_display = env::var("WAYLAND_DISPLAY").is_ok();
|
||||||
if env::var("RUST_LOG").is_err() {
|
if env::var("RUST_LOG").is_err() {
|
||||||
unsafe { env::set_var("RUST_LOG", log_level.to_owned() + ",winit=warn,tracing=warn,,iced_wgpu=warn,wgpu_hal=warn,wgpu_core=warn,librepods_rust::bluetooth::le=warn,cosmic_text=warn,naga=warn,iced_winit=warn") };
|
if wayland_display {
|
||||||
|
unsafe { env::set_var("WGPU_BACKEND", "gl") };
|
||||||
|
}
|
||||||
|
unsafe {
|
||||||
|
env::set_var(
|
||||||
|
"RUST_LOG",
|
||||||
|
log_level.to_owned()
|
||||||
|
+ &format!(
|
||||||
|
",winit=warn,tracing=warn,iced_wgpu=warn,wgpu_hal=warn,wgpu_core=warn,cosmic_text=warn,naga=warn,iced_winit=warn,librepods_rust::bluetooth::le={}",
|
||||||
|
if args.le_debug { "debug" } else { "warn" }
|
||||||
|
),
|
||||||
|
)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
|
|
||||||
let (ui_tx, ui_rx) = unbounded_channel::<BluetoothUIMessage>();
|
let (ui_tx, ui_rx) = unbounded_channel::<BluetoothUIMessage>();
|
||||||
|
|
||||||
let device_managers: Arc<RwLock<HashMap<String, DeviceManagers>>> = Arc::new(RwLock::new(HashMap::new()));
|
let device_managers: Arc<RwLock<HashMap<String, DeviceManagers>>> =
|
||||||
|
Arc::new(RwLock::new(HashMap::new()));
|
||||||
let device_managers_clone = device_managers.clone();
|
let device_managers_clone = device_managers.clone();
|
||||||
std::thread::spawn(|| {
|
std::thread::spawn(|| {
|
||||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||||
rt.block_on(async_main(ui_tx, device_managers_clone)).unwrap();
|
rt.block_on(async_main(ui_tx, device_managers_clone))
|
||||||
|
.unwrap();
|
||||||
});
|
});
|
||||||
|
|
||||||
ui::window::start_ui(ui_rx, args.start_minimized, device_managers)
|
ui::window::start_ui(ui_rx, args.start_minimized, device_managers)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async fn async_main(
|
async fn async_main(
|
||||||
ui_tx: tokio::sync::mpsc::UnboundedSender<BluetoothUIMessage>,
|
ui_tx: tokio::sync::mpsc::UnboundedSender<BluetoothUIMessage>,
|
||||||
device_managers: Arc<RwLock<HashMap<String, DeviceManagers>>>,
|
device_managers: Arc<RwLock<HashMap<String, DeviceManagers>>>,
|
||||||
@@ -70,16 +103,14 @@ async fn async_main(
|
|||||||
log::error!("Failed to read devices file: {}", e);
|
log::error!("Failed to read devices file: {}", e);
|
||||||
"{}".to_string()
|
"{}".to_string()
|
||||||
});
|
});
|
||||||
let devices_list: HashMap<String, DeviceData> = serde_json::from_str(&devices_json).unwrap_or_else(|e| {
|
let devices_list: HashMap<String, DeviceData> = serde_json::from_str(&devices_json)
|
||||||
log::error!("Deserialization failed: {}", e);
|
.unwrap_or_else(|e| {
|
||||||
HashMap::new()
|
log::error!("Deserialization failed: {}", e);
|
||||||
});
|
HashMap::new()
|
||||||
|
});
|
||||||
for (mac, device_data) in devices_list.iter() {
|
for (mac, device_data) in devices_list.iter() {
|
||||||
match device_data.type_ {
|
if device_data.type_ == devices::enums::DeviceType::Nothing {
|
||||||
devices::enums::DeviceType::Nothing => {
|
managed_devices_mac.push(mac.clone());
|
||||||
managed_devices_mac.push(mac.clone());
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,6 +119,8 @@ async fn async_main(
|
|||||||
} else {
|
} else {
|
||||||
let tray = MyTray {
|
let tray = MyTray {
|
||||||
conversation_detect_enabled: None,
|
conversation_detect_enabled: None,
|
||||||
|
battery_headphone: None,
|
||||||
|
battery_headphone_status: None,
|
||||||
battery_l: None,
|
battery_l: None,
|
||||||
battery_l_status: None,
|
battery_l_status: None,
|
||||||
battery_r: None,
|
battery_r: None,
|
||||||
@@ -121,9 +154,13 @@ async fn async_main(
|
|||||||
info!("Checking for connected devices...");
|
info!("Checking for connected devices...");
|
||||||
match find_connected_airpods(&adapter).await {
|
match find_connected_airpods(&adapter).await {
|
||||||
Ok(device) => {
|
Ok(device) => {
|
||||||
let name = device.name().await?.unwrap_or_else(|| "Unknown".to_string());
|
let name = device
|
||||||
|
.name()
|
||||||
|
.await?
|
||||||
|
.unwrap_or_else(|| "Unknown".to_string());
|
||||||
info!("Found connected AirPods: {}, initializing.", name);
|
info!("Found connected AirPods: {}, initializing.", name);
|
||||||
let airpods_device = AirPodsDevice::new(device.address(), tray_handle.clone(), ui_tx.clone()).await;
|
let airpods_device =
|
||||||
|
AirPodsDevice::new(device.address(), tray_handle.clone(), ui_tx.clone()).await;
|
||||||
|
|
||||||
let mut managers = device_managers.write().await;
|
let mut managers = device_managers.write().await;
|
||||||
// let dev_managers = DeviceManagers::with_both(airpods_device.aacp_manager.clone(), airpods_device.att_manager.clone());
|
// let dev_managers = DeviceManagers::with_both(airpods_device.aacp_manager.clone(), airpods_device.att_manager.clone());
|
||||||
@@ -133,7 +170,11 @@ async fn async_main(
|
|||||||
.or_insert(dev_managers)
|
.or_insert(dev_managers)
|
||||||
.set_aacp(airpods_device.aacp_manager);
|
.set_aacp(airpods_device.aacp_manager);
|
||||||
drop(managers);
|
drop(managers);
|
||||||
ui_tx.send(BluetoothUIMessage::DeviceConnected(device.address().to_string())).unwrap();
|
ui_tx
|
||||||
|
.send(BluetoothUIMessage::DeviceConnected(
|
||||||
|
device.address().to_string(),
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
info!("No connected AirPods found.");
|
info!("No connected AirPods found.");
|
||||||
@@ -144,23 +185,29 @@ async fn async_main(
|
|||||||
Ok(devices) => {
|
Ok(devices) => {
|
||||||
for device in devices {
|
for device in devices {
|
||||||
let addr_str = device.address().to_string();
|
let addr_str = device.address().to_string();
|
||||||
info!("Found connected managed device: {}, initializing.", addr_str);
|
info!(
|
||||||
|
"Found connected managed device: {}, initializing.",
|
||||||
|
addr_str
|
||||||
|
);
|
||||||
let type_ = devices_list.get(&addr_str).unwrap().type_.clone();
|
let type_ = devices_list.get(&addr_str).unwrap().type_.clone();
|
||||||
let ui_tx_clone = ui_tx.clone();
|
let ui_tx_clone = ui_tx.clone();
|
||||||
let device_managers = device_managers.clone();
|
let device_managers = device_managers.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let mut managers = device_managers.write().await;
|
let mut managers = device_managers.write().await;
|
||||||
match type_ {
|
if type_ == devices::enums::DeviceType::Nothing {
|
||||||
devices::enums::DeviceType::Nothing => {
|
let dev = devices::nothing::NothingDevice::new(
|
||||||
let dev = devices::nothing::NothingDevice::new(device.address(), ui_tx_clone.clone()).await;
|
device.address(),
|
||||||
let dev_managers = DeviceManagers::with_att(dev.att_manager.clone());
|
ui_tx_clone.clone(),
|
||||||
managers
|
)
|
||||||
.entry(addr_str.clone())
|
.await;
|
||||||
.or_insert(dev_managers)
|
let dev_managers = DeviceManagers::with_att(dev.att_manager.clone());
|
||||||
.set_att(dev.att_manager);
|
managers
|
||||||
ui_tx_clone.send(BluetoothUIMessage::DeviceConnected(addr_str)).unwrap();
|
.entry(addr_str.clone())
|
||||||
}
|
.or_insert(dev_managers)
|
||||||
_ => {}
|
.set_att(dev.att_manager);
|
||||||
|
ui_tx_clone
|
||||||
|
.send(BluetoothUIMessage::DeviceConnected(addr_str))
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
drop(managers)
|
drop(managers)
|
||||||
});
|
});
|
||||||
@@ -168,7 +215,9 @@ async fn async_main(
|
|||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::debug!("type of error: {:?}", e.kind);
|
log::debug!("type of error: {:?}", e.kind);
|
||||||
if e.kind != bluer::ErrorKind::Internal(InternalErrorKind::Io(std::io::ErrorKind::NotFound)) {
|
if e.kind
|
||||||
|
!= bluer::ErrorKind::Internal(InternalErrorKind::Io(std::io::ErrorKind::NotFound))
|
||||||
|
{
|
||||||
log::error!("Error finding other managed devices: {}", e);
|
log::error!("Error finding other managed devices: {}", e);
|
||||||
} else {
|
} else {
|
||||||
info!("No other managed devices found.");
|
info!("No other managed devices found.");
|
||||||
@@ -179,49 +228,62 @@ async fn async_main(
|
|||||||
let conn = Connection::new_system()?;
|
let conn = Connection::new_system()?;
|
||||||
let rule = MatchRule::new_signal("org.freedesktop.DBus.Properties", "PropertiesChanged");
|
let rule = MatchRule::new_signal("org.freedesktop.DBus.Properties", "PropertiesChanged");
|
||||||
conn.add_match(rule, move |_: (), conn, msg| {
|
conn.add_match(rule, move |_: (), conn, msg| {
|
||||||
let Some(path) = msg.path() else { return true; };
|
let Some(path) = msg.path() else {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
if !path.contains("/org/bluez/hci") || !path.contains("/dev_") {
|
if !path.contains("/org/bluez/hci") || !path.contains("/dev_") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// debug!("PropertiesChanged signal for path: {}", path);
|
// debug!("PropertiesChanged signal for path: {}", path);
|
||||||
let Ok((iface, changed, _)) = msg.read3::<String, HashMap<String, Variant<Box<dyn RefArg>>>, Vec<String>>() else {
|
let Ok((iface, changed, _)) =
|
||||||
|
msg.read3::<String, HashMap<String, Variant<Box<dyn RefArg>>>, Vec<String>>()
|
||||||
|
else {
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
if iface != "org.bluez.Device1" {
|
if iface != "org.bluez.Device1" {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
let Some(connected_var) = changed.get("Connected") else { return true; };
|
let Some(connected_var) = changed.get("Connected") else {
|
||||||
let Some(is_connected) = connected_var.0.as_ref().as_u64() else { return true; };
|
return true;
|
||||||
|
};
|
||||||
|
let Some(is_connected) = connected_var.0.as_ref().as_u64() else {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
if is_connected == 0 {
|
if is_connected == 0 {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
let proxy = conn.with_proxy("org.bluez", path, std::time::Duration::from_millis(5000));
|
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 Ok(uuids) = proxy.get::<Vec<String>>("org.bluez.Device1", "UUIDs") else {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
let target_uuid = "74ec2172-0bad-4d01-8f77-997b2be0722a";
|
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_str) = proxy.get::<String>("org.bluez.Device1", "Address") else {
|
||||||
let Ok(addr) = addr_str.parse::<Address>() else { return true; };
|
return true;
|
||||||
|
};
|
||||||
|
let Ok(addr) = addr_str.parse::<Address>() else {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
if managed_devices_mac.contains(&addr_str) {
|
if managed_devices_mac.contains(&addr_str) {
|
||||||
info!("Managed device connected: {}, initializing", addr_str);
|
info!("Managed device connected: {}, initializing", addr_str);
|
||||||
let type_ = devices_list.get(&addr_str).unwrap().type_.clone();
|
let type_ = devices_list.get(&addr_str).unwrap().type_.clone();
|
||||||
match type_ {
|
if type_ == devices::enums::DeviceType::Nothing {
|
||||||
devices::enums::DeviceType::Nothing => {
|
let ui_tx_clone = ui_tx.clone();
|
||||||
let ui_tx_clone = ui_tx.clone();
|
let device_managers = device_managers.clone();
|
||||||
let device_managers = device_managers.clone();
|
tokio::spawn(async move {
|
||||||
tokio::spawn(async move {
|
let mut managers = device_managers.write().await;
|
||||||
let mut managers = device_managers.write().await;
|
let dev = devices::nothing::NothingDevice::new(addr, ui_tx_clone.clone()).await;
|
||||||
let dev = devices::nothing::NothingDevice::new(addr, ui_tx_clone.clone()).await;
|
let dev_managers = DeviceManagers::with_att(dev.att_manager.clone());
|
||||||
let dev_managers = DeviceManagers::with_att(dev.att_manager.clone());
|
managers
|
||||||
managers
|
.entry(addr_str.clone())
|
||||||
.entry(addr_str.clone())
|
.or_insert(dev_managers)
|
||||||
.or_insert(dev_managers)
|
.set_att(dev.att_manager);
|
||||||
.set_att(dev.att_manager);
|
drop(managers);
|
||||||
drop(managers);
|
ui_tx_clone
|
||||||
ui_tx_clone.send(BluetoothUIMessage::DeviceConnected(addr_str.clone())).unwrap();
|
.send(BluetoothUIMessage::DeviceConnected(addr_str.clone()))
|
||||||
});
|
.unwrap();
|
||||||
}
|
});
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -229,7 +291,9 @@ async fn async_main(
|
|||||||
if !uuids.iter().any(|u| u.to_lowercase() == target_uuid) {
|
if !uuids.iter().any(|u| u.to_lowercase() == target_uuid) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
let name = proxy.get::<String>("org.bluez.Device1", "Name").unwrap_or_else(|_| "Unknown".to_string());
|
let name = proxy
|
||||||
|
.get::<String>("org.bluez.Device1", "Name")
|
||||||
|
.unwrap_or_else(|_| "Unknown".to_string());
|
||||||
info!("AirPods connected: {}, initializing", name);
|
info!("AirPods connected: {}, initializing", name);
|
||||||
let handle_clone = tray_handle.clone();
|
let handle_clone = tray_handle.clone();
|
||||||
let ui_tx_clone = ui_tx.clone();
|
let ui_tx_clone = ui_tx.clone();
|
||||||
@@ -244,7 +308,9 @@ async fn async_main(
|
|||||||
.or_insert(dev_managers)
|
.or_insert(dev_managers)
|
||||||
.set_aacp(airpods_device.aacp_manager);
|
.set_aacp(airpods_device.aacp_manager);
|
||||||
drop(managers);
|
drop(managers);
|
||||||
ui_tx_clone.send(BluetoothUIMessage::DeviceConnected(addr_str.clone())).unwrap();
|
ui_tx_clone
|
||||||
|
.send(BluetoothUIMessage::DeviceConnected(addr_str.clone()))
|
||||||
|
.unwrap();
|
||||||
});
|
});
|
||||||
true
|
true
|
||||||
})?;
|
})?;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,16 +1,18 @@
|
|||||||
use std::collections::HashMap;
|
use crate::bluetooth::aacp::{AACPManager, ControlCommandIdentifiers};
|
||||||
use std::sync::Arc;
|
|
||||||
use std::thread;
|
|
||||||
use iced::widget::{button, column, combo_box, container, row, rule, text, text_input, toggler, Rule, Space};
|
|
||||||
use iced::{Background, Border, Center, Color, Length, Padding, Theme};
|
|
||||||
use iced::Alignment::End;
|
use iced::Alignment::End;
|
||||||
use iced::border::Radius;
|
use iced::border::Radius;
|
||||||
use iced::overlay::menu;
|
use iced::overlay::menu;
|
||||||
use iced::widget::button::Style;
|
use iced::widget::button::Style;
|
||||||
use iced::widget::rule::FillMode;
|
use iced::widget::rule::FillMode;
|
||||||
|
use iced::widget::{
|
||||||
|
Rule, Space, button, column, combo_box, container, row, rule, text, text_input, toggler,
|
||||||
|
};
|
||||||
|
use iced::{Background, Border, Center, Color, Length, Padding, Theme};
|
||||||
use log::error;
|
use log::error;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::thread;
|
||||||
use tokio::runtime::Runtime;
|
use tokio::runtime::Runtime;
|
||||||
use crate::bluetooth::aacp::{AACPManager, ControlCommandIdentifiers};
|
|
||||||
// use crate::bluetooth::att::ATTManager;
|
// use crate::bluetooth::att::ATTManager;
|
||||||
use crate::devices::enums::{AirPodsState, DeviceData, DeviceInformation, DeviceState};
|
use crate::devices::enums::{AirPodsState, DeviceData, DeviceInformation, DeviceState};
|
||||||
use crate::ui::window::Message;
|
use crate::ui::window::Message;
|
||||||
@@ -24,30 +26,25 @@ pub fn airpods_view<'a>(
|
|||||||
) -> iced::widget::Container<'a, Message> {
|
) -> iced::widget::Container<'a, Message> {
|
||||||
let mac = mac.to_string();
|
let mac = mac.to_string();
|
||||||
// order: name, noise control, press and hold config, call controls (not sure if why it might be needed, adding it just in case), audio (personalized volume, conversational awareness, adaptive audio slider), connection settings, microphone, head gestures (not adding this), off listening mode, device information
|
// order: name, noise control, press and hold config, call controls (not sure if why it might be needed, adding it just in case), audio (personalized volume, conversational awareness, adaptive audio slider), connection settings, microphone, head gestures (not adding this), off listening mode, device information
|
||||||
|
|
||||||
let aacp_manager_for_rename = aacp_manager.clone();
|
let aacp_manager_for_rename = aacp_manager.clone();
|
||||||
let rename_input = container(
|
let rename_input = container(
|
||||||
row![
|
row![
|
||||||
Space::with_width(10),
|
Space::with_width(10),
|
||||||
text("Name").size(16).style(
|
text("Name").size(16).style(|theme: &Theme| {
|
||||||
|theme: &Theme| {
|
let mut style = text::Style::default();
|
||||||
let mut style = text::Style::default();
|
style.color = Some(theme.palette().text);
|
||||||
style.color = Some(theme.palette().text);
|
style
|
||||||
style
|
}),
|
||||||
}
|
|
||||||
),
|
|
||||||
Space::with_width(Length::Fill),
|
Space::with_width(Length::Fill),
|
||||||
text_input(
|
text_input("", &state.device_name)
|
||||||
"",
|
.padding(Padding {
|
||||||
&state.device_name
|
top: 5.0,
|
||||||
)
|
bottom: 5.0,
|
||||||
.padding(Padding{
|
left: 10.0,
|
||||||
top: 5.0,
|
right: 10.0,
|
||||||
bottom: 5.0,
|
})
|
||||||
left: 10.0,
|
.style(|theme: &Theme, _status| {
|
||||||
right: 10.0,
|
|
||||||
})
|
|
||||||
.style(
|
|
||||||
|theme: &Theme, _status| {
|
|
||||||
text_input::Style {
|
text_input::Style {
|
||||||
background: Background::Color(Color::TRANSPARENT),
|
background: Background::Color(Color::TRANSPARENT),
|
||||||
border: Default::default(),
|
border: Default::default(),
|
||||||
@@ -56,56 +53,52 @@ pub fn airpods_view<'a>(
|
|||||||
value: theme.palette().text,
|
value: theme.palette().text,
|
||||||
selection: Default::default(),
|
selection: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
)
|
.align_x(End)
|
||||||
.align_x(End)
|
.on_input({
|
||||||
.on_input({
|
|
||||||
let mac = mac.clone();
|
let mac = mac.clone();
|
||||||
let state = state.clone();
|
let state = state.clone();
|
||||||
move|new_name| {
|
move |new_name| {
|
||||||
let aacp_manager = aacp_manager_for_rename.clone();
|
let aacp_manager = aacp_manager_for_rename.clone();
|
||||||
run_async_in_thread(
|
run_async_in_thread({
|
||||||
{
|
let new_name = new_name.clone();
|
||||||
let new_name = new_name.clone();
|
async move {
|
||||||
async move {
|
aacp_manager
|
||||||
aacp_manager.send_rename_packet(&new_name).await.expect("Failed to send rename packet");
|
.send_rename_packet(&new_name)
|
||||||
}
|
.await
|
||||||
|
.expect("Failed to send rename packet");
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
let mut state = state.clone();
|
let mut state = state.clone();
|
||||||
state.device_name = new_name.clone();
|
state.device_name = new_name.clone();
|
||||||
Message::StateChanged(mac.to_string(), DeviceState::AirPods(state))
|
Message::StateChanged(mac.to_string(), DeviceState::AirPods(state))
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
)
|
|
||||||
]
|
]
|
||||||
.align_y(Center)
|
.align_y(Center),
|
||||||
)
|
)
|
||||||
.padding(Padding{
|
.padding(Padding {
|
||||||
top: 5.0,
|
top: 5.0,
|
||||||
bottom: 5.0,
|
bottom: 5.0,
|
||||||
left: 10.0,
|
left: 10.0,
|
||||||
right: 10.0,
|
right: 10.0,
|
||||||
})
|
})
|
||||||
.style(
|
.style(|theme: &Theme| {
|
||||||
|theme: &Theme| {
|
let mut style = container::Style::default();
|
||||||
let mut style = container::Style::default();
|
style.background = Some(Background::Color(theme.palette().primary.scale_alpha(0.1)));
|
||||||
style.background = Some(Background::Color(theme.palette().primary.scale_alpha(0.1)));
|
let mut border = Border::default();
|
||||||
let mut border = Border::default();
|
border.color = theme.palette().primary.scale_alpha(0.5);
|
||||||
border.color = theme.palette().primary.scale_alpha(0.5);
|
style.border = border.rounded(16);
|
||||||
style.border = border.rounded(16);
|
style
|
||||||
style
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
let listening_mode = container(row![
|
let listening_mode = container(
|
||||||
text("Listening Mode").size(16).style(
|
row![
|
||||||
|theme: &Theme| {
|
text("Listening Mode").size(16).style(|theme: &Theme| {
|
||||||
let mut style = text::Style::default();
|
let mut style = text::Style::default();
|
||||||
style.color = Some(theme.palette().text);
|
style.color = Some(theme.palette().text);
|
||||||
style
|
style
|
||||||
}
|
}),
|
||||||
),
|
|
||||||
Space::with_width(Length::Fill),
|
Space::with_width(Length::Fill),
|
||||||
{
|
{
|
||||||
let state_clone = state.clone();
|
let state_clone = state.clone();
|
||||||
@@ -120,78 +113,71 @@ pub fn airpods_view<'a>(
|
|||||||
move |selected_mode| {
|
move |selected_mode| {
|
||||||
let aacp_manager = aacp_manager.clone();
|
let aacp_manager = aacp_manager.clone();
|
||||||
let selected_mode_c = selected_mode.clone();
|
let selected_mode_c = selected_mode.clone();
|
||||||
run_async_in_thread(
|
run_async_in_thread(async move {
|
||||||
async move {
|
aacp_manager
|
||||||
aacp_manager.send_control_command(
|
.send_control_command(
|
||||||
ControlCommandIdentifiers::ListeningMode,
|
ControlCommandIdentifiers::ListeningMode,
|
||||||
&[selected_mode_c.to_byte()]
|
&[selected_mode_c.to_byte()],
|
||||||
).await.expect("Failed to send Noise Control Mode command");
|
)
|
||||||
}
|
.await
|
||||||
);
|
.expect("Failed to send Noise Control Mode command");
|
||||||
|
});
|
||||||
let mut state = state_clone.clone();
|
let mut state = state_clone.clone();
|
||||||
state.noise_control_mode = selected_mode.clone();
|
state.noise_control_mode = selected_mode.clone();
|
||||||
Message::StateChanged(mac.to_string(), DeviceState::AirPods(state))
|
Message::StateChanged(mac.to_string(), DeviceState::AirPods(state))
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
.width(Length::from(200))
|
.width(Length::from(200))
|
||||||
.input_style(
|
.input_style(|theme: &Theme, _status| text_input::Style {
|
||||||
|theme: &Theme, _status| {
|
background: Background::Color(theme.palette().primary.scale_alpha(0.2)),
|
||||||
text_input::Style {
|
border: Border {
|
||||||
background: Background::Color(theme.palette().primary.scale_alpha(0.2)),
|
width: 1.0,
|
||||||
border: Border {
|
color: theme.palette().text.scale_alpha(0.3),
|
||||||
width: 1.0,
|
radius: Radius::from(4.0),
|
||||||
color: theme.palette().text.scale_alpha(0.3),
|
},
|
||||||
radius: Radius::from(4.0)
|
icon: Default::default(),
|
||||||
},
|
placeholder: theme.palette().text,
|
||||||
icon: Default::default(),
|
value: theme.palette().text,
|
||||||
placeholder: theme.palette().text,
|
selection: Default::default(),
|
||||||
value: theme.palette().text,
|
})
|
||||||
selection: Default::default(),
|
.padding(Padding {
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.padding(Padding{
|
|
||||||
top: 5.0,
|
top: 5.0,
|
||||||
bottom: 5.0,
|
bottom: 5.0,
|
||||||
left: 10.0,
|
left: 10.0,
|
||||||
right: 10.0,
|
right: 10.0,
|
||||||
})
|
})
|
||||||
.menu_style(
|
.menu_style(|theme: &Theme| menu::Style {
|
||||||
|theme: &Theme| {
|
background: Background::Color(theme.palette().background),
|
||||||
menu::Style {
|
border: Border {
|
||||||
background: Background::Color(theme.palette().background),
|
width: 1.0,
|
||||||
border: Border {
|
color: theme.palette().text,
|
||||||
width: 1.0,
|
radius: Radius::from(4.0),
|
||||||
color: theme.palette().text,
|
},
|
||||||
radius: Radius::from(4.0)
|
text_color: theme.palette().text,
|
||||||
},
|
selected_text_color: theme.palette().text,
|
||||||
text_color: theme.palette().text,
|
selected_background: Background::Color(
|
||||||
selected_text_color: theme.palette().text,
|
theme.palette().primary.scale_alpha(0.3),
|
||||||
selected_background: Background::Color(theme.palette().primary.scale_alpha(0.3)),
|
),
|
||||||
}
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
.align_y(Center)
|
.align_y(Center),
|
||||||
)
|
)
|
||||||
.padding(Padding{
|
.padding(Padding {
|
||||||
top: 5.0,
|
top: 5.0,
|
||||||
bottom: 5.0,
|
bottom: 5.0,
|
||||||
left: 18.0,
|
left: 18.0,
|
||||||
right: 18.0,
|
right: 18.0,
|
||||||
})
|
})
|
||||||
.style(
|
.style(|theme: &Theme| {
|
||||||
|theme: &Theme| {
|
let mut style = container::Style::default();
|
||||||
let mut style = container::Style::default();
|
style.background = Some(Background::Color(theme.palette().primary.scale_alpha(0.1)));
|
||||||
style.background = Some(Background::Color(theme.palette().primary.scale_alpha(0.1)));
|
let mut border = Border::default();
|
||||||
let mut border = Border::default();
|
border.color = theme.palette().primary.scale_alpha(0.5);
|
||||||
border.color = theme.palette().primary.scale_alpha(0.5);
|
style.border = border.rounded(16);
|
||||||
style.border = border.rounded(16);
|
style
|
||||||
style
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
let mac_audio = mac.clone();
|
let mac_audio = mac.clone();
|
||||||
let mac_information = mac.clone();
|
let mac_information = mac.clone();
|
||||||
@@ -221,14 +207,13 @@ pub fn airpods_view<'a>(
|
|||||||
column![
|
column![
|
||||||
text("Personalized Volume").size(16),
|
text("Personalized Volume").size(16),
|
||||||
text("Adjusts the volume in response to your environment.").size(12).style(
|
text("Adjusts the volume in response to your environment.").size(12).style(
|
||||||
|theme: &Theme| {
|
|theme: &Theme| {
|
||||||
let mut style = text::Style::default();
|
let mut style = text::Style::default();
|
||||||
style.color = Some(theme.palette().text.scale_alpha(0.7));
|
style.color = Some(theme.palette().text.scale_alpha(0.7));
|
||||||
style
|
style
|
||||||
}
|
}
|
||||||
)
|
).width(Length::Fill),
|
||||||
],
|
].width(Length::Fill),
|
||||||
Space::with_width(Length::Fill),
|
|
||||||
toggler(state.personalized_volume_enabled)
|
toggler(state.personalized_volume_enabled)
|
||||||
.on_toggle(
|
.on_toggle(
|
||||||
{
|
{
|
||||||
@@ -255,6 +240,7 @@ pub fn airpods_view<'a>(
|
|||||||
.size(20)
|
.size(20)
|
||||||
]
|
]
|
||||||
.align_y(Center)
|
.align_y(Center)
|
||||||
|
.spacing(8)
|
||||||
},
|
},
|
||||||
Rule::horizontal(8).style(
|
Rule::horizontal(8).style(
|
||||||
|theme: &Theme| {
|
|theme: &Theme| {
|
||||||
@@ -272,14 +258,13 @@ pub fn airpods_view<'a>(
|
|||||||
column![
|
column![
|
||||||
text("Conversation Awareness").size(16),
|
text("Conversation Awareness").size(16),
|
||||||
text("Lowers the volume of your audio when it detects that you are speaking.").size(12).style(
|
text("Lowers the volume of your audio when it detects that you are speaking.").size(12).style(
|
||||||
|theme: &Theme| {
|
|theme: &Theme| {
|
||||||
let mut style = text::Style::default();
|
let mut style = text::Style::default();
|
||||||
style.color = Some(theme.palette().text.scale_alpha(0.7));
|
style.color = Some(theme.palette().text.scale_alpha(0.7));
|
||||||
style
|
style
|
||||||
}
|
}
|
||||||
)
|
).width(Length::Fill),
|
||||||
],
|
].width(Length::Fill),
|
||||||
Space::with_width(Length::Fill),
|
|
||||||
toggler(state.conversation_awareness_enabled)
|
toggler(state.conversation_awareness_enabled)
|
||||||
.on_toggle(move |is_enabled| {
|
.on_toggle(move |is_enabled| {
|
||||||
let aacp_manager = aacp_manager_conv_detect.clone();
|
let aacp_manager = aacp_manager_conv_detect.clone();
|
||||||
@@ -299,6 +284,7 @@ pub fn airpods_view<'a>(
|
|||||||
.size(20)
|
.size(20)
|
||||||
]
|
]
|
||||||
.align_y(Center)
|
.align_y(Center)
|
||||||
|
.spacing(8)
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
.spacing(4)
|
.spacing(4)
|
||||||
@@ -328,15 +314,14 @@ pub fn airpods_view<'a>(
|
|||||||
container(row![
|
container(row![
|
||||||
column![
|
column![
|
||||||
text("Off Listening Mode").size(16),
|
text("Off Listening Mode").size(16),
|
||||||
text("When this is on, AIrPods listening modes will include an Off option. Loud sound levels are not reduced when listening mode is set to Off.").size(12).style(
|
text("When this is on, AirPods listening modes will include an Off option. Loud sound levels are not reduced when listening mode is set to Off.").size(12).style(
|
||||||
|theme: &Theme| {
|
|theme: &Theme| {
|
||||||
let mut style = text::Style::default();
|
let mut style = text::Style::default();
|
||||||
style.color = Some(theme.palette().text.scale_alpha(0.7));
|
style.color = Some(theme.palette().text.scale_alpha(0.7));
|
||||||
style
|
style
|
||||||
}
|
}
|
||||||
)
|
).width(Length::Fill)
|
||||||
],
|
].width(Length::Fill),
|
||||||
Space::with_width(Length::Fill),
|
|
||||||
toggler(state.allow_off_mode)
|
toggler(state.allow_off_mode)
|
||||||
.on_toggle(move |is_enabled| {
|
.on_toggle(move |is_enabled| {
|
||||||
let aacp_manager = aacp_manager_olm.clone();
|
let aacp_manager = aacp_manager_olm.clone();
|
||||||
@@ -356,6 +341,7 @@ pub fn airpods_view<'a>(
|
|||||||
.size(20)
|
.size(20)
|
||||||
]
|
]
|
||||||
.align_y(Center)
|
.align_y(Center)
|
||||||
|
.spacing(8)
|
||||||
)
|
)
|
||||||
.padding(Padding{
|
.padding(Padding{
|
||||||
top: 5.0,
|
top: 5.0,
|
||||||
@@ -380,126 +366,102 @@ pub fn airpods_view<'a>(
|
|||||||
if let Some(DeviceInformation::AirPods(ref airpods_info)) = device.information {
|
if let Some(DeviceInformation::AirPods(ref airpods_info)) = device.information {
|
||||||
let info_rows = column![
|
let info_rows = column![
|
||||||
row![
|
row![
|
||||||
text("Model Number").size(16).style(
|
text("Model Number").size(16).style(|theme: &Theme| {
|
||||||
|theme: &Theme| {
|
let mut style = text::Style::default();
|
||||||
let mut style = text::Style::default();
|
style.color = Some(theme.palette().text);
|
||||||
style.color = Some(theme.palette().text);
|
style
|
||||||
style
|
}),
|
||||||
}
|
|
||||||
),
|
|
||||||
Space::with_width(Length::Fill),
|
Space::with_width(Length::Fill),
|
||||||
text(airpods_info.model_number.clone()).size(16)
|
text(airpods_info.model_number.clone()).size(16)
|
||||||
],
|
],
|
||||||
row![
|
row![
|
||||||
text("Manufacturer").size(16).style(
|
text("Manufacturer").size(16).style(|theme: &Theme| {
|
||||||
|theme: &Theme| {
|
let mut style = text::Style::default();
|
||||||
let mut style = text::Style::default();
|
style.color = Some(theme.palette().text);
|
||||||
style.color = Some(theme.palette().text);
|
style
|
||||||
style
|
}),
|
||||||
}
|
|
||||||
),
|
|
||||||
Space::with_width(Length::Fill),
|
Space::with_width(Length::Fill),
|
||||||
text(airpods_info.manufacturer.clone()).size(16)
|
text(airpods_info.manufacturer.clone()).size(16)
|
||||||
],
|
],
|
||||||
row![
|
row![
|
||||||
text("Serial Number").size(16).style(
|
text("Serial Number").size(16).style(|theme: &Theme| {
|
||||||
|theme: &Theme| {
|
let mut style = text::Style::default();
|
||||||
let mut style = text::Style::default();
|
style.color = Some(theme.palette().text);
|
||||||
style.color = Some(theme.palette().text);
|
style
|
||||||
style
|
}),
|
||||||
}
|
|
||||||
),
|
|
||||||
Space::with_width(Length::Fill),
|
Space::with_width(Length::Fill),
|
||||||
button(
|
button(text(airpods_info.serial_number.clone()).size(16))
|
||||||
text(airpods_info.serial_number.clone()).size(16)
|
.style(|theme: &Theme, _status| {
|
||||||
)
|
let mut style = Style::default();
|
||||||
.style(
|
style.text_color = theme.palette().text;
|
||||||
|theme: &Theme, _status| {
|
style.background = Some(Background::Color(Color::TRANSPARENT));
|
||||||
let mut style = Style::default();
|
style
|
||||||
style.text_color = theme.palette().text;
|
})
|
||||||
style.background = Some(Background::Color(Color::TRANSPARENT));
|
|
||||||
style
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.padding(0)
|
.padding(0)
|
||||||
.on_press(Message::CopyToClipboard(airpods_info.serial_number.clone()))
|
.on_press(Message::CopyToClipboard(airpods_info.serial_number.clone()))
|
||||||
],
|
],
|
||||||
row![
|
row![
|
||||||
text("Left Serial Number").size(16).style(
|
text("Left Serial Number").size(16).style(|theme: &Theme| {
|
||||||
|theme: &Theme| {
|
let mut style = text::Style::default();
|
||||||
let mut style = text::Style::default();
|
style.color = Some(theme.palette().text);
|
||||||
style.color = Some(theme.palette().text);
|
style
|
||||||
style
|
}),
|
||||||
}
|
|
||||||
),
|
|
||||||
Space::with_width(Length::Fill),
|
Space::with_width(Length::Fill),
|
||||||
button(
|
button(text(airpods_info.left_serial_number.clone()).size(16))
|
||||||
text(airpods_info.left_serial_number.clone()).size(16)
|
.style(|theme: &Theme, _status| {
|
||||||
)
|
let mut style = Style::default();
|
||||||
.style(
|
style.text_color = theme.palette().text;
|
||||||
|theme: &Theme, _status| {
|
style.background = Some(Background::Color(Color::TRANSPARENT));
|
||||||
let mut style = Style::default();
|
style
|
||||||
style.text_color = theme.palette().text;
|
})
|
||||||
style.background = Some(Background::Color(Color::TRANSPARENT));
|
|
||||||
style
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.padding(0)
|
.padding(0)
|
||||||
.on_press(Message::CopyToClipboard(airpods_info.left_serial_number.clone()))
|
.on_press(Message::CopyToClipboard(
|
||||||
|
airpods_info.left_serial_number.clone()
|
||||||
|
))
|
||||||
],
|
],
|
||||||
row![
|
row![
|
||||||
text("Right Serial Number").size(16).style(
|
text("Right Serial Number").size(16).style(|theme: &Theme| {
|
||||||
|theme: &Theme| {
|
let mut style = text::Style::default();
|
||||||
let mut style = text::Style::default();
|
style.color = Some(theme.palette().text);
|
||||||
style.color = Some(theme.palette().text);
|
style
|
||||||
style
|
}),
|
||||||
}
|
|
||||||
),
|
|
||||||
Space::with_width(Length::Fill),
|
Space::with_width(Length::Fill),
|
||||||
button(
|
button(text(airpods_info.right_serial_number.clone()).size(16))
|
||||||
text(airpods_info.right_serial_number.clone()).size(16)
|
.style(|theme: &Theme, _status| {
|
||||||
)
|
let mut style = Style::default();
|
||||||
.style(
|
style.text_color = theme.palette().text;
|
||||||
|theme: &Theme, _status| {
|
style.background = Some(Background::Color(Color::TRANSPARENT));
|
||||||
let mut style = Style::default();
|
style
|
||||||
style.text_color = theme.palette().text;
|
})
|
||||||
style.background = Some(Background::Color(Color::TRANSPARENT));
|
|
||||||
style
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.padding(0)
|
.padding(0)
|
||||||
.on_press(Message::CopyToClipboard(airpods_info.right_serial_number.clone()))
|
.on_press(Message::CopyToClipboard(
|
||||||
|
airpods_info.right_serial_number.clone()
|
||||||
|
))
|
||||||
],
|
],
|
||||||
row![
|
row![
|
||||||
text("Version 1").size(16).style(
|
text("Version 1").size(16).style(|theme: &Theme| {
|
||||||
|theme: &Theme| {
|
let mut style = text::Style::default();
|
||||||
let mut style = text::Style::default();
|
style.color = Some(theme.palette().text);
|
||||||
style.color = Some(theme.palette().text);
|
style
|
||||||
style
|
}),
|
||||||
}
|
|
||||||
),
|
|
||||||
Space::with_width(Length::Fill),
|
Space::with_width(Length::Fill),
|
||||||
text(airpods_info.version1.clone()).size(16)
|
text(airpods_info.version1.clone()).size(16)
|
||||||
],
|
],
|
||||||
row![
|
row![
|
||||||
text("Version 2").size(16).style(
|
text("Version 2").size(16).style(|theme: &Theme| {
|
||||||
|theme: &Theme| {
|
let mut style = text::Style::default();
|
||||||
let mut style = text::Style::default();
|
style.color = Some(theme.palette().text);
|
||||||
style.color = Some(theme.palette().text);
|
style
|
||||||
style
|
}),
|
||||||
}
|
|
||||||
),
|
|
||||||
Space::with_width(Length::Fill),
|
Space::with_width(Length::Fill),
|
||||||
text(airpods_info.version2.clone()).size(16)
|
text(airpods_info.version2.clone()).size(16)
|
||||||
],
|
],
|
||||||
row![
|
row![
|
||||||
text("Version 3").size(16).style(
|
text("Version 3").size(16).style(|theme: &Theme| {
|
||||||
|theme: &Theme| {
|
let mut style = text::Style::default();
|
||||||
let mut style = text::Style::default();
|
style.color = Some(theme.palette().text);
|
||||||
style.color = Some(theme.palette().text);
|
style
|
||||||
style
|
}),
|
||||||
}
|
|
||||||
),
|
|
||||||
Space::with_width(Length::Fill),
|
Space::with_width(Length::Fill),
|
||||||
text(airpods_info.version3.clone()).size(16)
|
text(airpods_info.version3.clone()).size(16)
|
||||||
]
|
]
|
||||||
@@ -508,56 +470,53 @@ pub fn airpods_view<'a>(
|
|||||||
.padding(8);
|
.padding(8);
|
||||||
|
|
||||||
information_col = column![
|
information_col = column![
|
||||||
container(
|
container(text("Device Information").size(18).style(|theme: &Theme| {
|
||||||
text("Device Information").size(18).style(
|
let mut style = text::Style::default();
|
||||||
|theme: &Theme| {
|
style.color = Some(theme.palette().primary);
|
||||||
let mut style = text::Style::default();
|
style
|
||||||
style.color = Some(theme.palette().primary);
|
}))
|
||||||
style
|
.padding(Padding {
|
||||||
}
|
|
||||||
)
|
|
||||||
).padding(Padding{
|
|
||||||
top: 5.0,
|
top: 5.0,
|
||||||
bottom: 5.0,
|
bottom: 5.0,
|
||||||
left: 18.0,
|
left: 18.0,
|
||||||
right: 18.0,
|
right: 18.0,
|
||||||
}),
|
}),
|
||||||
container(info_rows)
|
container(info_rows)
|
||||||
.padding(Padding{
|
.padding(Padding {
|
||||||
top: 5.0,
|
top: 5.0,
|
||||||
bottom: 5.0,
|
bottom: 5.0,
|
||||||
left: 10.0,
|
left: 10.0,
|
||||||
right: 10.0,
|
right: 10.0,
|
||||||
})
|
})
|
||||||
.style(
|
.style(|theme: &Theme| {
|
||||||
|theme: &Theme| {
|
let mut style = container::Style::default();
|
||||||
let mut style = container::Style::default();
|
style.background =
|
||||||
style.background = Some(Background::Color(theme.palette().primary.scale_alpha(0.1)));
|
Some(Background::Color(theme.palette().primary.scale_alpha(0.1)));
|
||||||
let mut border = Border::default();
|
let mut border = Border::default();
|
||||||
border.color = theme.palette().primary.scale_alpha(0.5);
|
border.color = theme.palette().primary.scale_alpha(0.5);
|
||||||
style.border = border.rounded(16);
|
style.border = border.rounded(16);
|
||||||
style
|
style
|
||||||
}
|
})
|
||||||
)
|
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
error!("Expected AirPodsInformation for device {}, got something else", mac.clone());
|
error!(
|
||||||
|
"Expected AirPodsInformation for device {}, got something else",
|
||||||
|
mac.clone()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
container(
|
container(column![
|
||||||
column![
|
rename_input,
|
||||||
rename_input,
|
Space::with_height(Length::from(20)),
|
||||||
Space::with_height(Length::from(20)),
|
listening_mode,
|
||||||
listening_mode,
|
Space::with_height(Length::from(20)),
|
||||||
Space::with_height(Length::from(20)),
|
audio_settings_col,
|
||||||
audio_settings_col,
|
Space::with_height(Length::from(20)),
|
||||||
Space::with_height(Length::from(20)),
|
off_listening_mode_toggle,
|
||||||
off_listening_mode_toggle,
|
Space::with_height(Length::from(20)),
|
||||||
Space::with_height(Length::from(20)),
|
information_col
|
||||||
information_col
|
])
|
||||||
]
|
|
||||||
)
|
|
||||||
.padding(20)
|
.padding(20)
|
||||||
.center_x(Length::Fill)
|
.center_x(Length::Fill)
|
||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ use crate::bluetooth::aacp::AACPEvent;
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum BluetoothUIMessage {
|
pub enum BluetoothUIMessage {
|
||||||
OpenWindow,
|
OpenWindow,
|
||||||
DeviceConnected(String), // mac
|
DeviceConnected(String), // mac
|
||||||
DeviceDisconnected(String), // mac
|
DeviceDisconnected(String), // mac
|
||||||
AACPUIEvent(String, AACPEvent), // mac, event
|
AACPUIEvent(String, AACPEvent), // mac, event
|
||||||
ATTNotification(String, u16, Vec<u8>), // mac, handle, data
|
ATTNotification(String, u16, Vec<u8>), // mac, handle, data
|
||||||
NoOp
|
NoOp,
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
|
mod airpods;
|
||||||
|
pub mod messages;
|
||||||
|
mod nothing;
|
||||||
pub mod tray;
|
pub mod tray;
|
||||||
pub mod window;
|
pub mod window;
|
||||||
pub mod messages;
|
|
||||||
mod airpods;
|
|
||||||
mod nothing;
|
|
||||||
@@ -1,73 +1,62 @@
|
|||||||
use std::collections::HashMap;
|
use crate::bluetooth::att::{ATTHandles, ATTManager};
|
||||||
use std::sync::Arc;
|
use crate::devices::enums::{DeviceData, DeviceInformation, DeviceState, NothingState};
|
||||||
use iced::{Background, Border, Length, Theme};
|
use crate::ui::window::Message;
|
||||||
use iced::widget::{container, text, column, row, Space};
|
|
||||||
use iced::widget::combo_box;
|
|
||||||
use iced::border::Radius;
|
use iced::border::Radius;
|
||||||
use iced::overlay::menu;
|
use iced::overlay::menu;
|
||||||
|
use iced::widget::combo_box;
|
||||||
use iced::widget::text_input;
|
use iced::widget::text_input;
|
||||||
use tokio::runtime::Runtime;
|
use iced::widget::{Space, column, container, row, text};
|
||||||
|
use iced::{Background, Border, Length, Theme};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use crate::bluetooth::att::{ATTManager, ATTHandles};
|
use tokio::runtime::Runtime;
|
||||||
use crate::devices::enums::{DeviceData, DeviceInformation, NothingState, DeviceState};
|
|
||||||
use crate::ui::window::Message;
|
|
||||||
|
|
||||||
pub fn nothing_view<'a>(
|
pub fn nothing_view<'a>(
|
||||||
mac: &'a str,
|
mac: &'a str,
|
||||||
devices_list: &HashMap<String, DeviceData>,
|
devices_list: &HashMap<String, DeviceData>,
|
||||||
state: &'a NothingState,
|
state: &'a NothingState,
|
||||||
att_manager: Arc<ATTManager>
|
att_manager: Arc<ATTManager>,
|
||||||
) -> iced::widget::Container<'a, Message> {
|
) -> iced::widget::Container<'a, Message> {
|
||||||
let mut information_col = iced::widget::column![];
|
let mut information_col = iced::widget::column![];
|
||||||
let mac = mac.to_string();
|
let mac = mac.to_string();
|
||||||
if let Some(device) = devices_list.get(mac.as_str()) {
|
if let Some(device) = devices_list.get(mac.as_str())
|
||||||
if let Some(DeviceInformation::Nothing(ref nothing_info)) = device.information {
|
&& let Some(DeviceInformation::Nothing(ref nothing_info)) = device.information
|
||||||
information_col = information_col
|
{
|
||||||
.push(text("Device Information").size(18).style(
|
information_col = information_col
|
||||||
|theme: &Theme| {
|
.push(text("Device Information").size(18).style(|theme: &Theme| {
|
||||||
let mut style = text::Style::default();
|
let mut style = text::Style::default();
|
||||||
style.color = Some(theme.palette().primary);
|
style.color = Some(theme.palette().primary);
|
||||||
style
|
style
|
||||||
}
|
}))
|
||||||
))
|
.push(Space::with_height(iced::Length::from(10)))
|
||||||
.push(Space::with_height(iced::Length::from(10)))
|
.push(iced::widget::row![
|
||||||
.push(
|
text("Serial Number").size(16).style(|theme: &Theme| {
|
||||||
iced::widget::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),
|
|
||||||
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
|
|
||||||
}
|
|
||||||
),
|
|
||||||
Space::with_width(Length::Fill),
|
|
||||||
text(nothing_info.firmware_version.clone()).size(16)
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let noise_control_mode = container(row![
|
|
||||||
text("Noise Control Mode").size(16).style(
|
|
||||||
|theme: &Theme| {
|
|
||||||
let mut style = text::Style::default();
|
let mut style = text::Style::default();
|
||||||
style.color = Some(theme.palette().text);
|
style.color = Some(theme.palette().text);
|
||||||
style
|
style
|
||||||
}
|
}),
|
||||||
),
|
Space::with_width(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
|
||||||
|
}),
|
||||||
|
Space::with_width(Length::Fill),
|
||||||
|
text(nothing_info.firmware_version.clone()).size(16)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let noise_control_mode = container(
|
||||||
|
row![
|
||||||
|
text("Noise Control Mode").size(16).style(|theme: &Theme| {
|
||||||
|
let mut style = text::Style::default();
|
||||||
|
style.color = Some(theme.palette().text);
|
||||||
|
style
|
||||||
|
}),
|
||||||
Space::with_width(Length::Fill),
|
Space::with_width(Length::Fill),
|
||||||
{
|
{
|
||||||
let state_clone = state.clone();
|
let state_clone = state.clone();
|
||||||
@@ -82,110 +71,110 @@ pub fn nothing_view<'a>(
|
|||||||
let att_manager = att_manager_clone.clone();
|
let att_manager = att_manager_clone.clone();
|
||||||
let selected_mode_c = selected_mode.clone();
|
let selected_mode_c = selected_mode.clone();
|
||||||
let mac_s = mac.clone();
|
let mac_s = mac.clone();
|
||||||
run_async_in_thread(
|
run_async_in_thread(async move {
|
||||||
async move {
|
if let Err(e) = att_manager
|
||||||
if let Err(e) = att_manager.write(
|
.write(
|
||||||
ATTHandles::NothingEverything,
|
ATTHandles::NothingEverything,
|
||||||
&[
|
&[
|
||||||
0x55,
|
0x55,
|
||||||
0x60, 0x01,
|
0x60,
|
||||||
0x0F, 0xF0,
|
0x01,
|
||||||
0x03, 0x00,
|
0x0F,
|
||||||
0x00, 0x01,
|
0xF0,
|
||||||
selected_mode_c.to_byte(), 0x00,
|
0x03,
|
||||||
0x00, 0x00
|
0x00,
|
||||||
]
|
0x00,
|
||||||
).await {
|
0x01,
|
||||||
log::error!("Failed to set noise cancellation mode for device {}: {}", mac_s, e);
|
selected_mode_c.to_byte(),
|
||||||
}
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
log::error!(
|
||||||
|
"Failed to set noise cancellation mode for device {}: {}",
|
||||||
|
mac_s,
|
||||||
|
e
|
||||||
|
);
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
let mut state = state_clone.clone();
|
let mut state = state_clone.clone();
|
||||||
state.anc_mode = selected_mode.clone();
|
state.anc_mode = selected_mode.clone();
|
||||||
Message::StateChanged(mac.to_string(), DeviceState::Nothing(state))
|
Message::StateChanged(mac.to_string(), DeviceState::Nothing(state))
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
.width(Length::from(200))
|
.width(Length::from(200))
|
||||||
.input_style(
|
.input_style(|theme: &Theme, _status| text_input::Style {
|
||||||
|theme: &Theme, _status| {
|
background: Background::Color(theme.palette().primary.scale_alpha(0.2)),
|
||||||
text_input::Style {
|
border: Border {
|
||||||
background: Background::Color(theme.palette().primary.scale_alpha(0.2)),
|
width: 1.0,
|
||||||
border: Border {
|
color: theme.palette().text.scale_alpha(0.3),
|
||||||
width: 1.0,
|
radius: Radius::from(4.0),
|
||||||
color: theme.palette().text.scale_alpha(0.3),
|
},
|
||||||
radius: Radius::from(4.0)
|
icon: Default::default(),
|
||||||
},
|
placeholder: theme.palette().text,
|
||||||
icon: Default::default(),
|
value: theme.palette().text,
|
||||||
placeholder: theme.palette().text,
|
selection: Default::default(),
|
||||||
value: theme.palette().text,
|
})
|
||||||
selection: Default::default(),
|
.padding(iced::Padding {
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.padding(iced::Padding{
|
|
||||||
top: 5.0,
|
top: 5.0,
|
||||||
bottom: 5.0,
|
bottom: 5.0,
|
||||||
left: 10.0,
|
left: 10.0,
|
||||||
right: 10.0,
|
right: 10.0,
|
||||||
})
|
})
|
||||||
.menu_style(
|
.menu_style(|theme: &Theme| menu::Style {
|
||||||
|theme: &Theme| {
|
background: Background::Color(theme.palette().background),
|
||||||
menu::Style {
|
border: Border {
|
||||||
background: Background::Color(theme.palette().background),
|
width: 1.0,
|
||||||
border: Border {
|
color: theme.palette().text,
|
||||||
width: 1.0,
|
radius: Radius::from(4.0),
|
||||||
color: theme.palette().text,
|
},
|
||||||
radius: Radius::from(4.0)
|
text_color: theme.palette().text,
|
||||||
},
|
selected_text_color: theme.palette().text,
|
||||||
text_color: theme.palette().text,
|
selected_background: Background::Color(
|
||||||
selected_text_color: theme.palette().text,
|
theme.palette().primary.scale_alpha(0.3),
|
||||||
selected_background: Background::Color(theme.palette().primary.scale_alpha(0.3)),
|
),
|
||||||
}
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
.align_y(iced::Alignment::Center)
|
.align_y(iced::Alignment::Center),
|
||||||
)
|
)
|
||||||
.padding(iced::Padding{
|
.padding(iced::Padding {
|
||||||
top: 5.0,
|
top: 5.0,
|
||||||
bottom: 5.0,
|
bottom: 5.0,
|
||||||
left: 18.0,
|
left: 18.0,
|
||||||
right: 18.0,
|
right: 18.0,
|
||||||
})
|
})
|
||||||
.style(
|
.style(|theme: &Theme| {
|
||||||
|theme: &Theme| {
|
let mut style = container::Style::default();
|
||||||
let mut style = container::Style::default();
|
style.background = Some(Background::Color(theme.palette().primary.scale_alpha(0.1)));
|
||||||
style.background = Some(Background::Color(theme.palette().primary.scale_alpha(0.1)));
|
let mut border = Border::default();
|
||||||
let mut border = Border::default();
|
border.color = theme.palette().primary.scale_alpha(0.5);
|
||||||
border.color = theme.palette().primary.scale_alpha(0.5);
|
style.border = border.rounded(16);
|
||||||
style.border = border.rounded(16);
|
style
|
||||||
style
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
container(
|
container(column![
|
||||||
column![
|
noise_control_mode,
|
||||||
noise_control_mode,
|
Space::with_height(Length::from(20)),
|
||||||
Space::with_height(Length::from(20)),
|
container(information_col)
|
||||||
container(information_col)
|
.style(|theme: &Theme| {
|
||||||
.style(
|
let mut style = container::Style::default();
|
||||||
|theme: &Theme| {
|
style.background =
|
||||||
let mut style = container::Style::default();
|
Some(Background::Color(theme.palette().primary.scale_alpha(0.1)));
|
||||||
style.background = Some(Background::Color(theme.palette().primary.scale_alpha(0.1)));
|
let mut border = Border::default();
|
||||||
let mut border = Border::default();
|
border.color = theme.palette().text;
|
||||||
border.color = theme.palette().text;
|
style.border = border.rounded(20);
|
||||||
style.border = border.rounded(20);
|
style
|
||||||
style
|
})
|
||||||
}
|
.padding(20)
|
||||||
)
|
])
|
||||||
.padding(20)
|
.padding(20)
|
||||||
]
|
.center_x(Length::Fill)
|
||||||
)
|
.height(Length::Fill)
|
||||||
.padding(20)
|
|
||||||
.center_x(Length::Fill)
|
|
||||||
.height(Length::Fill)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_async_in_thread<F>(fut: F)
|
fn run_async_in_thread<F>(fut: F)
|
||||||
|
|||||||
@@ -4,23 +4,26 @@ use ab_glyph::{Font, ScaleFont};
|
|||||||
use ksni::{Icon, ToolTip};
|
use ksni::{Icon, ToolTip};
|
||||||
use tokio::sync::mpsc::UnboundedSender;
|
use tokio::sync::mpsc::UnboundedSender;
|
||||||
|
|
||||||
use crate::bluetooth::aacp::ControlCommandIdentifiers;
|
use crate::bluetooth::aacp::{BatteryStatus, ControlCommandIdentifiers};
|
||||||
use crate::ui::messages::BluetoothUIMessage;
|
use crate::ui::messages::BluetoothUIMessage;
|
||||||
|
use crate::utils::get_app_settings_path;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) struct MyTray {
|
pub struct MyTray {
|
||||||
pub(crate) conversation_detect_enabled: Option<bool>,
|
pub conversation_detect_enabled: Option<bool>,
|
||||||
pub(crate) battery_l: Option<u8>,
|
pub battery_headphone: Option<u8>,
|
||||||
pub(crate) battery_l_status: Option<crate::bluetooth::aacp::BatteryStatus>,
|
pub battery_headphone_status: Option<BatteryStatus>,
|
||||||
pub(crate) battery_r: Option<u8>,
|
pub battery_l: Option<u8>,
|
||||||
pub(crate) battery_r_status: Option<crate::bluetooth::aacp::BatteryStatus>,
|
pub battery_l_status: Option<BatteryStatus>,
|
||||||
pub(crate) battery_c: Option<u8>,
|
pub battery_r: Option<u8>,
|
||||||
pub(crate) battery_c_status: Option<crate::bluetooth::aacp::BatteryStatus>,
|
pub battery_r_status: Option<BatteryStatus>,
|
||||||
pub(crate) connected: bool,
|
pub battery_c: Option<u8>,
|
||||||
pub(crate) listening_mode: Option<u8>,
|
pub battery_c_status: Option<BatteryStatus>,
|
||||||
pub(crate) allow_off_option: Option<u8>,
|
pub connected: bool,
|
||||||
pub(crate) command_tx: Option<UnboundedSender<(ControlCommandIdentifiers, Vec<u8>)>>,
|
pub listening_mode: Option<u8>,
|
||||||
pub(crate) ui_tx: Option<UnboundedSender<BluetoothUIMessage>>,
|
pub allow_off_option: Option<u8>,
|
||||||
|
pub command_tx: Option<UnboundedSender<(ControlCommandIdentifiers, Vec<u8>)>>,
|
||||||
|
pub ui_tx: Option<UnboundedSender<BluetoothUIMessage>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ksni::Tray for MyTray {
|
impl ksni::Tray for MyTray {
|
||||||
@@ -33,21 +36,27 @@ impl ksni::Tray for MyTray {
|
|||||||
fn icon_pixmap(&self) -> Vec<Icon> {
|
fn icon_pixmap(&self) -> Vec<Icon> {
|
||||||
let text = {
|
let text = {
|
||||||
let mut levels: Vec<u8> = Vec::new();
|
let mut levels: Vec<u8> = Vec::new();
|
||||||
if let Some(l) = self.battery_l {
|
if let Some(h) = self.battery_headphone {
|
||||||
if self.battery_l_status != Some(crate::bluetooth::aacp::BatteryStatus::Disconnected) {
|
if self.battery_headphone_status != Some(BatteryStatus::Disconnected) {
|
||||||
|
levels.push(h);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let Some(l) = self.battery_l
|
||||||
|
&& self.battery_l_status != Some(BatteryStatus::Disconnected)
|
||||||
|
{
|
||||||
levels.push(l);
|
levels.push(l);
|
||||||
}
|
}
|
||||||
}
|
if let Some(r) = self.battery_r
|
||||||
if let Some(r) = self.battery_r {
|
&& self.battery_r_status != Some(BatteryStatus::Disconnected)
|
||||||
if self.battery_r_status != Some(crate::bluetooth::aacp::BatteryStatus::Disconnected) {
|
{
|
||||||
levels.push(r);
|
levels.push(r);
|
||||||
}
|
}
|
||||||
|
// if let Some(c) = self.battery_c {
|
||||||
|
// if self.battery_c_status != Some(BatteryStatus::Disconnected) {
|
||||||
|
// levels.push(c);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
// if let Some(c) = self.battery_c {
|
|
||||||
// if self.battery_c_status != Some(crate::bluetooth::aacp::BatteryStatus::Disconnected) {
|
|
||||||
// levels.push(c);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
let min_battery = levels.iter().min().copied();
|
let min_battery = levels.iter().min().copied();
|
||||||
if let Some(b) = min_battery {
|
if let Some(b) = min_battery {
|
||||||
format!("{}", b)
|
format!("{}", b)
|
||||||
@@ -55,26 +64,36 @@ impl ksni::Tray for MyTray {
|
|||||||
"?".to_string()
|
"?".to_string()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let any_bud_charging = matches!(self.battery_l_status, Some(crate::bluetooth::aacp::BatteryStatus::Charging))
|
let any_bud_charging = matches!(self.battery_l_status, Some(BatteryStatus::Charging))
|
||||||
|| matches!(self.battery_r_status, Some(crate::bluetooth::aacp::BatteryStatus::Charging));
|
|| matches!(self.battery_r_status, Some(BatteryStatus::Charging));
|
||||||
let icon = generate_icon(&text, false, any_bud_charging);
|
let app_settings_path = get_app_settings_path();
|
||||||
|
let settings = std::fs::read_to_string(&app_settings_path)
|
||||||
|
.ok()
|
||||||
|
.and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok());
|
||||||
|
let text_mode = settings
|
||||||
|
.clone()
|
||||||
|
.and_then(|v| v.get("tray_text_mode").cloned())
|
||||||
|
.and_then(|ttm| serde_json::from_value(ttm).ok())
|
||||||
|
.unwrap_or(false);
|
||||||
|
let icon = generate_icon(&text, text_mode, any_bud_charging);
|
||||||
vec![icon]
|
vec![icon]
|
||||||
}
|
}
|
||||||
fn tool_tip(&self) -> ToolTip {
|
fn tool_tip(&self) -> ToolTip {
|
||||||
let format_component = |label: &str, level: Option<u8>, status: Option<crate::bluetooth::aacp::BatteryStatus>| -> String {
|
let format_component =
|
||||||
match status {
|
|label: &str, level: Option<u8>, status: Option<BatteryStatus>| -> String {
|
||||||
Some(crate::bluetooth::aacp::BatteryStatus::Disconnected) => format!("{}: -", label),
|
match status {
|
||||||
_ => {
|
Some(BatteryStatus::Disconnected) => format!("{}: -", label),
|
||||||
let pct = level.map(|b| format!("{}%", b)).unwrap_or("?".to_string());
|
_ => {
|
||||||
let suffix = if status == Some(crate::bluetooth::aacp::BatteryStatus::Charging) {
|
let pct = level.map(|b| format!("{}%", b)).unwrap_or("?".to_string());
|
||||||
"⚡"
|
let suffix = if status == Some(BatteryStatus::Charging) {
|
||||||
} else {
|
"⚡"
|
||||||
""
|
} else {
|
||||||
};
|
""
|
||||||
format!("{}: {}{}", label, pct, suffix)
|
};
|
||||||
|
format!("{}: {}{}", label, pct, suffix)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
};
|
|
||||||
|
|
||||||
let l = format_component("L", self.battery_l, self.battery_l_status);
|
let l = format_component("L", self.battery_l, self.battery_l_status);
|
||||||
let r = format_component("R", self.battery_r, self.battery_r_status);
|
let r = format_component("R", self.battery_r, self.battery_r_status);
|
||||||
@@ -104,9 +123,10 @@ impl ksni::Tray for MyTray {
|
|||||||
("Adaptive", 0x04),
|
("Adaptive", 0x04),
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
let selected = self.listening_mode.and_then(|mode| {
|
let selected = self
|
||||||
options.iter().position(|&(_, val)| val == mode)
|
.listening_mode
|
||||||
}).unwrap_or(0);
|
.and_then(|mode| options.iter().position(|&(_, val)| val == mode))
|
||||||
|
.unwrap_or(0);
|
||||||
let options_clone = options.clone();
|
let options_clone = options.clone();
|
||||||
vec![
|
vec![
|
||||||
StandardItem {
|
StandardItem {
|
||||||
@@ -118,19 +138,26 @@ impl ksni::Tray for MyTray {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}.into(),
|
}
|
||||||
|
.into(),
|
||||||
RadioGroup {
|
RadioGroup {
|
||||||
selected,
|
selected,
|
||||||
select: Box::new(move |this: &mut Self, current| {
|
select: Box::new(move |this: &mut Self, current| {
|
||||||
if let Some(tx) = &this.command_tx {
|
if let Some(tx) = &this.command_tx {
|
||||||
let value = options_clone.get(current).map(|&(_, val)| val).unwrap_or(0x02);
|
let value = options_clone
|
||||||
|
.get(current)
|
||||||
|
.map(|&(_, val)| val)
|
||||||
|
.unwrap_or(0x02);
|
||||||
let _ = tx.send((ControlCommandIdentifiers::ListeningMode, vec![value]));
|
let _ = tx.send((ControlCommandIdentifiers::ListeningMode, vec![value]));
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
options: options.into_iter().map(|(label, _)| RadioItem {
|
options: options
|
||||||
label: label.into(),
|
.into_iter()
|
||||||
..Default::default()
|
.map(|(label, _)| RadioItem {
|
||||||
}).collect(),
|
label: label.into(),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
@@ -140,13 +167,16 @@ impl ksni::Tray for MyTray {
|
|||||||
checked: self.conversation_detect_enabled.unwrap_or(false),
|
checked: self.conversation_detect_enabled.unwrap_or(false),
|
||||||
enabled: self.conversation_detect_enabled.is_some(),
|
enabled: self.conversation_detect_enabled.is_some(),
|
||||||
activate: Box::new(|this: &mut Self| {
|
activate: Box::new(|this: &mut Self| {
|
||||||
if let Some(tx) = &this.command_tx {
|
if let Some(tx) = &this.command_tx
|
||||||
if let Some(is_enabled) = this.conversation_detect_enabled {
|
&& let Some(is_enabled) = this.conversation_detect_enabled
|
||||||
let new_state = !is_enabled;
|
{
|
||||||
let value = if !new_state { 0x02 } else { 0x01 };
|
let new_state = !is_enabled;
|
||||||
let _ = tx.send((ControlCommandIdentifiers::ConversationDetectConfig, vec![value]));
|
let value = if !new_state { 0x02 } else { 0x01 };
|
||||||
this.conversation_detect_enabled = Some(new_state);
|
let _ = tx.send((
|
||||||
}
|
ControlCommandIdentifiers::ConversationDetectConfig,
|
||||||
|
vec![value],
|
||||||
|
));
|
||||||
|
this.conversation_detect_enabled = Some(new_state);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@@ -212,7 +242,8 @@ fn generate_icon(text: &str, text_mode: bool, charging: bool) -> Icon {
|
|||||||
let dist = (dx * dx + dy * dy).sqrt();
|
let dist = (dx * dx + dy * dy).sqrt();
|
||||||
if dist > inner_radius && dist <= outer_radius {
|
if dist > inner_radius && dist <= outer_radius {
|
||||||
let angle = dy.atan2(dx);
|
let angle = dy.atan2(dx);
|
||||||
let angle_from_top = (angle + std::f32::consts::PI / 2.0).rem_euclid(2.0 * std::f32::consts::PI);
|
let angle_from_top =
|
||||||
|
(angle + std::f32::consts::PI / 2.0).rem_euclid(2.0 * std::f32::consts::PI);
|
||||||
if angle_from_top <= percentage * 2.0 * std::f32::consts::PI {
|
if angle_from_top <= percentage * 2.0 * std::f32::consts::PI {
|
||||||
img.put_pixel(x, y, Rgba([0u8, 255u8, 0u8, 255u8]));
|
img.put_pixel(x, y, Rgba([0u8, 255u8, 0u8, 255u8]));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,33 @@
|
|||||||
use std::collections::HashMap;
|
use crate::bluetooth::aacp::{
|
||||||
use iced::widget::button::Style;
|
AACPEvent, BatteryComponent, BatteryStatus, ControlCommandIdentifiers,
|
||||||
use iced::widget::{button, column, container, pane_grid, text, Space, combo_box, row, text_input, scrollable, vertical_rule, rule};
|
};
|
||||||
use iced::{daemon, window, Background, Border, Center, Color, Element, Length, Size, Subscription, Task, Theme};
|
use crate::bluetooth::managers::DeviceManagers;
|
||||||
use std::sync::Arc;
|
use crate::devices::enums::{
|
||||||
|
AirPodsNoiseControlMode, AirPodsState, DeviceData, DeviceState, DeviceType, NothingAncMode,
|
||||||
|
NothingState,
|
||||||
|
};
|
||||||
|
use crate::ui::airpods::airpods_view;
|
||||||
|
use crate::ui::messages::BluetoothUIMessage;
|
||||||
|
use crate::ui::nothing::nothing_view;
|
||||||
|
use crate::utils::{MyTheme, get_app_settings_path, get_devices_path};
|
||||||
use bluer::{Address, Session};
|
use bluer::{Address, Session};
|
||||||
use iced::border::Radius;
|
use iced::border::Radius;
|
||||||
use iced::overlay::menu;
|
use iced::overlay::menu;
|
||||||
|
use iced::widget::button::Style;
|
||||||
use iced::widget::rule::FillMode;
|
use iced::widget::rule::FillMode;
|
||||||
|
use iced::widget::{
|
||||||
|
Space, button, column, combo_box, container, pane_grid, row, rule, scrollable, text,
|
||||||
|
text_input, toggler, vertical_rule,
|
||||||
|
};
|
||||||
|
use iced::{
|
||||||
|
Background, Border, Center, Element, Font, Length, Padding, Size, Subscription, Task, Theme,
|
||||||
|
daemon, window,
|
||||||
|
};
|
||||||
use log::{debug, error};
|
use log::{debug, error};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
use tokio::sync::mpsc::UnboundedReceiver;
|
use tokio::sync::mpsc::UnboundedReceiver;
|
||||||
use tokio::sync::{Mutex, RwLock};
|
use tokio::sync::{Mutex, RwLock};
|
||||||
use crate::bluetooth::aacp::{AACPEvent, ControlCommandIdentifiers};
|
|
||||||
use crate::bluetooth::managers::DeviceManagers;
|
|
||||||
use crate::devices::enums::{AirPodsNoiseControlMode, AirPodsState, DeviceData, DeviceState, DeviceType, NothingAncMode, NothingState};
|
|
||||||
use crate::ui::messages::BluetoothUIMessage;
|
|
||||||
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(
|
pub fn start_ui(
|
||||||
ui_rx: UnboundedReceiver<BluetoothUIMessage>,
|
ui_rx: UnboundedReceiver<BluetoothUIMessage>,
|
||||||
@@ -26,6 +37,8 @@ pub fn start_ui(
|
|||||||
daemon(App::title, App::update, App::view)
|
daemon(App::title, App::update, App::view)
|
||||||
.subscription(App::subscription)
|
.subscription(App::subscription)
|
||||||
.theme(App::theme)
|
.theme(App::theme)
|
||||||
|
.font(include_bytes!("../../assets/font/sf_pro.otf").as_slice())
|
||||||
|
.default_font(Font::with_name("SF Pro Text"))
|
||||||
.run_with(move || App::new(ui_rx, start_minimized, device_managers))
|
.run_with(move || App::new(ui_rx, start_minimized, device_managers))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,10 +56,11 @@ pub struct App {
|
|||||||
pending_add_device: Option<(String, Address)>,
|
pending_add_device: Option<(String, Address)>,
|
||||||
device_type_state: combo_box::State<DeviceType>,
|
device_type_state: combo_box::State<DeviceType>,
|
||||||
selected_device_type: Option<DeviceType>,
|
selected_device_type: Option<DeviceType>,
|
||||||
|
tray_text_mode: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct BluetoothState {
|
pub struct BluetoothState {
|
||||||
connected_devices: Vec<String>
|
connected_devices: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BluetoothState {
|
impl BluetoothState {
|
||||||
@@ -73,13 +87,14 @@ pub enum Message {
|
|||||||
ConfirmAddDevice,
|
ConfirmAddDevice,
|
||||||
CancelAddDevice,
|
CancelAddDevice,
|
||||||
StateChanged(String, DeviceState),
|
StateChanged(String, DeviceState),
|
||||||
|
TrayTextModeChanged(bool), // yes, I know I should add all settings to a struct, but I'm lazy
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||||
pub enum Tab {
|
pub enum Tab {
|
||||||
Device(String),
|
Device(String),
|
||||||
Settings,
|
Settings,
|
||||||
AddDevice
|
AddDevice,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
@@ -100,10 +115,7 @@ impl App {
|
|||||||
|
|
||||||
let ui_rx = Arc::new(Mutex::new(ui_rx));
|
let ui_rx = Arc::new(Mutex::new(ui_rx));
|
||||||
|
|
||||||
let wait_task = Task::perform(
|
let wait_task = Task::perform(wait_for_message(Arc::clone(&ui_rx)), |msg| msg);
|
||||||
wait_for_message(Arc::clone(&ui_rx)),
|
|
||||||
|msg| msg,
|
|
||||||
);
|
|
||||||
|
|
||||||
let (window, open_task) = if start_minimized {
|
let (window, open_task) = if start_minimized {
|
||||||
(None, Task::none())
|
(None, Task::none())
|
||||||
@@ -116,12 +128,19 @@ impl App {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let app_settings_path = get_app_settings_path();
|
let app_settings_path = get_app_settings_path();
|
||||||
let selected_theme = std::fs::read_to_string(&app_settings_path)
|
let settings = std::fs::read_to_string(&app_settings_path)
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
|
.and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok());
|
||||||
|
let selected_theme = settings
|
||||||
|
.clone()
|
||||||
.and_then(|v| v.get("theme").cloned())
|
.and_then(|v| v.get("theme").cloned())
|
||||||
.and_then(|t| serde_json::from_value(t).ok())
|
.and_then(|t| serde_json::from_value(t).ok())
|
||||||
.unwrap_or(MyTheme::Dark);
|
.unwrap_or(MyTheme::Dark);
|
||||||
|
let tray_text_mode = settings
|
||||||
|
.clone()
|
||||||
|
.and_then(|v| v.get("tray_text_mode").cloned())
|
||||||
|
.and_then(|ttm| serde_json::from_value(ttm).ok())
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
let bluetooth_state = BluetoothState::new();
|
let bluetooth_state = BluetoothState::new();
|
||||||
|
|
||||||
@@ -168,13 +187,12 @@ impl App {
|
|||||||
paired_devices: HashMap::new(),
|
paired_devices: HashMap::new(),
|
||||||
device_states,
|
device_states,
|
||||||
pending_add_device: None,
|
pending_add_device: None,
|
||||||
device_type_state: combo_box::State::new(vec![
|
device_type_state: combo_box::State::new(vec![DeviceType::Nothing]),
|
||||||
DeviceType::Nothing
|
|
||||||
]),
|
|
||||||
selected_device_type: None,
|
selected_device_type: None,
|
||||||
device_managers
|
device_managers,
|
||||||
|
tray_text_mode,
|
||||||
},
|
},
|
||||||
Task::batch(vec![open_task, wait_task])
|
Task::batch(vec![open_task, wait_task]),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,55 +223,45 @@ impl App {
|
|||||||
Message::ThemeSelected(theme) => {
|
Message::ThemeSelected(theme) => {
|
||||||
self.selected_theme = theme;
|
self.selected_theme = theme;
|
||||||
let app_settings_path = get_app_settings_path();
|
let app_settings_path = get_app_settings_path();
|
||||||
let settings = serde_json::json!({"theme": self.selected_theme});
|
let settings = serde_json::json!({"theme": self.selected_theme, "tray_text_mode": self.tray_text_mode});
|
||||||
debug!("Writing settings to {}: {}", app_settings_path.to_str().unwrap() , settings);
|
debug!(
|
||||||
|
"Writing settings to {}: {}",
|
||||||
|
app_settings_path.to_str().unwrap(),
|
||||||
|
settings
|
||||||
|
);
|
||||||
std::fs::write(app_settings_path, settings.to_string()).ok();
|
std::fs::write(app_settings_path, settings.to_string()).ok();
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::CopyToClipboard(data) => {
|
Message::CopyToClipboard(data) => iced::clipboard::write(data),
|
||||||
iced::clipboard::write(data)
|
|
||||||
}
|
|
||||||
Message::BluetoothMessage(ui_message) => {
|
Message::BluetoothMessage(ui_message) => {
|
||||||
match ui_message {
|
match ui_message {
|
||||||
BluetoothUIMessage::NoOp => {
|
BluetoothUIMessage::NoOp => {
|
||||||
let ui_rx = Arc::clone(&self.ui_rx);
|
let ui_rx = Arc::clone(&self.ui_rx);
|
||||||
let wait_task = Task::perform(
|
|
||||||
wait_for_message(ui_rx),
|
Task::perform(wait_for_message(ui_rx), |msg| msg)
|
||||||
|msg| msg,
|
|
||||||
);
|
|
||||||
wait_task
|
|
||||||
}
|
}
|
||||||
BluetoothUIMessage::OpenWindow => {
|
BluetoothUIMessage::OpenWindow => {
|
||||||
let ui_rx = Arc::clone(&self.ui_rx);
|
let ui_rx = Arc::clone(&self.ui_rx);
|
||||||
let wait_task = Task::perform(
|
let wait_task = Task::perform(wait_for_message(ui_rx), |msg| msg);
|
||||||
wait_for_message(ui_rx),
|
|
||||||
|msg| msg,
|
|
||||||
);
|
|
||||||
debug!("Opening main window...");
|
debug!("Opening main window...");
|
||||||
if let Some(window_id) = self.window {
|
if let Some(window_id) = self.window {
|
||||||
Task::batch(vec![
|
Task::batch(vec![window::gain_focus(window_id), wait_task])
|
||||||
window::gain_focus(window_id),
|
|
||||||
wait_task,
|
|
||||||
])
|
|
||||||
} else {
|
} else {
|
||||||
let mut settings = window::Settings::default();
|
let mut settings = window::Settings::default();
|
||||||
settings.min_size = Some(Size::new(400.0, 300.0));
|
settings.min_size = Some(Size::new(400.0, 300.0));
|
||||||
settings.icon = window::icon::from_file("../../assets/icon.png").ok();
|
settings.icon = window::icon::from_file("../../assets/icon.png").ok();
|
||||||
let (new_window_task, open_task) = window::open(settings);
|
let (new_window_task, open_task) = window::open(settings);
|
||||||
self.window = Some(new_window_task);
|
self.window = Some(new_window_task);
|
||||||
Task::batch(vec![
|
Task::batch(vec![open_task.map(Message::WindowOpened), wait_task])
|
||||||
open_task.map(Message::WindowOpened),
|
|
||||||
wait_task,
|
|
||||||
])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BluetoothUIMessage::DeviceConnected(mac) => {
|
BluetoothUIMessage::DeviceConnected(mac) => {
|
||||||
let ui_rx = Arc::clone(&self.ui_rx);
|
let ui_rx = Arc::clone(&self.ui_rx);
|
||||||
let wait_task = Task::perform(
|
let wait_task = Task::perform(wait_for_message(ui_rx), |msg| msg);
|
||||||
wait_for_message(ui_rx),
|
debug!(
|
||||||
|msg| msg,
|
"Device connected: {}. Adding to connected devices list",
|
||||||
|
mac
|
||||||
);
|
);
|
||||||
debug!("Device connected: {}. Adding to connected devices list", mac);
|
|
||||||
let mut already_connected = false;
|
let mut already_connected = false;
|
||||||
for device in &self.bluetooth_state.connected_devices {
|
for device in &self.bluetooth_state.connected_devices {
|
||||||
if device == &mac {
|
if device == &mac {
|
||||||
@@ -270,14 +278,16 @@ impl App {
|
|||||||
// }));
|
// }));
|
||||||
|
|
||||||
let type_ = {
|
let type_ = {
|
||||||
let devices_json = std::fs::read_to_string(get_devices_path()).unwrap_or_else(|e| {
|
let devices_json = std::fs::read_to_string(get_devices_path())
|
||||||
error!("Failed to read devices file: {}", e);
|
.unwrap_or_else(|e| {
|
||||||
"{}".to_string()
|
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);
|
let devices_list: HashMap<String, DeviceData> =
|
||||||
HashMap::new()
|
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())
|
devices_list.get(&mac).map(|d| d.type_.clone())
|
||||||
};
|
};
|
||||||
match type_ {
|
match type_ {
|
||||||
@@ -289,21 +299,27 @@ impl App {
|
|||||||
let state = aacp_manager_state.blocking_lock();
|
let state = aacp_manager_state.blocking_lock();
|
||||||
debug!("AACP manager found for AirPods device {}", mac);
|
debug!("AACP manager found for AirPods device {}", mac);
|
||||||
let device_name = {
|
let device_name = {
|
||||||
let devices_json = std::fs::read_to_string(get_devices_path()).unwrap_or_else(|e| {
|
let devices_json = std::fs::read_to_string(get_devices_path())
|
||||||
error!("Failed to read devices file: {}", e);
|
.unwrap_or_else(|e| {
|
||||||
"{}".to_string()
|
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);
|
let devices_list: HashMap<String, DeviceData> =
|
||||||
HashMap::new()
|
serde_json::from_str(&devices_json).unwrap_or_else(|e| {
|
||||||
});
|
error!("Deserialization failed: {}", e);
|
||||||
devices_list.get(&mac).map(|d| d.name.clone()).unwrap_or_else(|| "Unknown Device".to_string())
|
HashMap::new()
|
||||||
|
});
|
||||||
|
devices_list
|
||||||
|
.get(&mac)
|
||||||
|
.map(|d| d.name.clone())
|
||||||
|
.unwrap_or_else(|| "Unknown Device".to_string())
|
||||||
};
|
};
|
||||||
self.device_states.insert(mac.clone(), DeviceState::AirPods(AirPodsState {
|
self.device_states.insert(mac.clone(), DeviceState::AirPods(AirPodsState {
|
||||||
device_name,
|
device_name,
|
||||||
|
battery: state.battery_info.clone(),
|
||||||
noise_control_mode: state.control_command_status_list.iter().find_map(|status| {
|
noise_control_mode: state.control_command_status_list.iter().find_map(|status| {
|
||||||
if status.identifier == ControlCommandIdentifiers::ListeningMode {
|
if status.identifier == ControlCommandIdentifiers::ListeningMode {
|
||||||
status.value.get(0).map(|b| AirPodsNoiseControlMode::from_byte(b))
|
status.value.first().map(AirPodsNoiseControlMode::from_byte)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@@ -339,130 +355,144 @@ impl App {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
Some(DeviceType::Nothing) => {
|
Some(DeviceType::Nothing) => {
|
||||||
self.device_states.insert(mac.clone(), DeviceState::Nothing(NothingState {
|
self.device_states.insert(
|
||||||
anc_mode: NothingAncMode::Off,
|
mac.clone(),
|
||||||
anc_mode_state: combo_box::State::new(vec![
|
DeviceState::Nothing(NothingState {
|
||||||
NothingAncMode::Off,
|
anc_mode: NothingAncMode::Off,
|
||||||
NothingAncMode::Transparency,
|
anc_mode_state: combo_box::State::new(vec![
|
||||||
NothingAncMode::AdaptiveNoiseCancellation,
|
NothingAncMode::Off,
|
||||||
NothingAncMode::LowNoiseCancellation,
|
NothingAncMode::Transparency,
|
||||||
NothingAncMode::MidNoiseCancellation,
|
NothingAncMode::AdaptiveNoiseCancellation,
|
||||||
NothingAncMode::HighNoiseCancellation
|
NothingAncMode::LowNoiseCancellation,
|
||||||
]),
|
NothingAncMode::MidNoiseCancellation,
|
||||||
}));
|
NothingAncMode::HighNoiseCancellation,
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
Task::batch(vec![
|
Task::batch(vec![wait_task])
|
||||||
wait_task,
|
|
||||||
])
|
|
||||||
}
|
}
|
||||||
BluetoothUIMessage::DeviceDisconnected(mac) => {
|
BluetoothUIMessage::DeviceDisconnected(mac) => {
|
||||||
let ui_rx = Arc::clone(&self.ui_rx);
|
let ui_rx = Arc::clone(&self.ui_rx);
|
||||||
let wait_task = Task::perform(
|
let wait_task = Task::perform(wait_for_message(ui_rx), |msg| msg);
|
||||||
wait_for_message(ui_rx),
|
|
||||||
|msg| msg,
|
|
||||||
);
|
|
||||||
debug!("Device disconnected: {}", mac);
|
debug!("Device disconnected: {}", mac);
|
||||||
|
|
||||||
self.device_states.remove(&mac);
|
self.device_states.remove(&mac);
|
||||||
Task::batch(vec![
|
Task::batch(vec![wait_task])
|
||||||
wait_task,
|
|
||||||
])
|
|
||||||
}
|
}
|
||||||
BluetoothUIMessage::AACPUIEvent(mac, event) => {
|
BluetoothUIMessage::AACPUIEvent(mac, event) => {
|
||||||
let ui_rx = Arc::clone(&self.ui_rx);
|
let ui_rx = Arc::clone(&self.ui_rx);
|
||||||
let wait_task = Task::perform(
|
let wait_task = Task::perform(wait_for_message(ui_rx), |msg| msg);
|
||||||
wait_for_message(ui_rx),
|
|
||||||
|msg| msg,
|
|
||||||
);
|
|
||||||
debug!("AACP UI Event for {}: {:?}", mac, event);
|
debug!("AACP UI Event for {}: {:?}", mac, event);
|
||||||
match event {
|
match event {
|
||||||
AACPEvent::ControlCommand(status) => {
|
AACPEvent::ControlCommand(status) => match status.identifier {
|
||||||
match status.identifier {
|
ControlCommandIdentifiers::ListeningMode => {
|
||||||
ControlCommandIdentifiers::ListeningMode => {
|
let mode = status
|
||||||
let mode = status.value.get(0).map(|b| AirPodsNoiseControlMode::from_byte(b)).unwrap_or(AirPodsNoiseControlMode::Transparency);
|
.value
|
||||||
if let Some(DeviceState::AirPods(state)) = self.device_states.get_mut(&mac) {
|
.first()
|
||||||
state.noise_control_mode = mode;
|
.map(AirPodsNoiseControlMode::from_byte)
|
||||||
}
|
.unwrap_or(AirPodsNoiseControlMode::Transparency);
|
||||||
|
if let Some(DeviceState::AirPods(state)) =
|
||||||
|
self.device_states.get_mut(&mac)
|
||||||
|
{
|
||||||
|
state.noise_control_mode = mode;
|
||||||
}
|
}
|
||||||
ControlCommandIdentifiers::ConversationDetectConfig => {
|
}
|
||||||
let is_enabled = match status.value.as_slice() {
|
ControlCommandIdentifiers::ConversationDetectConfig => {
|
||||||
[0x01] => true,
|
let is_enabled = match status.value.as_slice() {
|
||||||
[0x02] => false,
|
[0x01] => true,
|
||||||
_ => {
|
[0x02] => false,
|
||||||
error!("Unknown Conversation Detect Config value: {:?}", status.value);
|
_ => {
|
||||||
false
|
error!(
|
||||||
}
|
"Unknown Conversation Detect Config value: {:?}",
|
||||||
};
|
status.value
|
||||||
if let Some(DeviceState::AirPods(state)) = self.device_states.get_mut(&mac) {
|
|
||||||
state.conversation_awareness_enabled = is_enabled;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ControlCommandIdentifiers::AdaptiveVolumeConfig => {
|
|
||||||
let is_enabled = match status.value.as_slice() {
|
|
||||||
[0x01] => true,
|
|
||||||
[0x02] => false,
|
|
||||||
_ => {
|
|
||||||
error!("Unknown Adaptive Volume Config value: {:?}", status.value);
|
|
||||||
false
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if let Some(DeviceState::AirPods(state)) = self.device_states.get_mut(&mac) {
|
|
||||||
state.personalized_volume_enabled = is_enabled;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ControlCommandIdentifiers::AllowOffOption => {
|
|
||||||
let is_enabled = match status.value.as_slice() {
|
|
||||||
[0x01] => true,
|
|
||||||
[0x02] => false,
|
|
||||||
_ => {
|
|
||||||
error!("Unknown Allow Off Option value: {:?}", status.value);
|
|
||||||
false
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if let Some(DeviceState::AirPods(state)) = self.device_states.get_mut(&mac) {
|
|
||||||
state.allow_off_mode = is_enabled;
|
|
||||||
state.noise_control_state = combo_box::State::new(
|
|
||||||
{
|
|
||||||
let mut modes = vec![
|
|
||||||
AirPodsNoiseControlMode::Transparency,
|
|
||||||
AirPodsNoiseControlMode::NoiseCancellation,
|
|
||||||
AirPodsNoiseControlMode::Adaptive
|
|
||||||
];
|
|
||||||
if is_enabled {
|
|
||||||
modes.insert(0, AirPodsNoiseControlMode::Off);
|
|
||||||
}
|
|
||||||
modes
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
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);
|
ControlCommandIdentifiers::AdaptiveVolumeConfig => {
|
||||||
|
let is_enabled = match status.value.as_slice() {
|
||||||
|
[0x01] => true,
|
||||||
|
[0x02] => false,
|
||||||
|
_ => {
|
||||||
|
error!(
|
||||||
|
"Unknown Adaptive Volume Config value: {:?}",
|
||||||
|
status.value
|
||||||
|
);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Some(DeviceState::AirPods(state)) =
|
||||||
|
self.device_states.get_mut(&mac)
|
||||||
|
{
|
||||||
|
state.personalized_volume_enabled = is_enabled;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ControlCommandIdentifiers::AllowOffOption => {
|
||||||
|
let is_enabled = match status.value.as_slice() {
|
||||||
|
[0x01] => true,
|
||||||
|
[0x02] => false,
|
||||||
|
_ => {
|
||||||
|
error!(
|
||||||
|
"Unknown Allow Off Option value: {:?}",
|
||||||
|
status.value
|
||||||
|
);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Some(DeviceState::AirPods(state)) =
|
||||||
|
self.device_states.get_mut(&mac)
|
||||||
|
{
|
||||||
|
state.allow_off_mode = is_enabled;
|
||||||
|
state.noise_control_state = combo_box::State::new({
|
||||||
|
let mut modes = vec![
|
||||||
|
AirPodsNoiseControlMode::Transparency,
|
||||||
|
AirPodsNoiseControlMode::NoiseCancellation,
|
||||||
|
AirPodsNoiseControlMode::Adaptive,
|
||||||
|
];
|
||||||
|
if is_enabled {
|
||||||
|
modes.insert(0, AirPodsNoiseControlMode::Off);
|
||||||
|
}
|
||||||
|
modes
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
debug!("Unhandled Control Command Status: {:?}", status);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
AACPEvent::BatteryInfo(battery_info) => {
|
||||||
|
if let Some(DeviceState::AirPods(state)) =
|
||||||
|
self.device_states.get_mut(&mac)
|
||||||
|
{
|
||||||
|
state.battery = battery_info;
|
||||||
|
debug!("Updated battery info for {}: {:?}", mac, state.battery);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
Task::batch(vec![
|
Task::batch(vec![wait_task])
|
||||||
wait_task,
|
|
||||||
])
|
|
||||||
}
|
}
|
||||||
BluetoothUIMessage::ATTNotification(mac, handle, value) => {
|
BluetoothUIMessage::ATTNotification(mac, handle, value) => {
|
||||||
debug!("ATT Notification for {}: handle=0x{:04X}, value={:?}", mac, handle, value);
|
debug!(
|
||||||
|
"ATT Notification for {}: handle=0x{:04X}, value={:?}",
|
||||||
|
mac, handle, value
|
||||||
|
);
|
||||||
|
|
||||||
// TODO: Handle Nothing's ANC Mode changes here
|
// TODO: Handle Nothing's ANC Mode changes here
|
||||||
|
|
||||||
let ui_rx = Arc::clone(&self.ui_rx);
|
let ui_rx = Arc::clone(&self.ui_rx);
|
||||||
let wait_task = Task::perform(
|
let wait_task = Task::perform(wait_for_message(ui_rx), |msg| msg);
|
||||||
wait_for_message(ui_rx),
|
Task::batch(vec![wait_task])
|
||||||
|msg| msg,
|
|
||||||
);
|
|
||||||
Task::batch(vec![
|
|
||||||
wait_task,
|
|
||||||
])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -485,31 +515,35 @@ impl App {
|
|||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::ConfirmAddDevice => {
|
Message::ConfirmAddDevice => {
|
||||||
if let Some((name, addr)) = self.pending_add_device.take() {
|
if let Some((name, addr)) = self.pending_add_device.take()
|
||||||
if let Some(type_) = self.selected_device_type.take() {
|
&& 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| {
|
let devices_path = get_devices_path();
|
||||||
error!("Failed to read devices file: {}", e);
|
let devices_json = std::fs::read_to_string(&devices_path).unwrap_or_else(|e| {
|
||||||
"{}".to_string()
|
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| {
|
});
|
||||||
|
let mut devices_list: HashMap<String, DeviceData> =
|
||||||
|
serde_json::from_str(&devices_json).unwrap_or_else(|e| {
|
||||||
error!("Deserialization failed: {}", e);
|
error!("Deserialization failed: {}", e);
|
||||||
HashMap::new()
|
HashMap::new()
|
||||||
});
|
});
|
||||||
devices_list.insert(addr.to_string(), DeviceData {
|
devices_list.insert(
|
||||||
|
addr.to_string(),
|
||||||
|
DeviceData {
|
||||||
name,
|
name,
|
||||||
type_: type_.clone(),
|
type_: type_.clone(),
|
||||||
information: None
|
information: None,
|
||||||
});
|
},
|
||||||
let updated_json = serde_json::to_string(&devices_list).unwrap_or_else(|e| {
|
);
|
||||||
error!("Serialization failed: {}", e);
|
let updated_json = serde_json::to_string(&devices_list).unwrap_or_else(|e| {
|
||||||
"{}".to_string()
|
error!("Serialization failed: {}", e);
|
||||||
});
|
"{}".to_string()
|
||||||
if let Err(e) = std::fs::write(&devices_path, updated_json) {
|
});
|
||||||
error!("Failed to write devices file: {}", e);
|
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());
|
|
||||||
}
|
}
|
||||||
|
self.selected_tab = Tab::Device(addr.to_string());
|
||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
@@ -522,38 +556,47 @@ impl App {
|
|||||||
self.device_states.insert(mac.clone(), state);
|
self.device_states.insert(mac.clone(), state);
|
||||||
// if airpods, update the noise control state combo box based on allow off mode
|
// if airpods, update the noise control state combo box based on allow off mode
|
||||||
let type_ = {
|
let type_ = {
|
||||||
let devices_json = std::fs::read_to_string(get_devices_path()).unwrap_or_else(|e| {
|
let devices_json =
|
||||||
error!("Failed to read devices file: {}", e);
|
std::fs::read_to_string(get_devices_path()).unwrap_or_else(|e| {
|
||||||
"{}".to_string()
|
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);
|
let devices_list: HashMap<String, DeviceData> =
|
||||||
HashMap::new()
|
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())
|
devices_list.get(&mac).map(|d| d.type_.clone())
|
||||||
};
|
};
|
||||||
match type_ {
|
if let Some(DeviceType::AirPods) = type_
|
||||||
Some(DeviceType::AirPods) => {
|
&& let Some(DeviceState::AirPods(state)) = self.device_states.get_mut(&mac)
|
||||||
if let Some(DeviceState::AirPods(state)) = self.device_states.get_mut(&mac) {
|
{
|
||||||
state.noise_control_state = combo_box::State::new(
|
state.noise_control_state = combo_box::State::new({
|
||||||
{
|
let mut modes = vec![
|
||||||
let mut modes = vec![
|
AirPodsNoiseControlMode::Transparency,
|
||||||
AirPodsNoiseControlMode::Transparency,
|
AirPodsNoiseControlMode::NoiseCancellation,
|
||||||
AirPodsNoiseControlMode::NoiseCancellation,
|
AirPodsNoiseControlMode::Adaptive,
|
||||||
AirPodsNoiseControlMode::Adaptive
|
];
|
||||||
];
|
if state.allow_off_mode {
|
||||||
if state.allow_off_mode {
|
modes.insert(0, AirPodsNoiseControlMode::Off);
|
||||||
modes.insert(0, AirPodsNoiseControlMode::Off);
|
|
||||||
}
|
|
||||||
modes
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
modes
|
||||||
_ => {}
|
});
|
||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
Message::TrayTextModeChanged(is_enabled) => {
|
||||||
|
self.tray_text_mode = is_enabled;
|
||||||
|
let app_settings_path = get_app_settings_path();
|
||||||
|
let settings = serde_json::json!({"theme": self.selected_theme, "tray_text_mode": self.tray_text_mode});
|
||||||
|
debug!(
|
||||||
|
"Writing settings to {}: {}",
|
||||||
|
app_settings_path.to_str().unwrap(),
|
||||||
|
settings
|
||||||
|
);
|
||||||
|
std::fs::write(app_settings_path, settings.to_string()).ok();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -562,25 +605,64 @@ impl App {
|
|||||||
error!("Failed to read devices file: {}", e);
|
error!("Failed to read devices file: {}", e);
|
||||||
"{}".to_string()
|
"{}".to_string()
|
||||||
});
|
});
|
||||||
let devices_list: HashMap<String, DeviceData> = serde_json::from_str(&devices_json).unwrap_or_else(|e| {
|
let devices_list: HashMap<String, DeviceData> = serde_json::from_str(&devices_json)
|
||||||
error!("Deserialization failed: {}", e);
|
.unwrap_or_else(|e| {
|
||||||
HashMap::new()
|
error!("Deserialization failed: {}", e);
|
||||||
});
|
HashMap::new()
|
||||||
|
});
|
||||||
let pane_grid = pane_grid::PaneGrid::new(&self.panes, |_pane_id, pane, _is_maximized| {
|
let pane_grid = pane_grid::PaneGrid::new(&self.panes, |_pane_id, pane, _is_maximized| {
|
||||||
match pane {
|
match pane {
|
||||||
Pane::Sidebar => {
|
Pane::Sidebar => {
|
||||||
let create_tab_button = |tab: Tab, label: &str, description: &str, connected: bool| -> Element<'_, Message> {
|
let create_tab_button = |tab: Tab, label: &str, mac_addr: &str, connected: bool| -> Element<'_, Message> {
|
||||||
let label = label.to_string();
|
let label = label.to_string() + if connected { " " } else { "" };
|
||||||
let is_selected = self.selected_tab == tab;
|
let is_selected = self.selected_tab == tab;
|
||||||
let col = column![
|
let col = column![
|
||||||
text(label).size(16),
|
text(label).size(16),
|
||||||
text(
|
text({
|
||||||
if connected {
|
if connected {
|
||||||
format!("Connected - {}", description)
|
let mac = match tab {
|
||||||
|
Tab::Device(ref mac) => mac.as_str(),
|
||||||
|
_ => "",
|
||||||
|
};
|
||||||
|
|
||||||
|
match self.device_states.get(mac) {
|
||||||
|
Some(DeviceState::AirPods(state)) => {
|
||||||
|
let b = &state.battery;
|
||||||
|
let headphone = b.iter().find(|x| x.component == BatteryComponent::Headphone)
|
||||||
|
.map(|x| x.level);
|
||||||
|
// if headphones is not None, use only that
|
||||||
|
if let Some(level) = headphone {
|
||||||
|
let charging = b.iter().find(|x| x.component == BatteryComponent::Headphone)
|
||||||
|
.map(|x| x.status == BatteryStatus::Charging).unwrap_or(false);
|
||||||
|
format!(
|
||||||
|
" {}%{}",
|
||||||
|
level, if charging {"\u{1002E6}"} else {""}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
let left = b.iter().find(|x| x.component == BatteryComponent::Left)
|
||||||
|
.map(|x| x.level).unwrap_or_default();
|
||||||
|
let right = b.iter().find(|x| x.component == BatteryComponent::Right)
|
||||||
|
.map(|x| x.level).unwrap_or_default();
|
||||||
|
let case = b.iter().find(|x| x.component == BatteryComponent::Case)
|
||||||
|
.map(|x| x.level).unwrap_or_default();
|
||||||
|
let left_charging = b.iter().find(|x| x.component == BatteryComponent::Left)
|
||||||
|
.map(|x| x.status == BatteryStatus::Charging).unwrap_or(false);
|
||||||
|
let right_charging = b.iter().find(|x| x.component == BatteryComponent::Right)
|
||||||
|
.map(|x| x.status == BatteryStatus::Charging).unwrap_or(false);
|
||||||
|
let case_charging = b.iter().find(|x| x.component == BatteryComponent::Case)
|
||||||
|
.map(|x| x.status == BatteryStatus::Charging).unwrap_or(false);
|
||||||
|
format!(
|
||||||
|
"\u{1018E5} {}%{} \u{1018E8} {}%{} \u{100E6C} {}%{}",
|
||||||
|
left, if left_charging {"\u{1002E6}"} else {""}, right, if right_charging {"\u{1002E6}"} else {""}, case, if case_charging {"\u{1002E6}"} else {""}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => "Connected".to_string(),
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
format!("{}", description)
|
mac_addr.to_string()
|
||||||
}
|
}
|
||||||
).size(12)
|
}).size(12)
|
||||||
];
|
];
|
||||||
let content = container(col)
|
let content = container(col)
|
||||||
.padding(8);
|
.padding(8);
|
||||||
@@ -724,22 +806,17 @@ impl App {
|
|||||||
debug!("Rendering device view for {}: type={:?}, state={:?}", id, device_type, device_state);
|
debug!("Rendering device view for {}: type={:?}, state={:?}", id, device_type, device_state);
|
||||||
match device_type {
|
match device_type {
|
||||||
Some(DeviceType::AirPods) => {
|
Some(DeviceType::AirPods) => {
|
||||||
let view = device_state.as_ref().and_then(|state| {
|
|
||||||
|
device_state.as_ref().and_then(|state| {
|
||||||
match state {
|
match state {
|
||||||
DeviceState::AirPods(state) => {
|
DeviceState::AirPods(state) => {
|
||||||
device_managers.get(id).and_then(|managers| {
|
device_managers.get(id).and_then(|managers| {
|
||||||
managers.get_aacp().and_then(|aacp_manager| {
|
managers.get_aacp().map(|aacp_manager| airpods_view(
|
||||||
// managers.get_att().map(|att_manager| {
|
|
||||||
Some(airpods_view(
|
|
||||||
id,
|
id,
|
||||||
&devices_list,
|
&devices_list,
|
||||||
state,
|
state,
|
||||||
aacp_manager.clone()
|
aacp_manager.clone()
|
||||||
),
|
))
|
||||||
// att_manager.clone(),
|
|
||||||
)
|
|
||||||
// })
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
@@ -750,8 +827,7 @@ impl App {
|
|||||||
)
|
)
|
||||||
.center_x(Length::Fill)
|
.center_x(Length::Fill)
|
||||||
.center_y(Length::Fill)
|
.center_y(Length::Fill)
|
||||||
});
|
})
|
||||||
view
|
|
||||||
}
|
}
|
||||||
Some(DeviceType::Nothing) => {
|
Some(DeviceType::Nothing) => {
|
||||||
if let Some(DeviceState::Nothing(state)) = device_state {
|
if let Some(DeviceState::Nothing(state)) = device_state {
|
||||||
@@ -791,12 +867,65 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Tab::Settings => {
|
Tab::Settings => {
|
||||||
container(
|
let tray_text_mode_toggle = container(
|
||||||
column![
|
row![
|
||||||
text("Settings").size(40),
|
column![
|
||||||
Space::with_height(Length::from(20)),
|
text("Use text in tray").size(16),
|
||||||
|
text("Use text for battery status in tray instead of a progress bar.").size(12).style(
|
||||||
|
|theme: &Theme| {
|
||||||
|
let mut style = text::Style::default();
|
||||||
|
style.color = Some(theme.palette().text.scale_alpha(0.7));
|
||||||
|
style
|
||||||
|
}
|
||||||
|
).width(Length::Fill)
|
||||||
|
].width(Length::Fill),
|
||||||
|
toggler(self.tray_text_mode)
|
||||||
|
.on_toggle(move |is_enabled| {
|
||||||
|
Message::TrayTextModeChanged(is_enabled)
|
||||||
|
})
|
||||||
|
.spacing(0)
|
||||||
|
.size(20)
|
||||||
|
]
|
||||||
|
.align_y(Center)
|
||||||
|
.spacing(12)
|
||||||
|
)
|
||||||
|
.padding(Padding{
|
||||||
|
top: 5.0,
|
||||||
|
bottom: 5.0,
|
||||||
|
left: 18.0,
|
||||||
|
right: 18.0,
|
||||||
|
})
|
||||||
|
.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().primary.scale_alpha(0.5);
|
||||||
|
style.border = border.rounded(16);
|
||||||
|
style
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.align_y(Center);
|
||||||
|
|
||||||
|
let appearance_settings_col = column![
|
||||||
|
container(
|
||||||
|
text("Appearance").size(20).style(
|
||||||
|
|theme: &Theme| {
|
||||||
|
let mut style = text::Style::default();
|
||||||
|
style.color = Some(theme.palette().primary);
|
||||||
|
style
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.padding(Padding{
|
||||||
|
top: 0.0,
|
||||||
|
bottom: 0.0,
|
||||||
|
left: 18.0,
|
||||||
|
right: 18.0,
|
||||||
|
}),
|
||||||
|
container(
|
||||||
row![
|
row![
|
||||||
text("Theme:")
|
text("Theme")
|
||||||
.size(16),
|
.size(16),
|
||||||
Space::with_width(Length::Fill),
|
Space::with_width(Length::Fill),
|
||||||
combo_box(
|
combo_box(
|
||||||
@@ -808,23 +937,23 @@ impl App {
|
|||||||
.input_style(
|
.input_style(
|
||||||
|theme: &Theme, _status| {
|
|theme: &Theme, _status| {
|
||||||
text_input::Style {
|
text_input::Style {
|
||||||
background: Background::Color(Color::TRANSPARENT),
|
background: Background::Color(theme.palette().primary.scale_alpha(0.2)),
|
||||||
border: Border {
|
border: Border {
|
||||||
width: 1.0,
|
width: 1.0,
|
||||||
color: theme.palette().text,
|
color: theme.palette().text.scale_alpha(0.3),
|
||||||
radius: Radius::from(8.0),
|
radius: Radius::from(4.0)
|
||||||
},
|
},
|
||||||
icon: Default::default(),
|
icon: Default::default(),
|
||||||
placeholder: theme.palette().text.scale_alpha(0.5),
|
placeholder: theme.palette().text,
|
||||||
value: theme.palette().text,
|
value: theme.palette().text,
|
||||||
selection: theme.palette().primary
|
selection: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.menu_style(
|
.menu_style(
|
||||||
|theme: &Theme| {
|
|theme: &Theme| {
|
||||||
menu::Style {
|
menu::Style {
|
||||||
background: Background::Color(Color::TRANSPARENT),
|
background: Background::Color(theme.palette().background),
|
||||||
border: Border {
|
border: Border {
|
||||||
width: 1.0,
|
width: 1.0,
|
||||||
color: theme.palette().text,
|
color: theme.palette().text,
|
||||||
@@ -836,9 +965,40 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.width(Length::from(350))
|
.padding(Padding{
|
||||||
|
top: 5.0,
|
||||||
|
bottom: 5.0,
|
||||||
|
left: 10.0,
|
||||||
|
right: 10.0,
|
||||||
|
})
|
||||||
|
.width(Length::from(200))
|
||||||
]
|
]
|
||||||
.align_y(Center)
|
.align_y(Center)
|
||||||
|
)
|
||||||
|
.padding(Padding{
|
||||||
|
top: 5.0,
|
||||||
|
bottom: 5.0,
|
||||||
|
left: 18.0,
|
||||||
|
right: 18.0,
|
||||||
|
})
|
||||||
|
.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().primary.scale_alpha(0.5);
|
||||||
|
style.border = border.rounded(16);
|
||||||
|
style
|
||||||
|
}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
.spacing(12);
|
||||||
|
|
||||||
|
container(
|
||||||
|
column![
|
||||||
|
appearance_settings_col,
|
||||||
|
Space::with_height(Length::from(20)),
|
||||||
|
tray_text_mode_toggle
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
.padding(20)
|
.padding(20)
|
||||||
@@ -862,7 +1022,6 @@ impl App {
|
|||||||
].into(),
|
].into(),
|
||||||
Space::with_width(Length::Fill).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) {
|
if !matches!(&self.pending_add_device, Some((_, addr)) if addr == &device.1) {
|
||||||
row_elements.push(
|
row_elements.push(
|
||||||
button(
|
button(
|
||||||
@@ -882,14 +1041,14 @@ impl App {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
.padding(8)
|
.padding(8)
|
||||||
.on_press(Message::StartAddDevice(device.0.clone(), device.1.clone()))
|
.on_press(Message::StartAddDevice(device.0.clone(), device.1))
|
||||||
.into()
|
.into()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
item_col = item_col.push(row(row_elements).align_y(Center));
|
item_col = item_col.push(row(row_elements).align_y(Center));
|
||||||
|
|
||||||
if let Some((_, pending_addr)) = &self.pending_add_device {
|
if let Some((_, pending_addr)) = &self.pending_add_device
|
||||||
if pending_addr == &device.1 {
|
&& pending_addr == &device.1 {
|
||||||
item_col = item_col.push(
|
item_col = item_col.push(
|
||||||
row![
|
row![
|
||||||
text("Device Type:").size(16),
|
text("Device Type:").size(16),
|
||||||
@@ -965,8 +1124,6 @@ impl App {
|
|||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
list_col = list_col.push(
|
list_col = list_col.push(
|
||||||
container(item_col)
|
container(item_col)
|
||||||
.padding(8)
|
.padding(8)
|
||||||
@@ -1023,9 +1180,7 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn wait_for_message(
|
async fn wait_for_message(ui_rx: Arc<Mutex<UnboundedReceiver<BluetoothUIMessage>>>) -> Message {
|
||||||
ui_rx: Arc<Mutex<UnboundedReceiver<BluetoothUIMessage>>>,
|
|
||||||
) -> Message {
|
|
||||||
let mut rx = ui_rx.lock().await;
|
let mut rx = ui_rx.lock().await;
|
||||||
match rx.recv().await {
|
match rx.recv().await {
|
||||||
Some(msg) => Message::BluetoothMessage(msg),
|
Some(msg) => Message::BluetoothMessage(msg),
|
||||||
@@ -1042,10 +1197,15 @@ async fn load_paired_devices() -> HashMap<String, Address> {
|
|||||||
let adapter = session.default_adapter().await.ok().unwrap();
|
let adapter = session.default_adapter().await.ok().unwrap();
|
||||||
let addresses = adapter.device_addresses().await.ok().unwrap();
|
let addresses = adapter.device_addresses().await.ok().unwrap();
|
||||||
for addr in addresses {
|
for addr in addresses {
|
||||||
let device = adapter.device(addr.clone()).ok().unwrap();
|
let device = adapter.device(addr).ok().unwrap();
|
||||||
let paired = device.is_paired().await.ok().unwrap();
|
let paired = device.is_paired().await.ok().unwrap();
|
||||||
if paired {
|
if paired {
|
||||||
let name = device.name().await.ok().flatten().unwrap_or_else(|| "Unknown".to_string());
|
let name = device
|
||||||
|
.name()
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.unwrap_or_else(|| "Unknown".to_string());
|
||||||
devices.insert(name, addr);
|
devices.insert(name, addr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
use aes::Aes128;
|
||||||
use aes::cipher::generic_array::GenericArray;
|
use aes::cipher::generic_array::GenericArray;
|
||||||
use aes::cipher::{BlockEncrypt, KeyInit};
|
use aes::cipher::{BlockEncrypt, KeyInit};
|
||||||
use aes::Aes128;
|
|
||||||
use iced::Theme;
|
use iced::Theme;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
@@ -8,19 +8,25 @@ use std::path::PathBuf;
|
|||||||
pub fn get_devices_path() -> PathBuf {
|
pub fn get_devices_path() -> PathBuf {
|
||||||
let data_dir = std::env::var("XDG_DATA_HOME")
|
let data_dir = std::env::var("XDG_DATA_HOME")
|
||||||
.unwrap_or_else(|_| format!("{}/.local/share", std::env::var("HOME").unwrap_or_default()));
|
.unwrap_or_else(|_| format!("{}/.local/share", std::env::var("HOME").unwrap_or_default()));
|
||||||
PathBuf::from(data_dir).join("librepods").join("devices.json")
|
PathBuf::from(data_dir)
|
||||||
|
.join("librepods")
|
||||||
|
.join("devices.json")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_preferences_path() -> PathBuf {
|
pub fn get_preferences_path() -> PathBuf {
|
||||||
let config_dir = std::env::var("XDG_CONFIG_HOME")
|
let config_dir = std::env::var("XDG_CONFIG_HOME")
|
||||||
.unwrap_or_else(|_| format!("{}/.local/share", std::env::var("HOME").unwrap_or_default()));
|
.unwrap_or_else(|_| format!("{}/.local/share", std::env::var("HOME").unwrap_or_default()));
|
||||||
PathBuf::from(config_dir).join("librepods").join("preferences.json")
|
PathBuf::from(config_dir)
|
||||||
|
.join("librepods")
|
||||||
|
.join("preferences.json")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_app_settings_path() -> PathBuf {
|
pub fn get_app_settings_path() -> PathBuf {
|
||||||
let config_dir = std::env::var("XDG_CONFIG_HOME")
|
let config_dir = std::env::var("XDG_CONFIG_HOME")
|
||||||
.unwrap_or_else(|_| format!("{}/.local/share", std::env::var("HOME").unwrap_or_default()));
|
.unwrap_or_else(|_| format!("{}/.local/share", std::env::var("HOME").unwrap_or_default()));
|
||||||
PathBuf::from(config_dir).join("librepods").join("app_settings.json")
|
PathBuf::from(config_dir)
|
||||||
|
.join("librepods")
|
||||||
|
.join("app_settings.json")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn e(key: &[u8; 16], data: &[u8; 16]) -> [u8; 16] {
|
fn e(key: &[u8; 16], data: &[u8; 16]) -> [u8; 16] {
|
||||||
|
|||||||
12
shell.nix
Normal file
12
shell.nix
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
(import (
|
||||||
|
let
|
||||||
|
lock = builtins.fromJSON (builtins.readFile ./flake.lock);
|
||||||
|
nodeName = lock.nodes.root.inputs.flake-compat;
|
||||||
|
in
|
||||||
|
fetchTarball {
|
||||||
|
url =
|
||||||
|
lock.nodes.${nodeName}.locked.url
|
||||||
|
or "https://github.com/edolstra/flake-compat/archive/${lock.nodes.${nodeName}.locked.rev}.tar.gz";
|
||||||
|
sha256 = lock.nodes.${nodeName}.locked.narHash;
|
||||||
|
}
|
||||||
|
) { src = ./.; }).shellNix
|
||||||
Reference in New Issue
Block a user