Rudimentary support for new communication scheme in GUI

This commit is contained in:
Balthasar Reuter
2018-07-16 00:31:24 +02:00
parent ce9597ceed
commit 2a59d27437
9 changed files with 178 additions and 186 deletions

View File

@@ -55,6 +55,10 @@ class Context:
self.state = ErrorState(event.exception, self.state)
elif isinstance(event, TeardownEvent):
self.state = TeardownState(event.target)
if event.target == TeardownEvent.EXIT:
return 0
elif event.target == TeardownEvent.RESTART:
return 123
else:
self.state.handleEvent(event, self)
@@ -206,7 +210,7 @@ class ErrorState(State):
context.state = self.old_state
context.state.update()
elif isinstance(event, GuiEvent) and event.name == 'abort':
context.state = TeardownState()
context.state = TeardownState(TeardownEvent.WELCOME)
else:
raise TypeError('Unknown Event type "{}"'.format(event))
@@ -216,11 +220,27 @@ class TeardownState(State):
def __init__(self, target):
super().__init__()
self._target = target
def __str__(self):
return 'TeardownState'
@property
def target(self):
return self._target
def handleEvent(self, event, context):
if self._target == TeardownEvent.WELCOME:
if isinstance(event, GuiEvent) and event.name == 'welcome':
context.state = WelcomeState()
else:
raise ValueError('Unknown GuiEvent "{}"'.format(event.name))
else:
raise TypeError('Unknown Event type "{}"'.format(event))
class WelcomeState(State):
@@ -237,34 +257,14 @@ class WelcomeState(State):
if isinstance(event, GuiEvent):
if event.name == 'start':
context.state = StartupState()
elif event.name == 'settings':
context.state = SettingsState()
elif event.name == 'exit':
context.state = TeardownState()
context.state = TeardownState(TeardownEvent.EXIT)
else:
raise ValueError('Unknown GuiEvent "{}"'.format(event.name))
else:
raise TypeError('Unknown Event type "{}"'.format(event))
class SettingsState(State):
def __init__(self):
super().__init__()
def __str__(self):
return 'SettingsState'
def handleEvent(self, event, context):
if isinstance(event, GuiEvent) and event.name == 'welcome':
context.state = WelcomeState()
else:
raise TypeError('Unknown Event type "{}"'.format(event))
class StartupState(State):
def __init__(self):

View File

@@ -19,10 +19,13 @@
import logging
import os.path
import sys
from time import localtime, strftime
from .PictureList import PictureList
from .StateMachine import TeardownEvent, TeardownState
from .Threading import Workers
class WorkerTask:
@@ -60,13 +63,23 @@ class PictureSaver(WorkerTask):
class Worker:
def __init__(self, config, queue):
def __init__(self, config, comm):
self._queue = queue
self._comm = comm
def run(self):
for func, args in iter(self._queue.get, 'teardown'):
func(*args)
for state in self._comm.iter(Workers.WORKER):
self.handleState(state)
return 0
def handleState(self, state):
if isinstance(state, TeardownState):
self.teardown(state)
def teardown(self, state):
if state.target == TeardownEvent.EXIT:
sys.exit(0)
elif state.target == TeardownEvent.RESTART:
sys.exit(123)

View File

@@ -18,6 +18,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
import sys
from PIL import Image, ImageOps
@@ -51,13 +52,17 @@ class Camera:
self._is_keep_pictures = config.getBool('Photobooth', 'keep_pictures')
logging.info('Using camera {} preview functionality'.format(
'with' if self.is_preview else 'without'))
'with' if self._is_preview else 'without'))
self.setIdle()
def teardown(self):
def teardown(self, state):
self._cap.cleanup()
if state.target == StateMachine.TeardownEvent.EXIT:
sys.exit(0)
elif state.target == StateMachine.TeardownEvent.RESTART:
sys.exit(123)
def run(self):
@@ -75,7 +80,7 @@ class Camera:
elif isinstance(state, StateMachine.AssembleState):
self.assemblePicture()
elif isinstance(state, StateMachine.TeardownState):
self.teardown()
self.teardown(state)
def setActive(self):

View File

@@ -48,6 +48,8 @@ countdown_time = 8
display_time = 5
# Timeout for postprocessing (shown after review)
postprocess_time = 60
# Keep single pictures (True/False)
keep_pictures = False
[Picture]
# Basedir of output pictures

View File

@@ -17,7 +17,6 @@
# 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 . import GuiState
from .. import StateMachine
@@ -86,8 +85,6 @@ class GuiSkeleton:
self.showWelcome(state)
elif isinstance(state, StateMachine.StartupState):
self.showStartup(state)
elif isinstance(state, StateMachine.SettingsState):
self.showSettings(state)
elif isinstance(state, StateMachine.IdleState):
self.showIdle(state)
elif isinstance(state, StateMachine.GreeterState):

View File

@@ -34,7 +34,7 @@ from . import Widgets
from . import styles
class Start(QtWidgets.QFrame):
class Welcome(QtWidgets.QFrame):
def __init__(self, start_action, set_date_action, settings_action,
exit_action):
@@ -79,7 +79,7 @@ class Start(QtWidgets.QFrame):
class IdleMessage(QtWidgets.QFrame):
def __init__(self):
def __init__(self, trigger_action):
super().__init__()
self.setObjectName('IdleMessage')
@@ -87,12 +87,13 @@ class IdleMessage(QtWidgets.QFrame):
self._message_label = 'Hit the'
self._message_button = 'Button!'
self.initFrame()
self.initFrame(trigger_action)
def initFrame(self):
def initFrame(self, trigger_action):
lbl = QtWidgets.QLabel(self._message_label)
btn = QtWidgets.QPushButton(self._message_button)
btn.clicked.connect(trigger_action)
lay = QtWidgets.QVBoxLayout()
lay.addWidget(lbl)
@@ -102,7 +103,7 @@ class IdleMessage(QtWidgets.QFrame):
class GreeterMessage(QtWidgets.QFrame):
def __init__(self, num_x, num_y):
def __init__(self, num_x, num_y, countdown_action):
super().__init__()
self.setObjectName('GreeterMessage')
@@ -114,14 +115,15 @@ class GreeterMessage(QtWidgets.QFrame):
else:
self._text_label = ''
self.initFrame()
self.initFrame(countdown_action)
def initFrame(self):
def initFrame(self, countdown_action):
ttl = QtWidgets.QLabel(self._text_title)
ttl.setObjectName('title')
btn = QtWidgets.QPushButton(self._text_button)
btn.setObjectName('button')
btn.clicked.connect(countdown_action)
lbl = QtWidgets.QLabel(self._text_label)
lbl.setObjectName('message')
@@ -132,7 +134,7 @@ class GreeterMessage(QtWidgets.QFrame):
self.setLayout(lay)
class PoseMessage(QtWidgets.QFrame):
class CaptureMessage(QtWidgets.QFrame):
def __init__(self, num_picture, num_x, num_y):
@@ -325,6 +327,7 @@ class PostprocessMessage(Widgets.TransparentOverlay):
def disableAndCall(button, handle):
button.setEnabled(False)
button.update()
handle()
def createButton(task):

View File

