From af2068490d2e0c7c6402c0e98eea2a900800dbd5 Mon Sep 17 00:00:00 2001 From: Correl Date: Wed, 10 Feb 2021 18:51:36 -0500 Subject: [PATCH] Added graceful shutdown handling --- turntable/application.py | 10 ++++-- turntable/events.py | 4 +++ turntable/gui.py | 14 +++++--- turntable/hue.py | 74 ++++++++++++++++++++++++++++------------ turntable/icecast.py | 3 ++ turntable/turntable.py | 15 +++++++- 6 files changed, 90 insertions(+), 30 deletions(-) diff --git a/turntable/application.py b/turntable/application.py index 5364514..3a3d8ed 100644 --- a/turntable/application.py +++ b/turntable/application.py @@ -10,7 +10,7 @@ from typing import Any, Dict, Iterator, List, Optional from dejavu import Dejavu # type: ignore from turntable.audio import Listener, Player -from turntable.events import Event +from turntable.events import Event, Exit from turntable.hue import Hue from turntable.icecast import Icecast from turntable.models import PCM @@ -22,6 +22,7 @@ logger = logging.getLogger(__name__) class Application: def __init__(self, events: "Queue[Event]", pcm: "Optional[Queue[PCM]]" = None): + self.app_events: "Queue[Event]" = Queue() parser = argparse.ArgumentParser() parser.add_argument( "--config", default=os.path.expanduser("~/.config/turntable.json") @@ -98,6 +99,7 @@ class Application: turntable_config = self.config.get("turntable", dict()) turntable = Turntable( pcm_in, + self.app_events, event_queues, listener.framerate, listener.channels, @@ -128,7 +130,11 @@ class Application: process.start() def shutdown(self) -> None: - logging.info("Terminating") + logging.info("Telling processes to exit") + self.app_events.put(Exit()) for process in self.processes: + logging.debug("Waiting for %s to terminate", process) + process.join(3) if process.is_alive(): + logging.info("Killing process %s", process) process.kill() diff --git a/turntable/events.py b/turntable/events.py index 46e0ce6..a2e5b59 100644 --- a/turntable/events.py +++ b/turntable/events.py @@ -21,3 +21,7 @@ class StoppedPlaying(Event): @dataclass class NewMetadata(Event): title: str + + +class Exit(Event): + ... diff --git a/turntable/gui.py b/turntable/gui.py index a589739..ae059bd 100644 --- a/turntable/gui.py +++ b/turntable/gui.py @@ -174,14 +174,13 @@ def main(): app.run() clock = pygame.time.Clock() title = "" - while True: + stopping = False + while not stopping: for event in pygame.event.get(): if event.type == QUIT or ( event.type == KEYDOWN and event.key == K_ESCAPE ): - app.shutdown() - pygame.quit() - return + stopping = True try: while event := event_queue.get(False): ... @@ -191,9 +190,12 @@ def main(): title = "" elif isinstance(event, events.NewMetadata): title = event.title - + elif isinstance(event, events.Exit): + stopping = True except queue.Empty: ... + if stopping: + break try: while sample := pcm_in.get(False): plot.audio = sample @@ -212,4 +214,6 @@ def main(): except: logger.exception("Shutting down") finally: + logger.info("Stopping GUI") + pygame.quit() app.shutdown() diff --git a/turntable/hue.py b/turntable/hue.py index 04861ec..592c20b 100644 --- a/turntable/hue.py +++ b/turntable/hue.py @@ -57,22 +57,24 @@ class Hue(Process): self.username = username self.light = light self.light_id = None + self.light_state = dict() + self.active = False try: lights = hue_response( requests.get(f"http://{self.host}/api/{self.username}/lights") ) except HueError as error: - logger.warn(f"Error fetching lights: {error}") + logger.warn(f"Error fetching lights: %s", error) return try: - self.light_id = next( + self.light_id, self.light_state = next( filter( lambda i: i[1]["name"].lower() == self.light.lower(), lights.items() ) - )[0] + ) except StopIteration: - logger.warn(f"Could not find a light named '{light}") + logger.warn(f"Could not find a light named '%s'", light) return logger.info("Hue ready") @@ -83,33 +85,61 @@ class Hue(Process): logger.debug("Starting Hue") max_peak = 3000 audio = None - while True: + stopping = False + while not stopping: try: while event := self.events.get(False): - ... + if isinstance(event, StartedPlaying): + try: + self.light_state = hue_response( + requests.get( + f"http://{self.host}/api/{self.username}/lights/{self.light_id}" + ) + ) + logger.debug("Stored light state") + except HueError as e: + logger.warn(f"Error loading current light state: %s", e) + self.active = True + elif isinstance(event, StoppedPlaying): + self.active = False + original_brightness = self.light_state.get("state", {}).get( + "bri" + ) + if original_brightness is not None: + try: + hue_response( + requests.put( + f"http://{self.host}/api/{self.username}/lights/{self.light_id}/state", + json={"bri": original_brightness}, + ) + ) + logger.info( + "Restored %s to previous brightness", self.light + ) + except HueError as e: + logger.warn(f"Error restoring light brightness: %s", e) + elif isinstance(event, Exit): + stopping = True except queue.Empty: ... + if stopping: + break try: while sample := self.pcm_in.get(False): audio = sample except queue.Empty: ... - if not audio: - continue - rms = audioop.rms(audio.raw, audio.channels) - peak = audioop.max(audio.raw, audio.channels) - max_peak = max(peak, max_peak) - brightness = int(peak / max_peak * 255) - logger.debug(f"Brightness: {brightness}") + if audio and self.active: + rms = audioop.rms(audio.raw, audio.channels) + peak = audioop.max(audio.raw, audio.channels) + max_peak = max(peak, max_peak) + brightness = int(peak / max_peak * 255) + logger.debug(f"Brightness: {brightness}") - requests.put( - "http://192.168.1.199/api/bx1YKf6IQmU-W1MLHrsZ79Wz4bRWiBShb4ewBpfm/lights/7/state", - json={"bri": brightness, "transitiontime": 1}, - ) - - # requests.put( - # "http://192.168.1.199/api/bx1YKf6IQmU-W1MLHrsZ79Wz4bRWiBShb4ewBpfm/groups/2/action", - # json={"bri": brightness, "transitiontime": 1}, - # ) + requests.put( + f"http://{self.host}/api/{self.username}/lights/{self.light_id}/state", + json={"bri": brightness, "transitiontime": 1}, + ) time.sleep(0.1) + logger.info("Hue stopped") diff --git a/turntable/icecast.py b/turntable/icecast.py index 94d7690..266ade6 100644 --- a/turntable/icecast.py +++ b/turntable/icecast.py @@ -52,3 +52,6 @@ class Icecast(Process): self.set_title("") elif isinstance(event, NewMetadata): self.set_title(event.title) + elif isinstance(event, Exit): + break + logger.info("Icecast Updater stopped") diff --git a/turntable/turntable.py b/turntable/turntable.py index 5aa2da4..3e83151 100644 --- a/turntable/turntable.py +++ b/turntable/turntable.py @@ -4,6 +4,7 @@ import enum import logging from multiprocessing import Process, Queue from multiprocessing.connection import Connection +import queue import struct import time from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple @@ -53,6 +54,7 @@ class Turntable(Process): def __init__( self, pcm_in: "Queue[PCM]", + events_in: "Queue[Event]", events_out: "List[Queue[Event]]", framerate: int, channels: int, @@ -71,6 +73,7 @@ class Turntable(Process): self.buffer = PCM(framerate=framerate, channels=channels, maxlen=maxlen) self.recognizer = PCMRecognizer(dejavu) self.pcm_in = pcm_in + self.events_in = events_in self.events_out = events_out self.state: State = State.idle self.identified = False @@ -87,10 +90,20 @@ class Turntable(Process): def run(self) -> None: logger.debug("Starting Turntable") - while fragment := self.pcm_in.get(): + while True: + try: + event = self.events_in.get(block=False) + if isinstance(event, Exit): + self.publish(StoppedPlaying()) + self.publish(event) + break + except queue.Empty: + ... + fragment = self.pcm_in.get() self.buffer.append(fragment) maximum = audioop.max(fragment.raw, 2) self.update_audiolevel(maximum) + logger.info("Turntable stopped") def publish(self, event: Event) -> None: for queue in self.events_out: