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.
Files changed (52) hide show
  1. pibooth/__init__.py +15 -0
  2. pibooth/booth.py +589 -0
  3. pibooth/camera/__init__.py +52 -0
  4. pibooth/camera/base.py +134 -0
  5. pibooth/camera/gphoto.py +311 -0
  6. pibooth/camera/hybrid.py +89 -0
  7. pibooth/camera/opencv.py +239 -0
  8. pibooth/camera/rpi.py +169 -0
  9. pibooth/config/__init__.py +4 -0
  10. pibooth/config/menu.py +424 -0
  11. pibooth/config/parser.py +533 -0
  12. pibooth/counters.py +66 -0
  13. pibooth/fonts/Amatic-Bold.ttf +0 -0
  14. pibooth/fonts/AmaticSC-Regular.ttf +0 -0
  15. pibooth/fonts/DancingScript-Bold.ttf +0 -0
  16. pibooth/fonts/DancingScript-Regular.ttf +0 -0
  17. pibooth/fonts/Monoid-Bold.ttf +0 -0
  18. pibooth/fonts/Monoid-Regular.ttf +0 -0
  19. pibooth/fonts/Monoid-Retina.ttf +0 -0
  20. pibooth/fonts/Roboto-BoldItalic.ttf +0 -0
  21. pibooth/fonts/Roboto-LightItalic.ttf +0 -0
  22. pibooth/fonts/__init__.py +111 -0
  23. pibooth/language.py +278 -0
  24. pibooth/pictures/__init__.py +192 -0
  25. pibooth/pictures/factory.py +528 -0
  26. pibooth/pictures/pool.py +36 -0
  27. pibooth/pictures/sizing.py +114 -0
  28. pibooth/plugins/__init__.py +152 -0
  29. pibooth/plugins/camera_plugin.py +116 -0
  30. pibooth/plugins/hookspecs.py +512 -0
  31. pibooth/plugins/lights_plugin.py +72 -0
  32. pibooth/plugins/picture_plugin.py +159 -0
  33. pibooth/plugins/printer_plugin.py +67 -0
  34. pibooth/plugins/view_plugin.py +187 -0
  35. pibooth/printer.py +135 -0
  36. pibooth/scripts/__init__.py +0 -0
  37. pibooth/scripts/count.py +45 -0
  38. pibooth/scripts/diagnostic.py +223 -0
  39. pibooth/scripts/fonts.py +38 -0
  40. pibooth/scripts/printer.py +78 -0
  41. pibooth/scripts/regenerate.py +93 -0
  42. pibooth/states.py +105 -0
  43. pibooth/utils.py +296 -0
  44. pibooth/view/__init__.py +3 -0
  45. pibooth/view/background.py +681 -0
  46. pibooth/view/window.py +392 -0
  47. pibooth_ceeeeb-2.0.8.2.dist-info/LICENSE +22 -0
  48. pibooth_ceeeeb-2.0.8.2.dist-info/METADATA +73 -0
  49. pibooth_ceeeeb-2.0.8.2.dist-info/RECORD +52 -0
  50. pibooth_ceeeeb-2.0.8.2.dist-info/WHEEL +5 -0
  51. pibooth_ceeeeb-2.0.8.2.dist-info/entry_points.txt +7 -0
  52. 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")