@@ -27,10 +27,9 @@ from PyQt5 import QtWidgets
from PIL import ImageQt
# from ... import StateMachine
# from ...Threading import Workers
from ...StateMachine import GuiEvent, TeardownEvent
from ...Threading import Workers
from .. import GuiState
from ..GuiSkeleton import GuiSkeleton
from ..GuiPostprocessor import GuiPostprocessor
@@ -41,70 +40,54 @@ from . import Receiver
class PyQt5Gui(GuiSkeleton):
def __init__(self, argv, config, camera_conn, worker_queue, communicator):
def __init__(self, argv, config, communicator):
super().__init__(communicator)
self._cfg = config
self._conn = camera_conn
parser = argparse.ArgumentParser()
parser.add_argument('--run', action='store_true',
help='omit welcome screen and run photobooth')
parsed_args, unparsed_args = parser.parse_known_args()
self._omit_welcome = parsed_args.run
self._registerCallbacks()
is_start, unparsed_args = self._parseArgs()
self._initUI(argv[:1] + unparsed_args)
self._initReceiver()
self._picture = None
self._postprocess = GuiPostprocessor(self._cfg)
if is_start:
self._comm.send(Workers.MASTER, GuiEvent('start'))
def run(self):
if self._omit_welcome:
self._showStart(None)
else:
self._showWelcomeScreen()
exit_code = self._app.exec_()
self._gui = None
return exit_code
def close(self):
def _parseArgs(self):
self._gui.close()
# Add parameter for direct startup
parser = argparse.ArgumentParser()
parser.add_argument('--run', action='store_true',
help='omit welcome screen and run photobooth')
parsed_args, unparsed_args = parser.parse_known_args()
def restart(self):
self._app.exit(123)
def _registerCallbacks(self):
self.idle = self._showIdle
self.trigger = self._sendTrigger
self.greeter = self._showGreeter
self.countdown = self._showCountdown
self.preview = self._showPreview
self.pose = self._showPose
self.assemble = self._showAssemble
self.review = self._showReview
self.teardown = self._sendTeardown
self.error = self._showError
return (parsed_args.run, unparsed_args)
def _initUI(self, argv):
self._disableTrigger()
# Load stylesheet
style = self._cfg.get('Gui', 'style')
filename = next((file for name, file in styles if name == style))
with open(os.path.join(os.path.dirname(__file__), filename), 'r') as f:
stylesheet = f.read()
# Create application and main window
self._app = QtWidgets.QApplication(argv)
self._app.setStyleSheet(stylesheet)
self._gui = PyQt5MainWindow(self._cfg, self._handleKeypressEvent)
# Load additional fonts
fonts = ['photobooth/gui/Qt5Gui/fonts/AmaticSC-Regular.ttf',
'photobooth/gui/Qt5Gui/fonts/AmaticSC-Bold.ttf']
self._fonts = QtGui.QFontDatabase()
@@ -113,14 +96,11 @@ class PyQt5Gui(GuiSkeleton):
def _initReceiver(self):
self._receiver = Receiver.Receiver([self._conn])
# Create receiver thread
self._receiver = Receiver.Receiver(self._comm)
self._receiver.notify.connect(self.handleState)
self._receiver.start()
def _setWidget(self, widget):
self._gui.setCentralWidget(widget)
def _enableEscape(self):
self._is_escape = True
@@ -137,82 +117,58 @@ class PyQt5Gui(GuiSkeleton):
self._is_trigger = False
def _sendStart(self):
def _setWidget(self, widget):
self._conn.send('start')
self._gui.setCentralWidget(widget)
def _sendTrigger(self, state):
def close(self):
self._conn.send('triggered')
if self._gui.close():
self._comm.send(Workers.MASTER, TeardownEvent(TeardownEvent.EXIT))
def _sendAck(self):
def teardown(self, state):
self._conn.send('ack')
if state.target == TeardownEvent.EXIT:
self._app.exit(0)
elif state.target == TeardownEvent.RESTART:
self._app.exit(123)
elif state.target == TeardownEvent.WELCOME:
self._comm.send(Workers.MASTER, GuiEvent('welcome'))
def _sendCancel(self):
def showError(self, state):
self._conn.send('cancel')
logging.error('%s: %s', state.title, state.message)
def _sendTeardown(self, state):
MessageBox(self, MessageBox.RETRY, state.title, state.message,
lambda: self._comm.send(Workers.MASTER, GuiEvent('retry')),
lambda: self._comm.send(Workers.MASTER, GuiEvent('abort')))
self._conn.send('teardown')
self._showWelcomeScreen()
def _handleKeypressEvent(self, event):
if self._is_escape and event.key() == QtCore.Qt.Key_Escape:
self.handleState(GuiState.TeardownState())
elif self._is_trigger and event.key() == QtCore.Qt.Key_Space:
self.handleState(GuiState.TriggerState())
def _showWelcomeScreen(self):
def showWelcome(self, state):
self._disableTrigger()
self._disableEscape()
self._lastHandle = self._showWelcomeScreen
self._setWidget(Frames.Start(self._showStart, self._showSetDateTime,
self._showSettings, self.close))
self._setWidget(Frames.Welcome(
lambda: self._comm.send(Workers.MASTER, GuiEvent('start')),
self._showSetDateTime, self._showSettings, self.close))
if QtWidgets.QApplication.overrideCursor() != 0:
QtWidgets.QApplication.restoreOverrideCursor()
def _showSetDateTime(self):
self._disableTrigger()
self._disableEscape()
self._lastHandle = self._showSetDateTime
self._setWidget(Frames.SetDateTime(self._showWelcomeScreen,
self.restart))
def _showSettings(self):
# self._comm.send(Workers.MASTER, StateMachine.GuiEvent('settings'))
self._disableTrigger()
self._disableEscape()
self._lastHandle = self._showSettings
self._setWidget(Frames.Settings(self._cfg, self._showSettings,
self._showWelcomeScreen, self.restart))
def _showStart(self, state):
# self._comm.send(Workers.MASTER, StateMachine.GuiEvent('start'))
def showStartup(self, state):
self._disableTrigger()
self._enableEscape()
self._lastHandle = self._showWelcomeScreen
self._sendStart()
self._setWidget(Frames.WaitMessage('Starting the photobooth...'))
if self._cfg.getBool('Gui', 'hide_cursor'):
QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.BlankCursor)
def _showIdle(self, state):
def showIdle(self, state):
self._enableEscape()
self._enableTrigger()
self._lastHandle = self._showIdle
self._setWidget(Frames.IdleMessage())
self._setWidget(Frames.IdleMessage(
lambda: self._comm.send(Workers.MASTER, GuiEvent('trigger'))))
def _showGreeter(self, state):
def showGreeter(self, state):
self._enableEscape()
self._disableTrigger()
@@ -221,56 +177,79 @@ class PyQt5Gui(GuiSkeleton):
self._cfg.getInt('Picture', 'num_y'))
greeter_time = self._cfg.getInt('Photobooth', 'greeter_time') * 1000
self._setWidget(Frames.GreeterMessage(*num_pic))
QtCore.QTimer.singleShot(greeter_time, self._sendAck)
self._setWidget(Frames.GreeterMessage(
*num_pic,
lambda: self._comm.send(Workers.MASTER, GuiEvent('countdown'))))
QtCore.QTimer.singleShot(
greeter_time,
lambda: self._comm.send(Workers.MASTER, GuiEvent('countdown')))
def _showCountdown(self, state):
def showCountdown(self, state):
countdown_time = self._cfg.getInt('Photobooth', 'countdown_time')
self._setWidget(Frames.CountdownMessage(countdown_time, self._sendAck))
self._setWidget(Frames.CountdownMessage(
countdown_time,
lambda: self._comm.send(Workers.MASTER, GuiEvent('capture'))))
def _showPreview(self, state):
def updateCountdown(self, event):
self._gui.centralWidget().picture = ImageQt.ImageQt(state.picture)
self._gui.centralWidget().picture = ImageQt.ImageQt(event.picture)
self._gui.centralWidget().update()
def _showPose(self, state):
def showCapture(self, state):
num_pic = (self._cfg.getInt('Picture', 'num_x'),
self._cfg.getInt('Picture', 'num_y'))
self._setWidget(Frames.PoseMessage(state.num_picture, *num_pic))
self._setWidget(Frames.CaptureMessage(state.num_picture, *num_pic))
def _showAssemble(self, state):
def showAssemble(self, state):
self._setWidget(Frames.WaitMessage('Processing picture...'))
def _showReview(self, state):
def showReview(self, state):
img = ImageQt.ImageQt(state.picture)
self._picture = ImageQt.ImageQt(state.picture)
review_time = self._cfg.getInt('Photobooth', 'display_time') * 1000
self._setWidget(Frames.PictureMessage(img))
QtCore.QTimer.singleShot(review_time, lambda:
self._showPostprocess(state.picture))
self._setWidget(Frames.PictureMessage(self._picture))
QtCore.QTimer.singleShot(
review_time,
lambda: self._comm.send(Workers.MASTER, GuiEvent('postprocess')))
def _showPostprocess(self, picture):
def showPostprocess(self, state):
tasks = self._postprocess.get(picture)
tasks = self._postprocess.get(self._picture)
postproc_t = self._cfg.getInt('Photobooth', 'postprocess_time')
Frames.PostprocessMessage(self._gui.centralWidget(), tasks,
self._sendAck, postproc_t * 1000)
Frames.PostprocessMessage(
self._gui.centralWidget(), tasks,
lambda: self._comm.send(Workers.MASTER, GuiEvent('idle')),
postproc_t * 1000)
def _showError(self, state):
def _handleKeypressEvent(self, event):
logging.error('%s: %s', state.title, state.message)
if self._is_escape and event.key() == QtCore.Qt.Key_Escape:
self._comm.send(Workers.MASTER,
TeardownEvent(TeardownEvent.WELCOME))
elif self._is_trigger and event.key() == QtCore.Qt.Key_Space:
self._comm.send(Workers.MASTER, GuiEvent('trigger'))
def exec(*handles):
for handle in handles:
handle()
def _showSetDateTime(self):
MessageBox(self, MessageBox.RETRY, state.title, state.message,
exec(self._sendAck, self._lastState),
exec(self._sendCancel, self._showWelcomeScreen))
self._disableTrigger()
self._disableEscape()
self._setWidget(Frames.SetDateTime(
self.showWelcome,
lambda: self._comm.send(Workers.MASTER,
TeardownEvent(TeardownEvent.RESTART))))
def _showSettings(self):
self._disableTrigger()
self._disableEscape()
self._setWidget(Frames.Settings(
self._cfg, self._showSettings, self.showWelcome,
lambda: self._comm.send(Workers.MASTER,
TeardownEvent(TeardownEvent.RESTART))))
class PyQt5MainWindow(QtWidgets.QMainWindow):

