Added graceful shutdown handling

This commit is contained in:
Correl Roush 2021-02-10 18:51:36 -05:00
parent a7541084fa
commit af2068490d
6 changed files with 90 additions and 30 deletions

View file

@ -10,7 +10,7 @@ from typing import Any, Dict, Iterator, List, Optional
from dejavu import Dejavu # type: ignore from dejavu import Dejavu # type: ignore
from turntable.audio import Listener, Player from turntable.audio import Listener, Player
from turntable.events import Event from turntable.events import Event, Exit
from turntable.hue import Hue from turntable.hue import Hue
from turntable.icecast import Icecast from turntable.icecast import Icecast
from turntable.models import PCM from turntable.models import PCM
@ -22,6 +22,7 @@ logger = logging.getLogger(__name__)
class Application: class Application:
def __init__(self, events: "Queue[Event]", pcm: "Optional[Queue[PCM]]" = None): def __init__(self, events: "Queue[Event]", pcm: "Optional[Queue[PCM]]" = None):
self.app_events: "Queue[Event]" = Queue()
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument( parser.add_argument(
"--config", default=os.path.expanduser("~/.config/turntable.json") "--config", default=os.path.expanduser("~/.config/turntable.json")
@ -98,6 +99,7 @@ class Application:
turntable_config = self.config.get("turntable", dict()) turntable_config = self.config.get("turntable", dict())
turntable = Turntable( turntable = Turntable(
pcm_in, pcm_in,
self.app_events,
event_queues, event_queues,
listener.framerate, listener.framerate,
listener.channels, listener.channels,
@ -128,7 +130,11 @@ class Application:
process.start() process.start()
def shutdown(self) -> None: def shutdown(self) -> None:
logging.info("Terminating") logging.info("Telling processes to exit")
self.app_events.put(Exit())
for process in self.processes: for process in self.processes:
logging.debug("Waiting for %s to terminate", process)
process.join(3)
if process.is_alive(): if process.is_alive():
logging.info("Killing process %s", process)
process.kill() process.kill()

View file

@ -21,3 +21,7 @@ class StoppedPlaying(Event):
@dataclass @dataclass
class NewMetadata(Event): class NewMetadata(Event):
title: str title: str
class Exit(Event):
...

View file

@ -174,14 +174,13 @@ def main():
app.run() app.run()
clock = pygame.time.Clock() clock = pygame.time.Clock()
title = "<Idle>" title = "<Idle>"
while True: stopping = False
while not stopping:
for event in pygame.event.get(): for event in pygame.event.get():
if event.type == QUIT or ( if event.type == QUIT or (
event.type == KEYDOWN and event.key == K_ESCAPE event.type == KEYDOWN and event.key == K_ESCAPE
): ):
app.shutdown() stopping = True
pygame.quit()
return
try: try:
while event := event_queue.get(False): while event := event_queue.get(False):
... ...
@ -191,9 +190,12 @@ def main():
title = "<Idle>" title = "<Idle>"
elif isinstance(event, events.NewMetadata): elif isinstance(event, events.NewMetadata):
title = event.title title = event.title
elif isinstance(event, events.Exit):
stopping = True
except queue.Empty: except queue.Empty:
... ...
if stopping:
break
try: try:
while sample := pcm_in.get(False): while sample := pcm_in.get(False):
plot.audio = sample plot.audio = sample
@ -212,4 +214,6 @@ def main():
except: except:
logger.exception("Shutting down") logger.exception("Shutting down")
finally: finally:
logger.info("Stopping GUI")
pygame.quit()
app.shutdown() app.shutdown()

View file

@ -57,22 +57,24 @@ class Hue(Process):
self.username = username self.username = username
self.light = light self.light = light
self.light_id = None self.light_id = None
self.light_state = dict()
self.active = False
try: try:
lights = hue_response( lights = hue_response(
requests.get(f"http://{self.host}/api/{self.username}/lights") requests.get(f"http://{self.host}/api/{self.username}/lights")
) )
except HueError as error: except HueError as error:
logger.warn(f"Error fetching lights: {error}") logger.warn(f"Error fetching lights: %s", error)
return return
try: try:
self.light_id = next( self.light_id, self.light_state = next(
filter( filter(
lambda i: i[1]["name"].lower() == self.light.lower(), lights.items() lambda i: i[1]["name"].lower() == self.light.lower(), lights.items()
) )
)[0] )
except StopIteration: except StopIteration:
logger.warn(f"Could not find a light named '{light}") logger.warn(f"Could not find a light named '%s'", light)
return return
logger.info("Hue ready") logger.info("Hue ready")
@ -83,33 +85,61 @@ class Hue(Process):
logger.debug("Starting Hue") logger.debug("Starting Hue")
max_peak = 3000 max_peak = 3000
audio = None audio = None
while True: stopping = False
while not stopping:
try: try:
while event := self.events.get(False): 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: except queue.Empty:
... ...
if stopping:
break
try: try:
while sample := self.pcm_in.get(False): while sample := self.pcm_in.get(False):
audio = sample audio = sample
except queue.Empty: except queue.Empty:
... ...
if not audio: if audio and self.active:
continue rms = audioop.rms(audio.raw, audio.channels)
rms = audioop.rms(audio.raw, audio.channels) peak = audioop.max(audio.raw, audio.channels)
peak = audioop.max(audio.raw, audio.channels) max_peak = max(peak, max_peak)
max_peak = max(peak, max_peak) brightness = int(peak / max_peak * 255)
brightness = int(peak / max_peak * 255) logger.debug(f"Brightness: {brightness}")
logger.debug(f"Brightness: {brightness}")
requests.put( requests.put(
"http://192.168.1.199/api/bx1YKf6IQmU-W1MLHrsZ79Wz4bRWiBShb4ewBpfm/lights/7/state", f"http://{self.host}/api/{self.username}/lights/{self.light_id}/state",
json={"bri": brightness, "transitiontime": 1}, json={"bri": brightness, "transitiontime": 1},
) )
# requests.put(
# "http://192.168.1.199/api/bx1YKf6IQmU-W1MLHrsZ79Wz4bRWiBShb4ewBpfm/groups/2/action",
# json={"bri": brightness, "transitiontime": 1},
# )
time.sleep(0.1) time.sleep(0.1)
logger.info("Hue stopped")

View file

@ -52,3 +52,6 @@ class Icecast(Process):
self.set_title("<Idle>") self.set_title("<Idle>")
elif isinstance(event, NewMetadata): elif isinstance(event, NewMetadata):
self.set_title(event.title) self.set_title(event.title)
elif isinstance(event, Exit):
break
logger.info("Icecast Updater stopped")

View file

@ -4,6 +4,7 @@ import enum
import logging import logging
from multiprocessing import Process, Queue from multiprocessing import Process, Queue
from multiprocessing.connection import Connection from multiprocessing.connection import Connection
import queue
import struct import struct
import time import time
from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple
@ -53,6 +54,7 @@ class Turntable(Process):
def __init__( def __init__(
self, self,
pcm_in: "Queue[PCM]", pcm_in: "Queue[PCM]",
events_in: "Queue[Event]",
events_out: "List[Queue[Event]]", events_out: "List[Queue[Event]]",
framerate: int, framerate: int,
channels: int, channels: int,
@ -71,6 +73,7 @@ class Turntable(Process):
self.buffer = PCM(framerate=framerate, channels=channels, maxlen=maxlen) self.buffer = PCM(framerate=framerate, channels=channels, maxlen=maxlen)
self.recognizer = PCMRecognizer(dejavu) self.recognizer = PCMRecognizer(dejavu)
self.pcm_in = pcm_in self.pcm_in = pcm_in
self.events_in = events_in
self.events_out = events_out self.events_out = events_out
self.state: State = State.idle self.state: State = State.idle
self.identified = False self.identified = False
@ -87,10 +90,20 @@ class Turntable(Process):
def run(self) -> None: def run(self) -> None:
logger.debug("Starting Turntable") 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) self.buffer.append(fragment)
maximum = audioop.max(fragment.raw, 2) maximum = audioop.max(fragment.raw, 2)
self.update_audiolevel(maximum) self.update_audiolevel(maximum)
logger.info("Turntable stopped")
def publish(self, event: Event) -> None: def publish(self, event: Event) -> None:
for queue in self.events_out: for queue in self.events_out: