add ear detection (media control) to tray app

This commit is contained in:
Kavish Devar
2024-09-29 21:10:26 +05:30
parent 6f6b28b027
commit 028d83b2a4
2 changed files with 109 additions and 10 deletions

View File

@@ -1,7 +1,9 @@
# ALN - AirPods like Normal (Linux Only) # ALN - AirPods like Normal (Linux Only)
# Get Started!
## 1. Install the required packages ## Currently supported device(s)
- AirPods Pro 2
### 1. Install the required packages
```bash ```bash
sudo apt install python3 python3-pip sudo apt install python3 python3-pip
@@ -14,16 +16,16 @@ If you want to run it as a daemon (Refer to the [Daemon Version](#as-a-daemon-us
pip3 install python-daemon pip3 install python-daemon
``` ```
## 2. Clone the repository ### 2. Clone the repository
```bash ```bash
git clone https://github.com/kavishdevar/aln.git git clone https://github.com/kavishdevar/aln.git
cd aln cd aln
``` ```
## 3. Preprare ### 3. Preprare
Pair your AirPods with your machine before running this script! Pair your AirPods with your machine before running this script!
:warning: **Note:** DO NOT FORGET TO EDIT THE `AIRPODS_MAC` VARIABLE IN `main.py`/`standalone.py` WITH YOUR AIRPODS MAC ADDRESS! > **Note:** DO NOT FORGET TO EDIT THE `AIRPODS_MAC` VARIABLE IN EXAMPLE SCRIPTS WITH YOUR AIRPODS MAC ADDRESS BEFORE RUNNING THEM!
# Versions # Versions
@@ -79,7 +81,7 @@ python3 examples/daemon/ear-detection.py
![Tray Icon Hover Screenshot](imgs/tray-icon-hover.png) ![Tray Icon Hover Screenshot](imgs/tray-icon-hover.png)
![Tray Icon Menu Screenshot](imgs/tray-icon-menu.png) ![Tray Icon Menu Screenshot](imgs/tray-icon-menu.png)
This script is a simple tray icon that shows the battery percentage and set ANC modes. This script is a simple tray icon that shows the battery percentage and set ANC modes. It can also control the media with the in-ear status.
> Note: This script uses QT. > Note: This script uses QT.
```bash ```bash

View File

@@ -7,6 +7,9 @@ from PyQt5.QtWidgets import QApplication, QSystemTrayIcon, QMenu, QAction, QMess
from PyQt5.QtGui import QIcon from PyQt5.QtGui import QIcon
from PyQt5.QtCore import QObject, pyqtSignal from PyQt5.QtCore import QObject, pyqtSignal
import logging import logging
import subprocess
import time
import os
SOCKET_PATH = "/tmp/airpods_daemon.sock" SOCKET_PATH = "/tmp/airpods_daemon.sock"
@@ -20,9 +23,104 @@ battery_status = {
# Define a lock # Define a lock
battery_status_lock = threading.Lock() battery_status_lock = threading.Lock()
class MediaController:
def __init__(self):
self.earStatus = "Both out"
self.wasMusicPlayingInSingle = False
self.wasMusicPlayingInBoth = False
self.firstEarOutTime = 0
self.stop_thread_event = threading.Event()
def playMusic(self):
logging.info("Playing music")
subprocess.call(("playerctl", "play", "--ignore-player", "OnePlus_7"))
def pauseMusic(self):
logging.info("Pausing music")
subprocess.call(("playerctl", "pause", "--ignore-player", "OnePlus_7"))
def isPlaying(self):
return subprocess.check_output(["playerctl", "status", "--player", "spotify"]).decode("utf-8").strip() == "Playing"
def handlePlayPause(self, data):
primary_status = data[0]
secondary_status = data[1]
logging.debug(f"Handling play/pause with data: {data}, previousStatus: {self.earStatus}, wasMusicPlaying: {self.wasMusicPlayingInSingle or self.wasMusicPlayingInBoth}")
def delayed_action(s):
if not self.stop_thread_event.is_set():
if self.wasMusicPlayingInSingle:
self.playMusic()
self.wasMusicPlayingInBoth = False
elif self.wasMusicPlayingInBoth or s:
self.wasMusicPlayingInBoth = True
self.wasMusicPlayingInSingle = False
if primary_status and secondary_status:
if self.earStatus != "Both out":
s = self.isPlaying()
if s:
self.pauseMusic()
os.system("pactl set-card-profile bluez_card.28_2D_7F_C2_05_5B off")
logging.info("Setting profile to off")
if self.earStatus == "Only one in":
if self.firstEarOutTime != 0 and time.time() - self.firstEarOutTime < 0.3:
self.wasMusicPlayingInSingle = True
self.wasMusicPlayingInBoth = True
self.stop_thread_event.set()
else:
if s:
self.wasMusicPlayingInSingle = True
else:
self.wasMusicPlayingInSingle = False
elif self.earStatus == "Both in":
s = self.isPlaying()
if s:
self.wasMusicPlayingInBoth = True
self.wasMusicPlayingInSingle = False
else:
self.wasMusicPlayingInSingle = False
self.earStatus = "Both out"
return "Both out"
elif not primary_status and not secondary_status:
if self.earStatus != "Both in":
if self.earStatus == "Both out":
os.system("pactl set-card-profile bluez_card.28_2D_7F_C2_05_5B a2dp-sink")
logging.info("Setting profile to a2dp-sink")
elif self.earStatus == "Only one in":
self.stop_thread_event.set()
s = self.isPlaying()
if s:
self.wasMusicPlayingInBoth = True
if self.wasMusicPlayingInSingle or self.wasMusicPlayingInBoth:
self.playMusic()
self.wasMusicPlayingInBoth = True
self.wasMusicPlayingInSingle = False
self.earStatus = "Both in"
return "Both in"
elif (primary_status and not secondary_status) or (not primary_status and secondary_status):
if self.earStatus != "Only one in":
self.stop_thread_event.clear()
s = self.isPlaying()
if s:
self.pauseMusic()
delayed_thread = threading.Timer(0.3, delayed_action, args=[s])
delayed_thread.start()
self.firstEarOutTime = time.time()
if self.earStatus == "Both out":
os.system("pactl set-card-profile bluez_card.28_2D_7F_C2_05_5B a2dp-sink")
logging.info("Setting profile to a2dp-sink")
self.earStatus = "Only one in"
return "Only one in"
class BatteryStatusUpdater(QObject): class BatteryStatusUpdater(QObject):
battery_status_updated = pyqtSignal() battery_status_updated = pyqtSignal()
def __init__(self):
super().__init__()
self.media_controller = MediaController()
def listen_for_battery_updates(self): def listen_for_battery_updates(self):
global battery_status global battery_status
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client: with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client:
@@ -33,10 +131,11 @@ class BatteryStatusUpdater(QObject):
try: try:
response = json.loads(data.decode('utf-8')) response = json.loads(data.decode('utf-8'))
if response["type"] == "battery": if response["type"] == "battery":
print(response)
with battery_status_lock: with battery_status_lock:
battery_status = response battery_status = response
self.battery_status_updated.emit() self.battery_status_updated.emit()
elif response["type"] == "ear_detection":
self.media_controller.handlePlayPause([response['primary'], response['secondary']])
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
logging.warning(f"Error deserializing data: {e}") logging.warning(f"Error deserializing data: {e}")
except KeyError as e: except KeyError as e:
@@ -51,7 +150,6 @@ def get_battery_status():
case = battery_status["CASE"] case = battery_status["CASE"]
return f"Left: {left['level']}% - {left['status'].title().replace('_', ' ')} | Right: {right['level']}% - {right['status'].title().replace('_', ' ')} | Case: {case['level']}% - {case['status'].title().replace('_', ' ')}" return f"Left: {left['level']}% - {left['status'].title().replace('_', ' ')} | Right: {right['level']}% - {right['status'].title().replace('_', ' ')} | Case: {case['level']}% - {case['status'].title().replace('_', ' ')}"
from aln import enums from aln import enums
def set_anc_mode(mode): def set_anc_mode(mode):
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client: with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client:
@@ -69,7 +167,6 @@ def set_anc_mode(mode):
response = client.recv(1024) response = client.recv(1024)
return json.loads(response.decode()) return json.loads(response.decode())
def control_anc(action): def control_anc(action):
response = set_anc_mode(action) response = set_anc_mode(action)
logging.info(f"ANC action: {action}, Response: {response}") logging.info(f"ANC action: {action}, Response: {response}")