View File

@@ -17,19 +17,19 @@
# 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/>.
import multiprocessing as mp
from PyQt5 import QtCore
from ...Threading import Workers
class Receiver(QtCore.QThread):
notify = QtCore.pyqtSignal(object)
def __init__(self, conn):
def __init__(self, comm):
super().__init__()
self._conn = conn
self._comm = comm
def handle(self, state):
@@ -37,11 +37,5 @@ class Receiver(QtCore.QThread):
def run(self):
while self._conn:
for c in mp.connection.wait(self._conn):
try:
state = c.recv()
except EOFError:
break
else:
self.handle(state)
for state in self._comm.iter(Workers.GUI):
self.handle(state)

View File

@@ -98,37 +98,34 @@ class CameraProcess(mp.Process):
class WorkerProcess(mp.Process):
def __init__(self, config, queue):
def __init__(self, config, comm):
super().__init__()
self.daemon = True
self.cfg = config
self.queue = queue
self.comm = comm
def run(self):
sys.exit(Worker(self.cfg, self.queue).run())
sys.exit(Worker(self.cfg, self.comm).run())
class GuiProcess(mp.Process):
def __init__(self, argv, config, conn, queue, communicator):
def __init__(self, argv, config, communicator):
super().__init__()
self.argv = argv
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,
self.comm).run())
sys.exit(Gui(self.argv, self.cfg, self.comm).run())
def run(argv):
@@ -144,8 +141,8 @@ def run(argv):
# 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
gui_conn, camera_conn = mp.Pipe()
worker_queue = mp.SimpleQueue()
# gui_conn, camera_conn = mp.Pipe()
# worker_queue = mp.SimpleQueue()
# Initialize processes: We use three processes here:
# 1. Camera processing
@@ -154,25 +151,27 @@ def run(argv):
camera_proc = CameraProcess(config, comm) # camera_conn, worker_queue)
camera_proc.start()
worker_proc = WorkerProcess(config, worker_queue)
worker_proc = WorkerProcess(config, comm)
worker_proc.start()
gui_proc = GuiProcess(argv, config, gui_conn, worker_queue, comm)
gui_proc = GuiProcess(argv, config, comm)
gui_proc.start()
for event in comm.iter(Workers.MASTER):
context.handleEvent(event)
exit_code = context.handleEvent(event)
if exit_code in (0, 123):
break
# Close endpoints
gui_conn.close()
camera_conn.close()
# gui_conn.close()
# camera_conn.close()
# Wait for processes to finish
gui_proc.join()
worker_queue.put('teardown')
# worker_queue.put('teardown')
worker_proc.join()
camera_proc.join(1)
return gui_proc.exitcode
return exit_code
def main(argv):