mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-03-19 04:31:27 +00:00
add ear detection (media control) to tray app
This commit is contained in:
16
README.md
16
README.md
@@ -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
|
|||||||
|
|
||||||

|

|
||||||

|

|
||||||
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
|
||||||
@@ -94,4 +96,4 @@ python3 examples/daemon/tray.py
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 examples/standalone.py
|
python3 examples/standalone.py
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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}")
|
||||||
|
|||||||
Reference in New Issue
Block a user