Source code for pibooth.app

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""Pibooth main module.
"""

import os
import os.path as osp
import logging
import pstats
import cProfile

import pygame
from PIL import Image
from gpiozero import ButtonBoard, LEDBoard

import pibooth
from pibooth import view, fonts, language, evts
from pibooth.counters import Counters
from pibooth.states import StateMachine
from pibooth.tasks import AsyncTasksPool
from pibooth.view import get_scene
from pibooth.utils import LOGGER, PollingTimer, get_crash_message, set_logging_level


def load_last_saved_picture(path):
    """Load the saved picture last time pibooth was started.

    :return: PIL.Image instance and path
    :rtype: tuple
    """
    for name in sorted(os.listdir(path), reverse=True):
        filename = osp.join(path, name)
        if osp.isfile(filename) and osp.splitext(name)[-1] == '.jpg':
            return (Image.open(filename), filename)
    return (None, None)


[docs] class PiboothApplication: """Main class representing the ``pibooth`` software. :py:class:`PiboothApplication` emits the following events consumed by the view: - EVT_BUTTON_CAPTURE - EVT_BUTTON_PRINT - EVT_BUTTON_SETTINGS The following attributes are available for use in plugins (``app`` reprensents the PiboothApplication instance): - ``app.capture_nbr`` (int): number of capture to be done in the current sequence - ``app.capture_date`` (str): date (% Y-%m-%d-%H-%M-%S) of the first capture of the current sequence - ``app.capture_choices`` (tuple): possible choices of captures numbers. - ``app.previous_picture`` (:py:class:`PIL.Image`): picture generated during last sequence - ``app.previous_animated`` (:py:func:`itertools.cycle`): infinite list of picture to display during animation - ``app.previous_picture_file`` (str): file name of the picture generated during last sequence - ``app.count`` (:py:class:`pibooth.counters.Counters`): holder for counter values - ``app.camera`` (:py:class:`pibooth.camera.base.BaseCamera`): camera used - ``app.buttons`` (:py:class:`gpiozero.ButtonBoard`): access to hardware buttons ``capture`` and ``printer`` - ``app.leds`` (:py:class:`gpiozero.LEDBoard`): access to hardware LED ``capture`` and ``printer`` - ``app.printer`` (:py:class:`pibooth.printer.Printer`): printer used """ def __init__(self, config, plugin_manager, window_type='pygame'): self._pm = plugin_manager self._config = config self._tasks = AsyncTasksPool() # Create directories where pictures are saved for savedir in config.gettuple('GENERAL', 'directory', 'path'): if not osp.isdir(savedir): os.makedirs(savedir) # Create window of (width, height) init_size = self._config.gettyped('WINDOW', 'size') init_debug = self._config.getboolean('GENERAL', 'debug') init_color = self._config.gettyped('WINDOW', 'background') init_text_color = self._config.gettyped('WINDOW', 'text_color') if not isinstance(init_color, (tuple, list)): init_color = self._config.getpath('WINDOW', 'background') title = f"Pibooth v{pibooth.__version__}" self._window = view.get_window(window_type, title, init_size, init_color, init_text_color, init_debug) self._window.set_menu(self, self._config, self._pm) self._multipress_timer = PollingTimer(config.getfloat('CONTROLS', 'multi_press_delay'), False) # Define states of the application self._machine = StateMachine(self._pm, self._config, self, self._window) self._pm.hook.pibooth_setup_states(cfg=self._config, win=self._window, machine=self._machine) # --------------------------------------------------------------------- # Variables shared with plugins # Change them may break plugins compatibility self.capture_nbr = None self.capture_date = None self.capture_choices = (4, 1) self.previous_animated = None self.previous_picture, self.previous_picture_file = load_last_saved_picture( config.gettuple('GENERAL', 'directory', 'path')[0]) self.count = Counters(self._config.join_path("counters.pickle"), taken=0, printed=0, forgotten=0, remaining_duplicates=self._config.getint('PRINTER', 'max_duplicates')) self.camera = self._pm.hook.pibooth_setup_camera(cfg=self._config) self.buttons = ButtonBoard(capture="BOARD" + config.get('CONTROLS', 'capture_btn_pin'), printer="BOARD" + config.get('CONTROLS', 'print_btn_pin'), hold_time=config.getfloat('CONTROLS', 'debounce_delay'), pull_up=True) self.buttons.capture.when_held = self._on_button_capture_held self.buttons.printer.when_held = self._on_button_printer_held self.leds = LEDBoard(capture="BOARD" + config.get('CONTROLS', 'capture_led_pin'), printer="BOARD" + config.get('CONTROLS', 'print_led_pin')) self.printer = self._pm.hook.pibooth_setup_printer(cfg=self._config) self.printer.count = self.count # --------------------------------------------------------------------- def _initialize(self): """Restore the application with initial parameters defined in the configuration file. Only parameters that can be changed at runtime are restored. """ # Handle the language configuration language.set_current(self._config.get('GENERAL', 'language')) if self._config.get('WINDOW', 'font').endswith('.ttf') or self._config.get('WINDOW', 'font').endswith('.otf'): fonts.CURRENT = fonts.get_filename(self._config.getpath('WINDOW', 'font')) else: fonts.CURRENT = fonts.get_filename(self._config.get('WINDOW', 'font')) # Set the captures choices choices = self._config.gettuple('PICTURE', 'captures', int) for chx in choices: if chx not in [1, 2, 3, 4]: LOGGER.warning("Invalid captures number '%s' in config, fallback to '%s'", chx, self.capture_choices) choices = self.capture_choices break self.capture_choices = choices # Handle autostart of the application self._config.handle_autostart() self._window.arrow_location = self._config.get('WINDOW', 'arrows') self._window.arrow_offset = self._config.getint('WINDOW', 'arrows_x_offset') self._window.text_color = self._config.gettyped('WINDOW', 'text_color') # Handle window size size = self._config.gettyped('WINDOW', 'size') if isinstance(size, str) and size.lower() == 'fullscreen': if not self._window.is_fullscreen: self._window.toggle_fullscreen() else: if self._window.is_fullscreen: self._window.toggle_fullscreen() self._window.debug = self._config.getboolean('GENERAL', 'debug') # Handle debug mode if not self._config.getboolean('GENERAL', 'debug'): set_logging_level() # Restore default level self._machine.add_failsafe_state('failsafe', get_scene(self._window.type, 'failsafe')) else: set_logging_level(logging.DEBUG) self._machine.remove_state('failsafe') # Reset the print counter (in case of max_pages is reached) self.printer.max_pages = self._config.getint('PRINTER', 'max_pages') def _on_button_capture_held(self): """Called when the capture button is pressed. """ if all(self.buttons.value): self.buttons.capture.hold_repeat = True if self._multipress_timer.elapsed() == 0: self._multipress_timer.start() if self._multipress_timer.is_timeout(): # Capture was held while printer was pressed self.buttons.capture.hold_repeat = False self._multipress_timer.reset() LOGGER.debug("Event triggered: EVT_BUTTON_SETTINGS") evts.post(evts.EVT_BUTTON_SETTINGS, buttons=self.buttons, leds=self.leds) else: # Capture was held but printer not pressed self.buttons.capture.hold_repeat = False self._multipress_timer.reset() LOGGER.debug("Event triggered: EVT_BUTTON_CAPTURE") evts.post(evts.EVT_BUTTON_CAPTURE, buttons=self.buttons, leds=self.leds) def _on_button_printer_held(self): """Called when the printer button is pressed. """ if all(self.buttons.value): # Printer was held while capture was pressed # but don't do anything here, let capture_held handle it instead pass else: # Printer was held but capture not pressed LOGGER.debug("Event triggered: EVT_BUTTON_PRINT") evts.post(evts.EVT_BUTTON_PRINT, buttons=self.buttons, leds=self.leds) @property def picture_filename(self): """Return the final picture file name. """ if not self.capture_date: raise EnvironmentError("The 'capture_date' attribute is not set yet") return f"{self.capture_date}_pibooth.jpg"
[docs] def enable_plugin(self, plugin): """Enable plugin with given name. The "configure" and "startup" hooks will be called if never done before. :param plugin: plugin to disable :type plugin: object """ if not self._pm.is_registered(plugin): self._pm.register(plugin) LOGGER.debug("Plugin '%s' enable", self._pm.get_name(plugin)) # Because no hook is called for plugins disabled at pibooth startup, need to # ensure that mandatory hooks have been called when enabling a plugin if 'pibooth_configure' not in self._pm.get_calls_history(plugin): hook = self._pm.subset_hook_caller_for_plugin('pibooth_configure', plugin) hook(cfg=self._config) if 'pibooth_startup' not in self._pm.get_calls_history(plugin): hook = self._pm.subset_hook_caller_for_plugin('pibooth_startup', plugin) hook(cfg=self._config, app=self)
[docs] def disable_plugin(self, plugin): """Disable plugin with given name. :param plugin: plugin to disable :type plugin: object """ if self._pm.is_registered(plugin): LOGGER.debug("Plugin '%s' disabled", self._pm.get_name(plugin)) self._pm.unregister(plugin)
[docs] def update(self, events): """Update application and call plugins according to Pygame events. Better to call it in the main thread to avoid plugin thread-safe issues. :param events: list of events to process. :type events: list """ if evts.find_event(events, evts.EVT_PIBOOTH_SETTINGS): if self._window.is_menu_shown: # Settings menu is opened self.camera.stop_preview() self.leds.off() self.leds.blink(on_time=0.1, off_time=1) elif not self._window.is_menu_shown: # Settings menu is closed self.leds.off() self._initialize() self._machine.set_state('wait') elif not self._window.is_menu_shown: self._machine.process(events)
[docs] def exec(self, enable_profiler=False): """Start application. """ if enable_profiler: profiler = cProfile.Profile() try: self._initialize() self._pm.hook.pibooth_startup(cfg=self._config, app=self) self._machine.set_state('wait') if enable_profiler: profiler.enable() self._window.eventloop(self.update) except KeyboardInterrupt: print() except Exception as ex: LOGGER.error(str(ex), exc_info=True) LOGGER.error(get_crash_message()) finally: if enable_profiler: stats = pstats.Stats(profiler).sort_stats('cumtime') stats.print_stats() profiler.disable() self._pm.hook.pibooth_cleanup(app=self) self._tasks.quit() pygame.quit()