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 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()

View file

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

View file

@ -174,14 +174,13 @@ def main():
app.run()
clock = pygame.time.Clock()
title = "<Idle>"
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 = "<Idle>"
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()

View file

@ -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")

View file

@ -52,3 +52,6 @@ class Icecast(Process):
self.set_title("<Idle>")
elif isinstance(event, NewMetadata):
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
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: