Threading module with Communicator added

This commit is contained in:
Balthasar Reuter
2018-07-13 22:30:49 +02:00
parent 2925b3ecb0
commit f8918e895e
5 changed files with 144 additions and 11 deletions

View File

@@ -17,14 +17,15 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
class Context: class Context:
def __init__(self, initial_state): def __init__(self):
super().__init__() super().__init__()
self.state = WelcomeState()
self.state = initial_state
@property @property
def state(self): def state(self):
@@ -37,11 +38,17 @@ class Context:
if not isinstance(new_state, State): if not isinstance(new_state, State):
raise TypeError('new_state must implement State') raise TypeError('new_state must implement State')
logging.debug('New state is "{}"'.format(new_state))
self._state = new_state
def handleEvent(self, event): def handleEvent(self, event):
if not isinstance(event, Event): if not isinstance(event, Event):
raise TypeError('event must implement Event') raise TypeError('event must implement Event')
logging.debug('Handling event "{}"'.format(event))
if isinstance(event, ErrorEvent): if isinstance(event, ErrorEvent):
self.state = ErrorState(event.exception, self.state) self.state = ErrorState(event.exception, self.state)
elif isinstance(event, TeardownEvent): elif isinstance(event, TeardownEvent):
@@ -109,7 +116,7 @@ class TeardownEvent(Event):
def __init__(self, target): def __init__(self, target):
self._target = target self._target = target
super().__init__('Teardown') super().__init__('Teardown({})'.format(target))
@property @property
def target(self): def target(self):
@@ -168,6 +175,10 @@ class ErrorState(State):
self.old_state = old_state self.old_state = old_state
super().__init__() super().__init__()
def __str__(self):
return 'ErrorState'
@property @property
def old_state(self): def old_state(self):
@@ -198,6 +209,10 @@ class TeardownState(State):
super().__init__() super().__init__()
def __str__(self):
return 'TeardownState'
class WelcomeState(State): class WelcomeState(State):
@@ -205,6 +220,10 @@ class WelcomeState(State):
super().__init__() super().__init__()
def __str__(self):
return 'WelcomeState'
def handleEvent(self, event, context): def handleEvent(self, event, context):
if isinstance(event, GuiEvent): if isinstance(event, GuiEvent):
@@ -226,6 +245,10 @@ class SettingsState(State):
super().__init__() super().__init__()
def __str__(self):
return 'SettingsState'
def handleEvent(self, event, context): def handleEvent(self, event, context):
if isinstance(event, GuiEvent) and event.name == 'welcome': if isinstance(event, GuiEvent) and event.name == 'welcome':
@@ -240,6 +263,10 @@ class StartupState(State):
super().__init__() super().__init__()
def __str__(self):
return 'StartupState'
def handleEvent(self, event, context): def handleEvent(self, event, context):
if isinstance(event, CameraEvent) and event.name == 'ready': if isinstance(event, CameraEvent) and event.name == 'ready':
@@ -254,6 +281,10 @@ class IdleState(State):
super().__init__() super().__init__()
def __str__(self):
return 'IdleState'
def handleEvent(self, event, context): def handleEvent(self, event, context):
if ((isinstance(event, GuiEvent) or isinstance(event, GpioEvent)) and if ((isinstance(event, GuiEvent) or isinstance(event, GpioEvent)) and
@@ -269,6 +300,10 @@ class GreeterState(State):
super().__init__() super().__init__()
def __str__(self):
return 'GreeterState'
def handleEvent(self, event, context): def handleEvent(self, event, context):
if ((isinstance(event, GuiEvent) or isinstance(event, GpioEvent)) and if ((isinstance(event, GuiEvent) or isinstance(event, GpioEvent)) and
@@ -284,6 +319,10 @@ class CountdownState(State):
super().__init__() super().__init__()
def __str__(self):
return 'CountdownState'
def handleEvent(self, event, context): def handleEvent(self, event, context):
if isinstance(event, GuiEvent) and event.name == 'capture': if isinstance(event, GuiEvent) and event.name == 'capture':
@@ -298,6 +337,10 @@ class CaptureState(State):
super().__init__() super().__init__()
def __str__(self):
return 'CaptureState'
def handleEvent(self, event, context): def handleEvent(self, event, context):
if isinstance(event, CameraEvent) and event.name == 'next': if isinstance(event, CameraEvent) and event.name == 'next':
@@ -314,6 +357,10 @@ class AssembleState(State):
super().__init__() super().__init__()
def __str__(self):
return 'AssembleState'
def handleEvent(self, event, context): def handleEvent(self, event, context):
if isinstance(event, CameraEvent) and event.name == 'review': if isinstance(event, CameraEvent) and event.name == 'review':
@@ -328,6 +375,10 @@ class ReviewState(State):
super().__init__() super().__init__()
def __str__(self):
return 'ReviewState'
def handleEvent(self, event, context): def handleEvent(self, event, context):
if isinstance(event, GuiEvent) and event.name == 'postprocess': if isinstance(event, GuiEvent) and event.name == 'postprocess':
@@ -342,6 +393,10 @@ class PostprocessState(State):
super().__init__() super().__init__()
def __str__(self):
return 'PostprocessState'
def handleEvent(self, event, context): def handleEvent(self, event, context):
if ((isinstance(event, GuiEvent) or isinstance(event, GpioEvent)) and if ((isinstance(event, GuiEvent) or isinstance(event, GpioEvent)) and

59
photobooth/Threading.py Normal file
View File

@@ -0,0 +1,59 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Photobooth - a flexible photo booth software
# Copyright (C) 2018 Balthasar Reuter <photobooth at re - web dot eu>
#
# 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 <http://www.gnu.org/licenses/>.
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

View File

@@ -19,12 +19,15 @@
from . import GuiState from . import GuiState
from .. import StateMachine
class GuiSkeleton: class GuiSkeleton:
def __init__(self): def __init__(self, communicator):
super().__init__() super().__init__()
self._comm = communicator
@property @property
def idle(self): def idle(self):

View File

@@ -27,6 +27,9 @@ from PyQt5 import QtWidgets
from PIL import ImageQt from PIL import ImageQt
from ... import StateMachine
from ...Threading import Workers
from .. import GuiState from .. import GuiState
from ..GuiSkeleton import GuiSkeleton from ..GuiSkeleton import GuiSkeleton
from ..GuiPostprocessor import GuiPostprocessor from ..GuiPostprocessor import GuiPostprocessor
@@ -38,9 +41,9 @@ from . import Receiver
class PyQt5Gui(GuiSkeleton): 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._cfg = config
self._conn = camera_conn self._conn = camera_conn
@@ -182,6 +185,8 @@ class PyQt5Gui(GuiSkeleton):
def _showSettings(self): def _showSettings(self):
self._comm.send(Workers.MASTER, StateMachine.GuiEvent('settings'))
self._disableTrigger() self._disableTrigger()
self._disableEscape() self._disableEscape()
self._lastHandle = self._showSettings self._lastHandle = self._showSettings
@@ -190,6 +195,8 @@ class PyQt5Gui(GuiSkeleton):
def _showStart(self, state): def _showStart(self, state):
self._comm.send(Workers.MASTER, StateMachine.GuiEvent('start'))
self._disableTrigger() self._disableTrigger()
self._enableEscape() self._enableEscape()
self._lastHandle = self._showWelcomeScreen self._lastHandle = self._showWelcomeScreen

View File

@@ -32,6 +32,8 @@ from . import camera, gui
from .Config import Config from .Config import Config
from .Photobooth import Photobooth from .Photobooth import Photobooth
from .util import lookup_and_import from .util import lookup_and_import
from .StateMachine import Context
from .Threading import Communicator, Workers
from .Worker import Worker from .Worker import Worker
@@ -99,7 +101,7 @@ class WorkerProcess(mp.Process):
class GuiProcess(mp.Process): class GuiProcess(mp.Process):
def __init__(self, argv, config, conn, queue): def __init__(self, argv, config, conn, queue, communicator):
super().__init__() super().__init__()
@@ -107,12 +109,13 @@ class GuiProcess(mp.Process):
self.cfg = config self.cfg = config
self.conn = conn self.conn = conn
self.queue = queue self.queue = queue
self.comm = communicator
def run(self): def run(self):
Gui = lookup_and_import(gui.modules, self.cfg.get('Gui', 'module'), Gui = lookup_and_import(gui.modules, self.cfg.get('Gui', 'module'),
'gui') '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): def run(argv):
@@ -122,6 +125,9 @@ def run(argv):
# Load configuration # Load configuration
config = Config('photobooth.cfg') config = Config('photobooth.cfg')
comm = Communicator()
context = Context()
# Create communication objects: # Create communication objects:
# 1. We use a pipe to connect GUI and camera process # 1. We use a pipe to connect GUI and camera process
# 2. We use a queue to feed tasks to the postprocessing 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 = WorkerProcess(config, worker_queue)
worker_proc.start() 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() gui_proc.start()
for event in comm.iter(Workers.MASTER):
context.handleEvent(event)
# Close endpoints # Close endpoints
gui_conn.close() gui_conn.close()
camera_conn.close() camera_conn.close()
@@ -156,7 +165,7 @@ def run(argv):
def main(argv): def main(argv):
# Setup log level and format # Setup log level and format
log_level = logging.INFO log_level = logging.DEBUG
formatter = logging.Formatter( formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s') '%(asctime)s - %(name)s - %(levelname)s - %(message)s')