From f8918e895ecdf657c34bec3d90a85ac3ee8a3c0a Mon Sep 17 00:00:00 2001 From: Balthasar Reuter Date: Fri, 13 Jul 2018 22:30:49 +0200 Subject: [PATCH] Threading module with Communicator added --- photobooth/StateMachine.py | 63 +++++++++++++++++++++++++++++-- photobooth/Threading.py | 59 +++++++++++++++++++++++++++++ photobooth/gui/GuiSkeleton.py | 5 ++- photobooth/gui/Qt5Gui/PyQt5Gui.py | 11 +++++- photobooth/main.py | 17 +++++++-- 5 files changed, 144 insertions(+), 11 deletions(-) create mode 100644 photobooth/Threading.py diff --git a/photobooth/StateMachine.py b/photobooth/StateMachine.py index 45a166c..0fef796 100644 --- a/photobooth/StateMachine.py +++ b/photobooth/StateMachine.py @@ -17,14 +17,15 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import logging + class Context: - def __init__(self, initial_state): + def __init__(self): super().__init__() - - self.state = initial_state + self.state = WelcomeState() @property def state(self): @@ -37,11 +38,17 @@ class Context: if not isinstance(new_state, State): raise TypeError('new_state must implement State') + logging.debug('New state is "{}"'.format(new_state)) + + self._state = new_state + def handleEvent(self, event): if not isinstance(event, Event): raise TypeError('event must implement Event') + logging.debug('Handling event "{}"'.format(event)) + if isinstance(event, ErrorEvent): self.state = ErrorState(event.exception, self.state) elif isinstance(event, TeardownEvent): @@ -109,7 +116,7 @@ class TeardownEvent(Event): def __init__(self, target): self._target = target - super().__init__('Teardown') + super().__init__('Teardown({})'.format(target)) @property def target(self): @@ -168,6 +175,10 @@ class ErrorState(State): self.old_state = old_state super().__init__() + def __str__(self): + + return 'ErrorState' + @property def old_state(self): @@ -198,6 +209,10 @@ class TeardownState(State): super().__init__() + def __str__(self): + + return 'TeardownState' + class WelcomeState(State): @@ -205,6 +220,10 @@ class WelcomeState(State): super().__init__() + def __str__(self): + + return 'WelcomeState' + def handleEvent(self, event, context): if isinstance(event, GuiEvent): @@ -226,6 +245,10 @@ class SettingsState(State): super().__init__() + def __str__(self): + + return 'SettingsState' + def handleEvent(self, event, context): if isinstance(event, GuiEvent) and event.name == 'welcome': @@ -240,6 +263,10 @@ class StartupState(State): super().__init__() + def __str__(self): + + return 'StartupState' + def handleEvent(self, event, context): if isinstance(event, CameraEvent) and event.name == 'ready': @@ -254,6 +281,10 @@ class IdleState(State): super().__init__() + def __str__(self): + + return 'IdleState' + def handleEvent(self, event, context): if ((isinstance(event, GuiEvent) or isinstance(event, GpioEvent)) and @@ -269,6 +300,10 @@ class GreeterState(State): super().__init__() + def __str__(self): + + return 'GreeterState' + def handleEvent(self, event, context): if ((isinstance(event, GuiEvent) or isinstance(event, GpioEvent)) and @@ -284,6 +319,10 @@ class CountdownState(State): super().__init__() + def __str__(self): + + return 'CountdownState' + def handleEvent(self, event, context): if isinstance(event, GuiEvent) and event.name == 'capture': @@ -298,6 +337,10 @@ class CaptureState(State): super().__init__() + def __str__(self): + + return 'CaptureState' + def handleEvent(self, event, context): if isinstance(event, CameraEvent) and event.name == 'next': @@ -314,6 +357,10 @@ class AssembleState(State): super().__init__() + def __str__(self): + + return 'AssembleState' + def handleEvent(self, event, context): if isinstance(event, CameraEvent) and event.name == 'review': @@ -328,6 +375,10 @@ class ReviewState(State): super().__init__() + def __str__(self): + + return 'ReviewState' + def handleEvent(self, event, context): if isinstance(event, GuiEvent) and event.name == 'postprocess': @@ -342,6 +393,10 @@ class PostprocessState(State): super().__init__() + def __str__(self): + + return 'PostprocessState' + def handleEvent(self, event, context): if ((isinstance(event, GuiEvent) or isinstance(event, GpioEvent)) and diff --git a/photobooth/Threading.py b/photobooth/Threading.py new file mode 100644 index 0000000..ec723da --- /dev/null +++ b/photobooth/Threading.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Photobooth - a flexible photo booth software +# Copyright (C) 2018 Balthasar Reuter +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from enum import IntEnum +from multiprocessing import Queue + + +class Communicator: + + def __init__(self): + + super().__init__() + + self._queues = [Queue() for _ in Workers] + + def send(self, target, message): + + if not isinstance(target, Workers): + raise TypeError('target must be a member of Workers') + + self._queues[target].put(message) + + def recv(self, worker, block=True): + + if not isinstance(worker, Workers): + raise TypeError('worker must be a member of Workers') + + return self._queues[worker].get(block) + + def iter(self, worker): + + if not isinstance(worker, Workers): + raise TypeError('worker must be a member of Workers') + + return iter(self._queues[worker].get, None) + + +class Workers(IntEnum): + MASTER = 0 + GUI = 1 + CAMERA = 2 + GPIO = 3 + WORKER = 4 diff --git a/photobooth/gui/GuiSkeleton.py b/photobooth/gui/GuiSkeleton.py index 862fc39..3e36ad4 100644 --- a/photobooth/gui/GuiSkeleton.py +++ b/photobooth/gui/GuiSkeleton.py @@ -19,12 +19,15 @@ from . import GuiState +from .. import StateMachine + class GuiSkeleton: - def __init__(self): + def __init__(self, communicator): super().__init__() + self._comm = communicator @property def idle(self): diff --git a/photobooth/gui/Qt5Gui/PyQt5Gui.py b/photobooth/gui/Qt5Gui/PyQt5Gui.py index e4ae2f9..29d42be 100644 --- a/photobooth/gui/Qt5Gui/PyQt5Gui.py +++ b/photobooth/gui/Qt5Gui/PyQt5Gui.py @@ -27,6 +27,9 @@ from PyQt5 import QtWidgets from PIL import ImageQt +from ... import StateMachine +from ...Threading import Workers + from .. import GuiState from ..GuiSkeleton import GuiSkeleton from ..GuiPostprocessor import GuiPostprocessor @@ -38,9 +41,9 @@ from . import Receiver class PyQt5Gui(GuiSkeleton): - def __init__(self, argv, config, camera_conn, worker_queue): + def __init__(self, argv, config, camera_conn, worker_queue, communicator): - super().__init__() + super().__init__(communicator) self._cfg = config self._conn = camera_conn @@ -181,6 +184,8 @@ class PyQt5Gui(GuiSkeleton): self.restart)) def _showSettings(self): + + self._comm.send(Workers.MASTER, StateMachine.GuiEvent('settings')) self._disableTrigger() self._disableEscape() @@ -190,6 +195,8 @@ class PyQt5Gui(GuiSkeleton): def _showStart(self, state): + self._comm.send(Workers.MASTER, StateMachine.GuiEvent('start')) + self._disableTrigger() self._enableEscape() self._lastHandle = self._showWelcomeScreen diff --git a/photobooth/main.py b/photobooth/main.py index f98cd63..1eb57d6 100644 --- a/photobooth/main.py +++ b/photobooth/main.py @@ -32,6 +32,8 @@ from . import camera, gui from .Config import Config from .Photobooth import Photobooth from .util import lookup_and_import +from .StateMachine import Context +from .Threading import Communicator, Workers from .Worker import Worker @@ -99,7 +101,7 @@ class WorkerProcess(mp.Process): class GuiProcess(mp.Process): - def __init__(self, argv, config, conn, queue): + def __init__(self, argv, config, conn, queue, communicator): super().__init__() @@ -107,12 +109,13 @@ class GuiProcess(mp.Process): self.cfg = config self.conn = conn self.queue = queue + self.comm = communicator def run(self): Gui = lookup_and_import(gui.modules, self.cfg.get('Gui', 'module'), 'gui') - sys.exit(Gui(self.argv, self.cfg, self.conn, self.queue).run()) + sys.exit(Gui(self.argv, self.cfg, self.conn, self.queue, self.comm).run()) def run(argv): @@ -122,6 +125,9 @@ def run(argv): # Load configuration config = Config('photobooth.cfg') + comm = Communicator() + context = Context() + # Create communication objects: # 1. We use a pipe to connect GUI and camera process # 2. We use a queue to feed tasks to the postprocessing process @@ -138,9 +144,12 @@ def run(argv): worker_proc = WorkerProcess(config, worker_queue) worker_proc.start() - gui_proc = GuiProcess(argv, config, gui_conn, worker_queue) + gui_proc = GuiProcess(argv, config, gui_conn, worker_queue, comm) gui_proc.start() + for event in comm.iter(Workers.MASTER): + context.handleEvent(event) + # Close endpoints gui_conn.close() camera_conn.close() @@ -156,7 +165,7 @@ def run(argv): def main(argv): # Setup log level and format - log_level = logging.INFO + log_level = logging.DEBUG formatter = logging.Formatter( '%(asctime)s - %(name)s - %(levelname)s - %(message)s')