pibooth-ceeeeb 2.0.8.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pibooth/__init__.py +15 -0
- pibooth/booth.py +589 -0
- pibooth/camera/__init__.py +52 -0
- pibooth/camera/base.py +134 -0
- pibooth/camera/gphoto.py +311 -0
- pibooth/camera/hybrid.py +89 -0
- pibooth/camera/opencv.py +239 -0
- pibooth/camera/rpi.py +169 -0
- pibooth/config/__init__.py +4 -0
- pibooth/config/menu.py +424 -0
- pibooth/config/parser.py +533 -0
- pibooth/counters.py +66 -0
- pibooth/fonts/Amatic-Bold.ttf +0 -0
- pibooth/fonts/AmaticSC-Regular.ttf +0 -0
- pibooth/fonts/DancingScript-Bold.ttf +0 -0
- pibooth/fonts/DancingScript-Regular.ttf +0 -0
- pibooth/fonts/Monoid-Bold.ttf +0 -0
- pibooth/fonts/Monoid-Regular.ttf +0 -0
- pibooth/fonts/Monoid-Retina.ttf +0 -0
- pibooth/fonts/Roboto-BoldItalic.ttf +0 -0
- pibooth/fonts/Roboto-LightItalic.ttf +0 -0
- pibooth/fonts/__init__.py +111 -0
- pibooth/language.py +278 -0
- pibooth/pictures/__init__.py +192 -0
- pibooth/pictures/factory.py +528 -0
- pibooth/pictures/pool.py +36 -0
- pibooth/pictures/sizing.py +114 -0
- pibooth/plugins/__init__.py +152 -0
- pibooth/plugins/camera_plugin.py +116 -0
- pibooth/plugins/hookspecs.py +512 -0
- pibooth/plugins/lights_plugin.py +72 -0
- pibooth/plugins/picture_plugin.py +159 -0
- pibooth/plugins/printer_plugin.py +67 -0
- pibooth/plugins/view_plugin.py +187 -0
- pibooth/printer.py +135 -0
- pibooth/scripts/__init__.py +0 -0
- pibooth/scripts/count.py +45 -0
- pibooth/scripts/diagnostic.py +223 -0
- pibooth/scripts/fonts.py +38 -0
- pibooth/scripts/printer.py +78 -0
- pibooth/scripts/regenerate.py +93 -0
- pibooth/states.py +105 -0
- pibooth/utils.py +296 -0
- pibooth/view/__init__.py +3 -0
- pibooth/view/background.py +681 -0
- pibooth/view/window.py +392 -0
- pibooth_ceeeeb-2.0.8.2.dist-info/LICENSE +22 -0
- pibooth_ceeeeb-2.0.8.2.dist-info/METADATA +73 -0
- pibooth_ceeeeb-2.0.8.2.dist-info/RECORD +52 -0
- pibooth_ceeeeb-2.0.8.2.dist-info/WHEEL +5 -0
- pibooth_ceeeeb-2.0.8.2.dist-info/entry_points.txt +7 -0
- pibooth_ceeeeb-2.0.8.2.dist-info/top_level.txt +1 -0
pibooth/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""A photo booth application in pure Python for the Raspberry Pi."""
|
|
4
|
+
|
|
5
|
+
__version__ = "2.0.8.2"
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
|
|
9
|
+
import pluggy
|
|
10
|
+
|
|
11
|
+
# Marker to be imported and used in plugins (and for own implementations)
|
|
12
|
+
hookimpl = pluggy.HookimplMarker('pibooth')
|
|
13
|
+
|
|
14
|
+
except ImportError:
|
|
15
|
+
pass # When running the setup.py, pluggy is not yet installed
|
pibooth/booth.py
ADDED
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
"""Pibooth main module.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import os.path as osp
|
|
9
|
+
import tempfile
|
|
10
|
+
import shutil
|
|
11
|
+
import logging
|
|
12
|
+
import argparse
|
|
13
|
+
import multiprocessing
|
|
14
|
+
from warnings import filterwarnings
|
|
15
|
+
|
|
16
|
+
import pygame
|
|
17
|
+
from gpiozero import Device, Button, ButtonBoard, LEDBoard, pi_info
|
|
18
|
+
from gpiozero.exc import BadPinFactory, PinFactoryFallback
|
|
19
|
+
|
|
20
|
+
import pibooth
|
|
21
|
+
from pibooth import fonts
|
|
22
|
+
from pibooth import language
|
|
23
|
+
from pibooth.counters import Counters
|
|
24
|
+
from pibooth.utils import (LOGGER, PoolingTimer, configure_logging, get_crash_message,
|
|
25
|
+
set_logging_level, get_event_pos)
|
|
26
|
+
from pibooth.states import StateMachine
|
|
27
|
+
from pibooth.plugins import create_plugin_manager
|
|
28
|
+
from pibooth.view import PiWindow
|
|
29
|
+
from pibooth.config import PiConfigParser, PiConfigMenu
|
|
30
|
+
from pibooth.printer import PRINTER_TASKS_UPDATED, Printer
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Set the default pin factory to a mock factory if pibooth is not started a Raspberry Pi
|
|
34
|
+
try:
|
|
35
|
+
filterwarnings("ignore", category=PinFactoryFallback)
|
|
36
|
+
GPIO_INFO = "on Raspberry pi {0}".format(pi_info().model)
|
|
37
|
+
except BadPinFactory:
|
|
38
|
+
from gpiozero.pins.mock import MockFactory
|
|
39
|
+
Device.pin_factory = MockFactory()
|
|
40
|
+
GPIO_INFO = "without physical GPIO, fallback to GPIO mock"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
BUTTONDOWN = pygame.USEREVENT + 1
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# Workaround for ButtonBoard bug in gpiozero 2.0.1
|
|
48
|
+
class LgpioButton:
|
|
49
|
+
"""Simple button using lgpio directly with polling."""
|
|
50
|
+
def __init__(self, gpio_num, hold_time=0.2):
|
|
51
|
+
import lgpio
|
|
52
|
+
import threading
|
|
53
|
+
self._gpio = gpio_num
|
|
54
|
+
self._hold_time = hold_time
|
|
55
|
+
self._handle = lgpio.gpiochip_open(0)
|
|
56
|
+
lgpio.gpio_claim_input(self._handle, gpio_num, lgpio.SET_PULL_UP)
|
|
57
|
+
self._callback = None
|
|
58
|
+
self._running = True
|
|
59
|
+
self._last_state = 1 # Not pressed
|
|
60
|
+
self._press_start = 0
|
|
61
|
+
self._thread = threading.Thread(target=self._monitor, daemon=True)
|
|
62
|
+
self._thread.start()
|
|
63
|
+
|
|
64
|
+
def _monitor(self):
|
|
65
|
+
import lgpio
|
|
66
|
+
import time
|
|
67
|
+
while self._running:
|
|
68
|
+
try:
|
|
69
|
+
current = lgpio.gpio_read(self._handle, self._gpio)
|
|
70
|
+
if current == 0 and self._last_state == 1:
|
|
71
|
+
# Button just pressed
|
|
72
|
+
self._press_start = time.time()
|
|
73
|
+
LOGGER.debug("LgpioButton GPIO%s: PRESSED", self._gpio)
|
|
74
|
+
elif current == 0 and self._last_state == 0:
|
|
75
|
+
# Button still pressed
|
|
76
|
+
if self._callback and (time.time() - self._press_start) >= self._hold_time:
|
|
77
|
+
LOGGER.debug("LgpioButton GPIO%s: HOLD triggered, calling callback", self._gpio)
|
|
78
|
+
self._callback()
|
|
79
|
+
self._press_start = time.time() + 999 # Prevent repeat
|
|
80
|
+
self._last_state = current
|
|
81
|
+
except Exception as e:
|
|
82
|
+
LOGGER.debug("LgpioButton GPIO%s: error %s", self._gpio, e)
|
|
83
|
+
time.sleep(0.05)
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def when_held(self):
|
|
87
|
+
return self._callback
|
|
88
|
+
|
|
89
|
+
@when_held.setter
|
|
90
|
+
def when_held(self, callback):
|
|
91
|
+
self._callback = callback
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def value(self):
|
|
95
|
+
import lgpio
|
|
96
|
+
# Return 1 when pressed (GPIO LOW), 0 when not pressed (GPIO HIGH)
|
|
97
|
+
return 1 - lgpio.gpio_read(self._handle, self._gpio)
|
|
98
|
+
|
|
99
|
+
def close(self):
|
|
100
|
+
self._running = False
|
|
101
|
+
import lgpio
|
|
102
|
+
try:
|
|
103
|
+
lgpio.gpiochip_close(self._handle)
|
|
104
|
+
except:
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class ButtonsWrapper:
|
|
109
|
+
"""Wrapper that uses individual Button objects."""
|
|
110
|
+
def __init__(self, capture_pin, printer_pin, hold_time, pull_up):
|
|
111
|
+
self.capture = Button(capture_pin, pull_up=pull_up, hold_time=hold_time)
|
|
112
|
+
# Use lgpio directly for printer button due to gpiozero bug
|
|
113
|
+
printer_gpio = int(printer_pin.replace("BOARD", ""))
|
|
114
|
+
# Convert BOARD to BCM: BOARD32 = GPIO12
|
|
115
|
+
board_to_bcm = {32: 12, 11: 17, 36: 16, 37: 26, 33: 13, 31: 6}
|
|
116
|
+
self.printer = LgpioButton(board_to_bcm.get(printer_gpio, 12), hold_time=hold_time)
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def value(self):
|
|
120
|
+
return (self.capture.value, self.printer.value)
|
|
121
|
+
|
|
122
|
+
def close(self):
|
|
123
|
+
self.capture.close()
|
|
124
|
+
self.printer.close()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class PiApplication(object):
|
|
128
|
+
|
|
129
|
+
"""Main class representing the ``pibooth`` software.
|
|
130
|
+
The following attributes are available for use in plugins:
|
|
131
|
+
|
|
132
|
+
:attr capture_nbr: number of capture to be done in the current sequence
|
|
133
|
+
:type capture_nbr: int
|
|
134
|
+
:attr capture_date: date (%Y-%m-%d-%H-%M-%S) of the first capture of the current sequence
|
|
135
|
+
:type capture_date: str
|
|
136
|
+
:attr capture_choices: possible choices of captures numbers.
|
|
137
|
+
:type capture_choices: tuple
|
|
138
|
+
:attr previous_picture: picture generated during last sequence
|
|
139
|
+
:type previous_picture: :py:class:`PIL.Image`
|
|
140
|
+
:attr previous_animated: infinite list of picture to display during animation
|
|
141
|
+
:type previous_animated: :py:func:`itertools.cycle`
|
|
142
|
+
:attr previous_picture_file: file name of the picture generated during last sequence
|
|
143
|
+
:type previous_picture_file: str
|
|
144
|
+
:attr count: holder for counter values
|
|
145
|
+
:type count: :py:class:`pibooth.counters.Counters`
|
|
146
|
+
:attr camera: camera used
|
|
147
|
+
:type camera: :py:class:`pibooth.camera.base.BaseCamera`
|
|
148
|
+
:attr buttons: access to hardware buttons ``capture`` and ``printer``
|
|
149
|
+
:type buttons: :py:class:`gpiozero.ButtonBoard`
|
|
150
|
+
:attr leds: access to hardware LED ``capture`` and ``printer``
|
|
151
|
+
:attr leds: :py:class:`gpiozero.LEDBoard`
|
|
152
|
+
:attr printer: printer used
|
|
153
|
+
:type printer: :py:class:`pibooth.printer.Printer`
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
def __init__(self, config, plugin_manager):
|
|
157
|
+
self._pm = plugin_manager
|
|
158
|
+
self._config = config
|
|
159
|
+
|
|
160
|
+
# Create directories where pictures are saved
|
|
161
|
+
for savedir in config.gettuple('GENERAL', 'directory', 'path'):
|
|
162
|
+
if osp.isdir(savedir) and config.getboolean('GENERAL', 'debug'):
|
|
163
|
+
shutil.rmtree(savedir)
|
|
164
|
+
if not osp.isdir(savedir):
|
|
165
|
+
os.makedirs(savedir)
|
|
166
|
+
|
|
167
|
+
# Create window of (width, height)
|
|
168
|
+
init_size = self._config.gettyped('WINDOW', 'size')
|
|
169
|
+
init_debug = self._config.getboolean('GENERAL', 'debug')
|
|
170
|
+
init_color = self._config.gettyped('WINDOW', 'background')
|
|
171
|
+
init_text_color = self._config.gettyped('WINDOW', 'text_color')
|
|
172
|
+
if not isinstance(init_color, (tuple, list)):
|
|
173
|
+
init_color = self._config.getpath('WINDOW', 'background')
|
|
174
|
+
|
|
175
|
+
title = 'Pibooth v{}'.format(pibooth.__version__)
|
|
176
|
+
if not isinstance(init_size, str):
|
|
177
|
+
self._window = PiWindow(title, init_size, color=init_color,
|
|
178
|
+
text_color=init_text_color, debug=init_debug)
|
|
179
|
+
else:
|
|
180
|
+
self._window = PiWindow(title, color=init_color,
|
|
181
|
+
text_color=init_text_color, debug=init_debug)
|
|
182
|
+
|
|
183
|
+
self._menu = None
|
|
184
|
+
self._multipress_timer = PoolingTimer(config.getfloat('CONTROLS', 'multi_press_delay'), False)
|
|
185
|
+
self._fingerdown_events = []
|
|
186
|
+
|
|
187
|
+
# Define states of the application
|
|
188
|
+
self._machine = StateMachine(self._pm, self._config, self, self._window)
|
|
189
|
+
self._machine.add_state('wait')
|
|
190
|
+
self._machine.add_state('choose')
|
|
191
|
+
self._machine.add_state('chosen')
|
|
192
|
+
self._machine.add_state('preview')
|
|
193
|
+
self._machine.add_state('capture')
|
|
194
|
+
self._machine.add_state('processing')
|
|
195
|
+
self._machine.add_state('print')
|
|
196
|
+
self._machine.add_state('finish')
|
|
197
|
+
|
|
198
|
+
# ---------------------------------------------------------------------
|
|
199
|
+
# Variables shared with plugins
|
|
200
|
+
# Change them may break plugins compatibility
|
|
201
|
+
self.capture_nbr = None
|
|
202
|
+
self.capture_date = None
|
|
203
|
+
self.capture_choices = (4, 1)
|
|
204
|
+
self.previous_picture = None
|
|
205
|
+
self.previous_animated = None
|
|
206
|
+
self.previous_picture_file = None
|
|
207
|
+
|
|
208
|
+
self.count = Counters(self._config.join_path("counters.pickle"),
|
|
209
|
+
taken=0, printed=0, forgotten=0,
|
|
210
|
+
remaining_duplicates=self._config.getint('PRINTER', 'max_duplicates'))
|
|
211
|
+
|
|
212
|
+
self.camera = self._pm.hook.pibooth_setup_camera(cfg=self._config)
|
|
213
|
+
|
|
214
|
+
self.buttons = ButtonsWrapper(capture_pin="BOARD" + config.get('CONTROLS', 'picture_btn_pin'),
|
|
215
|
+
printer_pin="BOARD" + config.get('CONTROLS', 'print_btn_pin'),
|
|
216
|
+
hold_time=config.getfloat('CONTROLS', 'debounce_delay'),
|
|
217
|
+
pull_up=True)
|
|
218
|
+
self.buttons.capture.when_held = self._on_button_capture_held
|
|
219
|
+
self.buttons.printer.when_held = self._on_button_printer_held
|
|
220
|
+
LOGGER.info("Buttons initialized: capture.value=%s printer.value=%s", self.buttons.capture.value, self.buttons.printer.value)
|
|
221
|
+
|
|
222
|
+
self.leds = LEDBoard(capture="BOARD" + config.get('CONTROLS', 'picture_led_pin'),
|
|
223
|
+
printer="BOARD" + config.get('CONTROLS', 'print_led_pin'))
|
|
224
|
+
|
|
225
|
+
self.printer = Printer(config.get('PRINTER', 'printer_name'),
|
|
226
|
+
config.getint('PRINTER', 'max_pages'),
|
|
227
|
+
config.gettyped('PRINTER', 'printer_options'),
|
|
228
|
+
self.count)
|
|
229
|
+
# ---------------------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
def _initialize(self):
|
|
232
|
+
"""Restore the application with initial parameters defined in the
|
|
233
|
+
configuration file.
|
|
234
|
+
Only parameters that can be changed at runtime are restored.
|
|
235
|
+
"""
|
|
236
|
+
# Handle the language configuration
|
|
237
|
+
language.CURRENT = self._config.get('GENERAL', 'language')
|
|
238
|
+
fonts.CURRENT = fonts.get_filename(self._config.get('WINDOW', 'font'))
|
|
239
|
+
|
|
240
|
+
# Set the captures choices
|
|
241
|
+
choices = self._config.gettuple('PICTURE', 'captures', int)
|
|
242
|
+
for chx in choices:
|
|
243
|
+
if chx not in [1, 2, 3, 4]:
|
|
244
|
+
LOGGER.warning("Invalid captures number '%s' in config, fallback to '%s'",
|
|
245
|
+
chx, self.capture_choices)
|
|
246
|
+
choices = self.capture_choices
|
|
247
|
+
break
|
|
248
|
+
self.capture_choices = choices
|
|
249
|
+
|
|
250
|
+
# Handle autostart of the application
|
|
251
|
+
self._config.handle_autostart()
|
|
252
|
+
|
|
253
|
+
self._window.arrow_location = self._config.get('WINDOW', 'arrows')
|
|
254
|
+
self._window.arrow_offset = self._config.getint('WINDOW', 'arrows_x_offset')
|
|
255
|
+
self._window.text_color = self._config.gettyped('WINDOW', 'text_color')
|
|
256
|
+
self._window.drop_cache()
|
|
257
|
+
|
|
258
|
+
# Handle window size
|
|
259
|
+
size = self._config.gettyped('WINDOW', 'size')
|
|
260
|
+
if isinstance(size, str) and size.lower() == 'fullscreen':
|
|
261
|
+
if not self._window.is_fullscreen:
|
|
262
|
+
self._window.toggle_fullscreen()
|
|
263
|
+
else:
|
|
264
|
+
if self._window.is_fullscreen:
|
|
265
|
+
self._window.toggle_fullscreen()
|
|
266
|
+
self._window.debug = self._config.getboolean('GENERAL', 'debug')
|
|
267
|
+
|
|
268
|
+
# Handle debug mode
|
|
269
|
+
if not self._config.getboolean('GENERAL', 'debug'):
|
|
270
|
+
set_logging_level() # Restore default level
|
|
271
|
+
self._machine.add_failsafe_state('failsafe')
|
|
272
|
+
else:
|
|
273
|
+
set_logging_level(logging.DEBUG)
|
|
274
|
+
self._machine.remove_state('failsafe')
|
|
275
|
+
|
|
276
|
+
# Reset the print counter (in case of max_pages is reached)
|
|
277
|
+
self.printer.max_pages = self._config.getint('PRINTER', 'max_pages')
|
|
278
|
+
|
|
279
|
+
def _on_button_capture_held(self):
|
|
280
|
+
"""Called when the capture button is pressed.
|
|
281
|
+
"""
|
|
282
|
+
if all(self.buttons.value):
|
|
283
|
+
self.buttons.capture.hold_repeat = True
|
|
284
|
+
if self._multipress_timer.elapsed() == 0:
|
|
285
|
+
self._multipress_timer.start()
|
|
286
|
+
if self._multipress_timer.is_timeout():
|
|
287
|
+
# Capture was held while printer was pressed
|
|
288
|
+
if self._menu and self._menu.is_shown():
|
|
289
|
+
# Convert HW button events to keyboard events for menu
|
|
290
|
+
event = self._menu.create_back_event()
|
|
291
|
+
LOGGER.debug("BUTTONDOWN: generate MENU-ESC event")
|
|
292
|
+
else:
|
|
293
|
+
event = pygame.event.Event(BUTTONDOWN, capture=1, printer=1,
|
|
294
|
+
button=self.buttons)
|
|
295
|
+
LOGGER.debug("BUTTONDOWN: generate DOUBLE buttons event")
|
|
296
|
+
self.buttons.capture.hold_repeat = False
|
|
297
|
+
self._multipress_timer.reset()
|
|
298
|
+
pygame.event.post(event)
|
|
299
|
+
else:
|
|
300
|
+
# Capture was held but printer not pressed
|
|
301
|
+
if self._menu and self._menu.is_shown():
|
|
302
|
+
# Convert HW button events to keyboard events for menu
|
|
303
|
+
event = self._menu.create_next_event()
|
|
304
|
+
LOGGER.debug("BUTTONDOWN: generate MENU-NEXT event")
|
|
305
|
+
else:
|
|
306
|
+
event = pygame.event.Event(BUTTONDOWN, capture=1, printer=0,
|
|
307
|
+
button=self.buttons.capture)
|
|
308
|
+
LOGGER.debug("BUTTONDOWN: generate CAPTURE button event")
|
|
309
|
+
self.buttons.capture.hold_repeat = False
|
|
310
|
+
self._multipress_timer.reset()
|
|
311
|
+
pygame.event.post(event)
|
|
312
|
+
|
|
313
|
+
def _on_button_printer_held(self):
|
|
314
|
+
"""Called when the printer button is pressed."""
|
|
315
|
+
if self._menu and self._menu.is_shown():
|
|
316
|
+
# Convert HW button events to keyboard events for menu
|
|
317
|
+
event = self._menu.create_click_event()
|
|
318
|
+
LOGGER.debug("BUTTONDOWN: generate MENU-APPLY event")
|
|
319
|
+
else:
|
|
320
|
+
event = pygame.event.Event(BUTTONDOWN, capture=0, printer=1,
|
|
321
|
+
button=self.buttons.printer)
|
|
322
|
+
LOGGER.debug("BUTTONDOWN: generate PRINTER event")
|
|
323
|
+
pygame.event.post(event)
|
|
324
|
+
|
|
325
|
+
@property
|
|
326
|
+
def picture_filename(self):
|
|
327
|
+
"""Return the final picture file name.
|
|
328
|
+
"""
|
|
329
|
+
if not self.capture_date:
|
|
330
|
+
raise EnvironmentError("The 'capture_date' attribute is not set yet")
|
|
331
|
+
return "{}_pibooth.jpg".format(self.capture_date)
|
|
332
|
+
|
|
333
|
+
def find_quit_event(self, events):
|
|
334
|
+
"""Return the first found event if found in the list.
|
|
335
|
+
"""
|
|
336
|
+
for event in events:
|
|
337
|
+
if event.type == pygame.QUIT:
|
|
338
|
+
return event
|
|
339
|
+
return None
|
|
340
|
+
|
|
341
|
+
def find_settings_event(self, events):
|
|
342
|
+
"""Return the first found event if found in the list.
|
|
343
|
+
"""
|
|
344
|
+
for event in events:
|
|
345
|
+
if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
|
|
346
|
+
return event
|
|
347
|
+
if event.type == BUTTONDOWN and event.capture and event.printer:
|
|
348
|
+
return event
|
|
349
|
+
if event.type == pygame.FINGERDOWN:
|
|
350
|
+
# Press but not release
|
|
351
|
+
self._fingerdown_events.append(event)
|
|
352
|
+
if event.type == pygame.FINGERUP:
|
|
353
|
+
# Resetting touch_events
|
|
354
|
+
self._fingerdown_events = []
|
|
355
|
+
if len(self._fingerdown_events) > 3:
|
|
356
|
+
# 4 fingers on the screen trigger the menu
|
|
357
|
+
self._fingerdown_events = []
|
|
358
|
+
return pygame.event.Event(BUTTONDOWN, capture=1, printer=1,
|
|
359
|
+
button=self.buttons)
|
|
360
|
+
return None
|
|
361
|
+
|
|
362
|
+
def find_close_menu_event(self, events):
|
|
363
|
+
"""Return the first 4-finger gesture from the events.
|
|
364
|
+
|
|
365
|
+
Used to trigger the save/discard confirmation popup while the
|
|
366
|
+
config menu is open. Shares ``_fingerdown_events`` with
|
|
367
|
+
``find_settings_event``.
|
|
368
|
+
"""
|
|
369
|
+
for event in events:
|
|
370
|
+
if event.type == pygame.FINGERDOWN:
|
|
371
|
+
self._fingerdown_events.append(event)
|
|
372
|
+
if event.type == pygame.FINGERUP:
|
|
373
|
+
self._fingerdown_events = []
|
|
374
|
+
if len(self._fingerdown_events) > 3:
|
|
375
|
+
self._fingerdown_events = []
|
|
376
|
+
return event
|
|
377
|
+
return None
|
|
378
|
+
|
|
379
|
+
def find_fullscreen_event(self, events):
|
|
380
|
+
"""Return the first found event if found in the list.
|
|
381
|
+
"""
|
|
382
|
+
for event in events:
|
|
383
|
+
if event.type == pygame.KEYDOWN and \
|
|
384
|
+
event.key == pygame.K_f and pygame.key.get_mods() & pygame.KMOD_CTRL:
|
|
385
|
+
return event
|
|
386
|
+
return None
|
|
387
|
+
|
|
388
|
+
def find_resize_event(self, events):
|
|
389
|
+
"""Return the first found event if found in the list.
|
|
390
|
+
"""
|
|
391
|
+
for event in events:
|
|
392
|
+
if event.type == pygame.VIDEORESIZE:
|
|
393
|
+
return event
|
|
394
|
+
return None
|
|
395
|
+
|
|
396
|
+
def find_capture_event(self, events):
|
|
397
|
+
"""Return the first found event if found in the list.
|
|
398
|
+
"""
|
|
399
|
+
for event in events:
|
|
400
|
+
if event.type == pygame.KEYDOWN and event.key == pygame.K_p:
|
|
401
|
+
return event
|
|
402
|
+
if (event.type == pygame.MOUSEBUTTONUP and event.button in (1, 2, 3)) or event.type == pygame.FINGERUP:
|
|
403
|
+
pos = get_event_pos(self._window.display_size, event)
|
|
404
|
+
rect = self._window.get_rect()
|
|
405
|
+
if pygame.Rect(0, 0, rect.width // 2, rect.height).collidepoint(pos):
|
|
406
|
+
return event
|
|
407
|
+
if event.type == BUTTONDOWN and event.capture:
|
|
408
|
+
return event
|
|
409
|
+
return None
|
|
410
|
+
|
|
411
|
+
def find_print_event(self, events):
|
|
412
|
+
"""Return the first found event if found in the list.
|
|
413
|
+
"""
|
|
414
|
+
for event in events:
|
|
415
|
+
if event.type == pygame.KEYDOWN and event.key == pygame.K_e\
|
|
416
|
+
and pygame.key.get_mods() & pygame.KMOD_CTRL:
|
|
417
|
+
return event
|
|
418
|
+
if (event.type == pygame.MOUSEBUTTONUP and event.button in (1, 2, 3)) or event.type == pygame.FINGERUP:
|
|
419
|
+
pos = get_event_pos(self._window.display_size, event)
|
|
420
|
+
rect = self._window.get_rect()
|
|
421
|
+
if pygame.Rect(rect.width // 2, 0, rect.width // 2, rect.height).collidepoint(pos):
|
|
422
|
+
return event
|
|
423
|
+
if event.type == BUTTONDOWN and event.printer:
|
|
424
|
+
return event
|
|
425
|
+
return None
|
|
426
|
+
|
|
427
|
+
def find_print_status_event(self, events):
|
|
428
|
+
"""Return the first found event if found in the list.
|
|
429
|
+
"""
|
|
430
|
+
for event in events:
|
|
431
|
+
if event.type == PRINTER_TASKS_UPDATED:
|
|
432
|
+
return event
|
|
433
|
+
return None
|
|
434
|
+
|
|
435
|
+
def find_choice_event(self, events):
|
|
436
|
+
"""Return the first found event if found in the list.
|
|
437
|
+
"""
|
|
438
|
+
for event in events:
|
|
439
|
+
if event.type == pygame.KEYDOWN and event.key == pygame.K_LEFT:
|
|
440
|
+
return event
|
|
441
|
+
if event.type == pygame.KEYDOWN and event.key == pygame.K_RIGHT:
|
|
442
|
+
return event
|
|
443
|
+
if (event.type == pygame.MOUSEBUTTONUP and event.button in (1, 2, 3)) or event.type == pygame.FINGERUP:
|
|
444
|
+
pos = get_event_pos(self._window.display_size, event)
|
|
445
|
+
rect = self._window.get_rect()
|
|
446
|
+
if pygame.Rect(0, 0, rect.width // 2, rect.height).collidepoint(pos):
|
|
447
|
+
event.key = pygame.K_LEFT
|
|
448
|
+
else:
|
|
449
|
+
event.key = pygame.K_RIGHT
|
|
450
|
+
return event
|
|
451
|
+
if event.type == BUTTONDOWN:
|
|
452
|
+
if event.capture:
|
|
453
|
+
event.key = pygame.K_LEFT
|
|
454
|
+
else:
|
|
455
|
+
event.key = pygame.K_RIGHT
|
|
456
|
+
return event
|
|
457
|
+
return None
|
|
458
|
+
|
|
459
|
+
def main_loop(self):
|
|
460
|
+
try:
|
|
461
|
+
fps = 40
|
|
462
|
+
clock = pygame.time.Clock()
|
|
463
|
+
self._initialize()
|
|
464
|
+
self._pm.hook.pibooth_startup(cfg=self._config, app=self)
|
|
465
|
+
self._machine.set_state('wait')
|
|
466
|
+
|
|
467
|
+
while True:
|
|
468
|
+
events = list(pygame.event.get())
|
|
469
|
+
|
|
470
|
+
if self.find_quit_event(events):
|
|
471
|
+
break
|
|
472
|
+
|
|
473
|
+
if self.find_fullscreen_event(events):
|
|
474
|
+
self._window.toggle_fullscreen()
|
|
475
|
+
|
|
476
|
+
event = self.find_resize_event(events)
|
|
477
|
+
if event:
|
|
478
|
+
self._window.resize(event.size)
|
|
479
|
+
|
|
480
|
+
if not self._menu and self.find_settings_event(events):
|
|
481
|
+
self.camera.stop_preview()
|
|
482
|
+
self.leds.off()
|
|
483
|
+
self._menu = PiConfigMenu(self._pm, self._config, self, self._window)
|
|
484
|
+
self._menu.show()
|
|
485
|
+
self.leds.blink(on_time=0.1, off_time=1)
|
|
486
|
+
elif self._menu and self._menu.is_shown():
|
|
487
|
+
if not self._menu.is_confirming() and self.find_close_menu_event(events):
|
|
488
|
+
self._menu.show_close_confirmation()
|
|
489
|
+
self._menu.process(events)
|
|
490
|
+
elif self._menu and not self._menu.is_shown():
|
|
491
|
+
self.leds.off()
|
|
492
|
+
self._initialize()
|
|
493
|
+
self._machine.set_state('wait')
|
|
494
|
+
self._menu = None
|
|
495
|
+
else:
|
|
496
|
+
self._machine.process(events)
|
|
497
|
+
|
|
498
|
+
pygame.display.update()
|
|
499
|
+
clock.tick(fps) # Ensure the program will never run at more than <fps> frames per second
|
|
500
|
+
|
|
501
|
+
except Exception as ex:
|
|
502
|
+
LOGGER.error(str(ex), exc_info=True)
|
|
503
|
+
LOGGER.error(get_crash_message())
|
|
504
|
+
finally:
|
|
505
|
+
self._pm.hook.pibooth_cleanup(app=self)
|
|
506
|
+
pygame.quit()
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def main():
|
|
510
|
+
"""Application entry point.
|
|
511
|
+
"""
|
|
512
|
+
if hasattr(multiprocessing, 'set_start_method'):
|
|
513
|
+
# Avoid use 'fork': safely forking a multithreaded process is problematic
|
|
514
|
+
multiprocessing.set_start_method('spawn')
|
|
515
|
+
|
|
516
|
+
parser = argparse.ArgumentParser(usage="%(prog)s [options]", description=pibooth.__doc__)
|
|
517
|
+
|
|
518
|
+
parser.add_argument("config_directory", nargs='?', default="~/.config/pibooth",
|
|
519
|
+
help=u"path to configuration directory (default: %(default)s)")
|
|
520
|
+
|
|
521
|
+
parser.add_argument('--version', action='version', version=pibooth.__version__,
|
|
522
|
+
help=u"show program's version number and exit")
|
|
523
|
+
|
|
524
|
+
parser.add_argument("--config", action='store_true',
|
|
525
|
+
help=u"edit the current configuration and exit")
|
|
526
|
+
|
|
527
|
+
parser.add_argument("--translate", action='store_true',
|
|
528
|
+
help=u"edit the GUI translations and exit")
|
|
529
|
+
|
|
530
|
+
parser.add_argument("--reset", action='store_true',
|
|
531
|
+
help=u"restore the default configuration/translations and exit")
|
|
532
|
+
|
|
533
|
+
parser.add_argument("--nolog", action='store_true', default=False,
|
|
534
|
+
help=u"don't save console output in a file (avoid filling the /tmp directory)")
|
|
535
|
+
|
|
536
|
+
group = parser.add_mutually_exclusive_group()
|
|
537
|
+
group.add_argument("-v", "--verbose", dest='logging', action='store_const', const=logging.DEBUG,
|
|
538
|
+
help=u"report more information about operations", default=logging.INFO)
|
|
539
|
+
group.add_argument("-q", "--quiet", dest='logging', action='store_const', const=logging.WARNING,
|
|
540
|
+
help=u"report only errors and warnings", default=logging.INFO)
|
|
541
|
+
|
|
542
|
+
options = parser.parse_args()
|
|
543
|
+
|
|
544
|
+
if not options.nolog:
|
|
545
|
+
filename = osp.join(tempfile.gettempdir(), 'pibooth.log')
|
|
546
|
+
else:
|
|
547
|
+
filename = None
|
|
548
|
+
configure_logging(options.logging, '[ %(levelname)-8s] %(name)-18s: %(message)s', filename=filename)
|
|
549
|
+
|
|
550
|
+
plugin_manager = create_plugin_manager()
|
|
551
|
+
|
|
552
|
+
# Load the configuration
|
|
553
|
+
config = PiConfigParser(osp.join(options.config_directory, "pibooth.cfg"), plugin_manager, not options.reset)
|
|
554
|
+
|
|
555
|
+
# Register plugins
|
|
556
|
+
plugin_manager.load_all_plugins(config.gettuple('GENERAL', 'plugins', 'path'),
|
|
557
|
+
config.gettuple('GENERAL', 'plugins_disabled', str))
|
|
558
|
+
LOGGER.info("Installed plugins: %s", ", ".join(
|
|
559
|
+
[plugin_manager.get_friendly_name(p) for p in plugin_manager.list_external_plugins()]))
|
|
560
|
+
|
|
561
|
+
# Load the languages
|
|
562
|
+
language.init(config.join_path("translations.cfg"), options.reset)
|
|
563
|
+
|
|
564
|
+
# Update configuration with plugins ones
|
|
565
|
+
plugin_manager.hook.pibooth_configure(cfg=config)
|
|
566
|
+
|
|
567
|
+
# Ensure config files are present in case of first pibooth launch
|
|
568
|
+
if not options.reset:
|
|
569
|
+
if not osp.isfile(config.filename):
|
|
570
|
+
config.save(default=True)
|
|
571
|
+
plugin_manager.hook.pibooth_reset(cfg=config, hard=False)
|
|
572
|
+
|
|
573
|
+
if options.config:
|
|
574
|
+
LOGGER.info("Editing the pibooth configuration...")
|
|
575
|
+
config.edit()
|
|
576
|
+
elif options.translate:
|
|
577
|
+
LOGGER.info("Editing the GUI translations...")
|
|
578
|
+
language.edit()
|
|
579
|
+
elif options.reset:
|
|
580
|
+
config.save(default=True)
|
|
581
|
+
plugin_manager.hook.pibooth_reset(cfg=config, hard=True)
|
|
582
|
+
else:
|
|
583
|
+
LOGGER.info("Starting the photo booth application %s", GPIO_INFO)
|
|
584
|
+
app = PiApplication(config, plugin_manager)
|
|
585
|
+
app.main_loop()
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
if __name__ == '__main__':
|
|
589
|
+
main()
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
from pibooth.utils import LOGGER
|
|
4
|
+
from pibooth.camera.rpi import RpiCamera, get_rpi_camera_proxy
|
|
5
|
+
from pibooth.camera.gphoto import GpCamera, get_gp_camera_proxy
|
|
6
|
+
from pibooth.camera.opencv import CvCamera, get_cv_camera_proxy
|
|
7
|
+
from pibooth.camera.hybrid import HybridRpiCamera, HybridCvCamera
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def close_proxy(rpi_cam_proxy, gp_cam_proxy, cv_cam_proxy):
|
|
11
|
+
"""Close proxy drivers.
|
|
12
|
+
"""
|
|
13
|
+
if rpi_cam_proxy:
|
|
14
|
+
RpiCamera(rpi_cam_proxy).quit()
|
|
15
|
+
if gp_cam_proxy:
|
|
16
|
+
GpCamera(gp_cam_proxy).quit()
|
|
17
|
+
if cv_cam_proxy:
|
|
18
|
+
CvCamera(cv_cam_proxy).quit()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def find_camera():
|
|
22
|
+
"""Initialize the camera depending of the connected one. The priority order
|
|
23
|
+
is chosen in order to have best rendering during preview and to take captures.
|
|
24
|
+
The gPhoto2 camera is first (drivers most restrictive) to avoid connection
|
|
25
|
+
concurence in case of DSLR compatible with OpenCV.
|
|
26
|
+
"""
|
|
27
|
+
rpi_cam_proxy = get_rpi_camera_proxy()
|
|
28
|
+
gp_cam_proxy = get_gp_camera_proxy()
|
|
29
|
+
cv_cam_proxy = get_cv_camera_proxy()
|
|
30
|
+
|
|
31
|
+
if rpi_cam_proxy and gp_cam_proxy:
|
|
32
|
+
LOGGER.info("Configuring hybrid camera (Picamera + gPhoto2) ...")
|
|
33
|
+
close_proxy(None, None, cv_cam_proxy)
|
|
34
|
+
return HybridRpiCamera(rpi_cam_proxy, gp_cam_proxy)
|
|
35
|
+
elif cv_cam_proxy and gp_cam_proxy:
|
|
36
|
+
LOGGER.info("Configuring hybrid camera (OpenCV + gPhoto2) ...")
|
|
37
|
+
close_proxy(rpi_cam_proxy, None, None)
|
|
38
|
+
return HybridCvCamera(cv_cam_proxy, gp_cam_proxy)
|
|
39
|
+
elif gp_cam_proxy:
|
|
40
|
+
LOGGER.info("Configuring gPhoto2 camera ...")
|
|
41
|
+
close_proxy(rpi_cam_proxy, None, cv_cam_proxy)
|
|
42
|
+
return GpCamera(gp_cam_proxy)
|
|
43
|
+
elif rpi_cam_proxy:
|
|
44
|
+
LOGGER.info("Configuring Picamera camera ...")
|
|
45
|
+
close_proxy(None, gp_cam_proxy, cv_cam_proxy)
|
|
46
|
+
return RpiCamera(rpi_cam_proxy)
|
|
47
|
+
elif cv_cam_proxy:
|
|
48
|
+
LOGGER.info("Configuring OpenCV camera ...")
|
|
49
|
+
close_proxy(rpi_cam_proxy, gp_cam_proxy, None)
|
|
50
|
+
return CvCamera(cv_cam_proxy)
|
|
51
|
+
|
|
52
|
+
raise EnvironmentError("Neither Raspberry Pi nor GPhoto2 nor OpenCV camera detected")
|