From f6334f0b9b0db68ca33c8917a18ceb1e1f4211d4 Mon Sep 17 00:00:00 2001 From: Kavish Devar Date: Fri, 27 Sep 2024 16:18:59 +0530 Subject: [PATCH] add example daemon read/write programs --- README.md | 31 ++++++++++++++ airpods_daemon.py | 94 +++++++++++++++++++++++++++++++----------- example_deamon_read.py | 45 ++++++++++++++++++++ main.py | 4 -- 4 files changed, 145 insertions(+), 29 deletions(-) create mode 100644 example_deamon_read.py diff --git a/README.md b/README.md index ccab9fb..83ec425 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,12 @@ sudo apt install python3 python3-pip pip3 install pybluez ``` +If you want to run it as a daemon (Refer to the [Daemonizing](#Daemonizing) section), you will need to install the `python-daemon` package. + +```bash +pip3 install python-daemon +``` + ## 2. Clone the repository ```bash @@ -36,3 +42,28 @@ python3 main.py ```bash python3 standalone.py ``` + +## Daemonizing +If you want to run this as a deamon, you can use the `airpods_daemon.py` script. This creates a standard UNIX socket at `/tmp/airpods_daemon.sock` and listens for commands (and sends battery/in-ear info, soon!). + +You can run it as follows: + +```bash +python3 airpods_daemon.py +``` + +### Sending data to the daemon +You can send data to the daemon using the `send_data.py` script. Since it's a standard UNIX socket, you can send data to it using any programming language that supports UNIX sockets. + +This package includes a demo script that sends a command to turn off the ANC. You can run it as follows: + +```bash +python3 example_daemon_send.py +``` + +### Reading data from the daemon +Youhcan listen to the daemon's output by running the `example_daemon_read.py` script. This script listens to the UNIX socket and prints the data it receives. Currenty, it only prints the battery percentage and the in-ear status. + +```bash +python3 example_daemon_read.py +``` \ No newline at end of file diff --git a/airpods_daemon.py b/airpods_daemon.py index a870932..b186142 100644 --- a/airpods_daemon.py +++ b/airpods_daemon.py @@ -6,6 +6,9 @@ import logging from aln import Connection, enums from aln.Notifications import Notifications import os +import pickle + +connection = None AIRPODS_MAC = '28:2D:7F:C2:05:5B' SOCKET_PATH = '/tmp/airpods_daemon.sock' @@ -17,33 +20,70 @@ running = True # Configure logging to write to a file logging.basicConfig(filename=LOG_FILE, level=logging.DEBUG, format='%(asctime)s %(levelname)s : %(message)s') +# logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(levelname)s : %(message)s') def handle_client(connection, client_socket): - """Handle client requests by forwarding all received data to aln.Connection.""" - while running: - try: - data = client_socket.recv(1024) # Receive data in bytes - if not data: - break # Client disconnected + """Handle client requests by forwarding all received data to aln.Connection, send data back to the client.""" - # Forward the raw data to aln.Connection - connection.send(data) - logging.info(f'Forwarded data: {data}') + def send_status(): + while running: + try: + data = globals().get("battery") + if data: + if not client_socket or not isinstance(client_socket, socket.socket): + logging.error("Invalid client socket") + break + logging.info(f'Sending battery status: {data}') + client_socket.send(pickle.dumps(data)) + logging.info(f'Sent battery status: {data}') + globals()["battery"] = None + + data = globals().get("earDetection") + if data: + if not client_socket or not isinstance(client_socket, socket.socket): + logging.error("Invalid client socket") + break + logging.info(f'Sending ear detection status: {data}') + client_socket.send(pickle.dumps(data)) + logging.info(f'Sent ear detection status: {data}') + globals()["earDetection"] = None + except socket.error as e: + logging.error(f"Socket error sending status: {e}") + break + except Exception as e: + logging.error(f"Error sending status: {e}") + break - except Exception as e: - logging.error(f"Error handling client: {e}") - break + def receive_commands(): + while running: + try: + data = client_socket.recv(1024) + if not data: + break + logging.info(f'Received command: {data}') + connection.send(data) + except Exception as e: + logging.error(f"Error receiving command: {e}") + break + + # Start two threads to handle sending and receiving data + send_thread = threading.Thread(target=send_status) + send_thread.start() + receive_thread = threading.Thread(target=receive_commands) + receive_thread.start() + + send_thread.join() + receive_thread.join() client_socket.close() + logging.info("Client socket closed") def start_socket_server(connection): """Start a UNIX domain socket server.""" global running - + # Set up the socket server_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - - # Bind the socket to the path try: server_socket.bind(SOCKET_PATH) except OSError: @@ -88,27 +128,27 @@ def stop_daemon(signum, frame): def notification_handler(notification_type: int): global connection + logging.debug(f"Received notification: {notification_type}") if notification_type == Notifications.BATTERY_UPDATED: logger = logging.getLogger("Battery Status") - for i in connection.notificationListener.BatteryNotification.getBattery(): + battery = connection.notificationListener.BatteryNotification.getBattery() + globals()["battery"] = battery + for i in battery: logger.debug(f'{i.get_component()} - {i.get_status()}: {i.get_level()}') elif notification_type == Notifications.EAR_DETECTION_UPDATED: logger = logging.getLogger("In-Ear Status") - logger.debug(f'{connection.notificationListener.EarDetectionNotification.getEarDetection()}') + earDetection = connection.notificationListener.EarDetectionNotification.getEarDetection() + globals()["earDetection"] = earDetection + logger.debug(earDetection) def main(): global running - - # Set up signal handlers to handle termination signals - signal.signal(signal.SIGINT, stop_daemon) # Handle Ctrl+C - signal.signal(signal.SIGTERM, stop_daemon) # Handle kill signal - logging.info("Starting AirPods daemon") - - # Initialize the connection to the AirPods - global connection connection = Connection(AIRPODS_MAC) + globals()['connection'] = connection + + # Connect to the AirPods and send the handshake connection.connect() connection.send(enums.HANDSHAKE) logging.info("Handshake sent") @@ -117,6 +157,10 @@ def main(): # Start the socket server to listen for client connections start_socket_server(connection) + # Set up signal handlers to handle termination signals + signal.signal(signal.SIGINT, stop_daemon) # Handle Ctrl+C + signal.signal(signal.SIGTERM, stop_daemon) # Handle kill signal + if __name__ == "__main__": # Daemonize the process if os.fork(): diff --git a/example_deamon_read.py b/example_deamon_read.py new file mode 100644 index 0000000..d7bfe38 --- /dev/null +++ b/example_deamon_read.py @@ -0,0 +1,45 @@ +import socket +import pickle +from aln.Notifications import Battery + +SOCKET_PATH = "/tmp/airpods_daemon.sock" + +def read(): + """Send a command to the daemon via UNIX domain socket.""" + client_socket = None + try: + # Create a socket connection to the daemon + client_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + print("Connecting to daemon...") + client_socket.connect(SOCKET_PATH) + + # Receive data + while True: + d = client_socket.recv(1024) + if d: + try: + data = pickle.loads(d) + if isinstance(data, str): + print(f"Received data: {data}") + elif isinstance(data, list) and all(isinstance(b, Battery.Battery) for b in data): + for b in data: + print(f"Received battery status: {b.get_component()} is {b.get_status()} at {b.get_level()}%") + elif isinstance(data, list) and len(data) == 2 and all(isinstance(i, int) for i in data): + print(f"Received ear detection status: Is in-ear? Primary: {data[0] == 0}, Secondary: {data[1] == 0}") + else: + print(f"Received unknown data: {data}") + all(isinstance(b, Battery.Battery) for b in data) + except pickle.UnpicklingError as e: + print(f"Error deserializing data: {e}") + else: + break + + except Exception as e: + print(f"Error communicating with daemon: {e}") + finally: + if client_socket: + client_socket.close() + print("Socket closed") + +if __name__ == "__main__": + read() \ No newline at end of file diff --git a/main.py b/main.py index 4dbd9bc..e568752 100644 --- a/main.py +++ b/main.py @@ -70,10 +70,6 @@ def main(): # Set up logging handler = ConsoleHandler() - log_format = ( - "%(asctime)s - %(levelname)s - %(name)s - %(message)s" - ) - logging.addLevelName(logging.DEBUG, "\033[1;34m%s\033[1;0m" % logging.getLevelName(logging.DEBUG)) logging.addLevelName(logging.INFO, "\033[1;32m%s\033[1;0m" % logging.getLevelName(logging.INFO)) logging.addLevelName(logging.WARNING, "\033[1;33m%s\033[1;0m" % logging.getLevelName(logging.WARNING))