コード例 #1
0
ファイル: mc.py プロジェクト: elliotstarks/mpf-mc
    def __init__(self, options, config, machine_path,
                 thread_stopper=None, **kwargs):

        self.log = logging.getLogger('mpfmc')
        self.log.info("Mission Pinball Framework Media Controller v%s", __version__)
        self.log.info("Mission Pinball Framework Game Engine v%s", __mpfversion__)

        if (__version__.split('.')[0] != __mpfversion__.split('.')[0] or
                __version__.split('.')[1] != __mpfversion__.split('.')[1]):

            self.log.error("MPF MC and MPF Game engines must be same "
                           "major.minor versions. You have MPF v{} and MPF-MC"
                           " v{}".format(__mpfversion__, __version__))

            raise ValueError("MPF MC and MPF Game engines must be same "
                           "major.minor versions. You have MPF v{} and MPF-MC"
                           " v{}".format(__mpfversion__, __version__))

        super().__init__(**kwargs)

        self.options = options
        self.machine_config = config
        self.log.info("Machine path: %s", machine_path)
        self.machine_path = machine_path
        self.clock = Clock
        # pylint: disable-msg=protected-access
        self.log.info("Starting clock at %sHz", Clock._max_fps)
        self._boot_holds = set()
        self.mpf_path = os.path.dirname(mpf.__file__)
        self.modes = CaseInsensitiveDict()
        self.player_list = list()
        self.player = None
        self.num_players = 0
        self.bcp_client_connected = False
        self.placeholder_manager = McPlaceholderManager(self)
        self.settings = McSettingsController(self)

        self.animation_configs = dict()
        self.active_slides = dict()
        self.scriptlets = list()

        self.register_boot_hold('init')
        self.displays = CaseInsensitiveDict()
        self.machine_vars = CaseInsensitiveDict()
        self.machine_var_monitor = False
        self.monitors = dict()
        self.targets = dict()
        """Dict which contains all the active slide frames in the machine that
        a slide can target. Will always contain an entry called 'default'
        which will be used if a slide doesn't specify targeting.
        """

        self.keyboard = None
        self.physical_dmds = []
        self.physical_rgb_dmds = []
        self.crash_queue = queue.Queue()
        self.ticks = 0
        self.start_time = 0
        self.is_init_done = False

        if thread_stopper:
            self.thread_stopper = thread_stopper
        else:
            self.thread_stopper = threading.Event()

        # Core components
        self.config_validator = ConfigValidator(self)
        self.events = EventManager(self)
        self.mode_controller = ModeController(self)
        create_config_collections(self, self.machine_config['mpf-mc']['config_collections'])
        ConfigValidator.load_config_spec()

        self.config_processor = ConfigProcessor(self)
        self.transition_manager = TransitionManager(self)

        self._set_machine_path()

        self._load_font_paths()

        # Initialize the sound system (must be done prior to creating the AssetManager).
        # If the sound system is not available, do not load any other sound-related modules.
        if SoundSystem is None:
            self.sound_system = None
        else:
            self.sound_system = SoundSystem(self)

        self.asset_manager = ThreadedAssetManager(self)
        self.bcp_processor = BcpProcessor(self)

        # Asset classes
        ImageAsset.initialize(self)
        VideoAsset.initialize(self)

        self._initialise_sound_system()

        self.clock.schedule_interval(self._check_crash_queue, 1)

        self.events.add_handler("client_connected", self._create_physical_dmds)
        self.events.add_handler("player_turn_start", self.player_start_turn)
コード例 #2
0
ファイル: mc.py プロジェクト: elliotstarks/mpf-mc
class MpfMc(App):

    """Kivy app for the mpf media controller."""

    def __init__(self, options, config, machine_path,
                 thread_stopper=None, **kwargs):

        self.log = logging.getLogger('mpfmc')
        self.log.info("Mission Pinball Framework Media Controller v%s", __version__)
        self.log.info("Mission Pinball Framework Game Engine v%s", __mpfversion__)

        if (__version__.split('.')[0] != __mpfversion__.split('.')[0] or
                __version__.split('.')[1] != __mpfversion__.split('.')[1]):

            self.log.error("MPF MC and MPF Game engines must be same "
                           "major.minor versions. You have MPF v{} and MPF-MC"
                           " v{}".format(__mpfversion__, __version__))

            raise ValueError("MPF MC and MPF Game engines must be same "
                           "major.minor versions. You have MPF v{} and MPF-MC"
                           " v{}".format(__mpfversion__, __version__))

        super().__init__(**kwargs)

        self.options = options
        self.machine_config = config
        self.log.info("Machine path: %s", machine_path)
        self.machine_path = machine_path
        self.clock = Clock
        # pylint: disable-msg=protected-access
        self.log.info("Starting clock at %sHz", Clock._max_fps)
        self._boot_holds = set()
        self.mpf_path = os.path.dirname(mpf.__file__)
        self.modes = CaseInsensitiveDict()
        self.player_list = list()
        self.player = None
        self.num_players = 0
        self.bcp_client_connected = False
        self.placeholder_manager = McPlaceholderManager(self)
        self.settings = McSettingsController(self)

        self.animation_configs = dict()
        self.active_slides = dict()
        self.scriptlets = list()

        self.register_boot_hold('init')
        self.displays = CaseInsensitiveDict()
        self.machine_vars = CaseInsensitiveDict()
        self.machine_var_monitor = False
        self.monitors = dict()
        self.targets = dict()
        """Dict which contains all the active slide frames in the machine that
        a slide can target. Will always contain an entry called 'default'
        which will be used if a slide doesn't specify targeting.
        """

        self.keyboard = None
        self.physical_dmds = []
        self.physical_rgb_dmds = []
        self.crash_queue = queue.Queue()
        self.ticks = 0
        self.start_time = 0
        self.is_init_done = False

        if thread_stopper:
            self.thread_stopper = thread_stopper
        else:
            self.thread_stopper = threading.Event()

        # Core components
        self.config_validator = ConfigValidator(self)
        self.events = EventManager(self)
        self.mode_controller = ModeController(self)
        create_config_collections(self, self.machine_config['mpf-mc']['config_collections'])
        ConfigValidator.load_config_spec()

        self.config_processor = ConfigProcessor(self)
        self.transition_manager = TransitionManager(self)

        self._set_machine_path()

        self._load_font_paths()

        # Initialize the sound system (must be done prior to creating the AssetManager).
        # If the sound system is not available, do not load any other sound-related modules.
        if SoundSystem is None:
            self.sound_system = None
        else:
            self.sound_system = SoundSystem(self)

        self.asset_manager = ThreadedAssetManager(self)
        self.bcp_processor = BcpProcessor(self)

        # Asset classes
        ImageAsset.initialize(self)
        VideoAsset.initialize(self)

        self._initialise_sound_system()

        self.clock.schedule_interval(self._check_crash_queue, 1)

        self.events.add_handler("client_connected", self._create_physical_dmds)
        self.events.add_handler("player_turn_start", self.player_start_turn)

    def _create_physical_dmds(self, **kwargs):
        self.create_physical_dmds()
        self.create_physical_rgb_dmds()

    def _load_font_paths(self):
        # Add local machine fonts path
        if os.path.isdir(os.path.join(self.machine_path,
                self.machine_config['mpf-mc']['paths']['fonts'])):

            resource_add_path(os.path.join(self.machine_path,
                self.machine_config['mpf-mc']['paths']['fonts']))

        # Add mpfmc fonts path
        resource_add_path(os.path.join(os.path.dirname(mpfmc.__file__),
                                       'fonts'))

    def _initialise_sound_system(self):
        # Only initialize sound assets if sound system is loaded and enabled
        if self.sound_system is not None and self.sound_system.enabled:
            SoundAsset.extensions = tuple(
                self.sound_system.audio_interface.supported_extensions())
            SoundAsset.initialize(self)
        else:
            # If the sound system is not loaded or enabled, remove the
            # sound_player and track_player from the list of config_player modules to setup
            del self.machine_config['mpf-mc']['config_players']['sound']
            del self.machine_config['mpf-mc']['config_players']['track']

    def get_system_config(self):
        return self.machine_config['mpf-mc']

    def validate_machine_config_section(self, section):
        if section not in ConfigValidator.config_spec:
            return

        if section not in self.machine_config:
            self.config[section] = dict()

        self.machine_config[section] = self.config_validator.validate_config(
            section, self.machine_config[section], section)

    def get_config(self):
        return self.machine_config

    def _set_machine_path(self):
        self.log.debug("Machine path: %s", self.machine_path)

        # Add the machine folder to sys.path so we can import modules from it
        sys.path.insert(0, self.machine_path)

    def register_boot_hold(self, hold):
        # print('registering boot hold', hold)
        self._boot_holds.add(hold)

    def clear_boot_hold(self, hold):
        # print('clearing boot hold', hold)
        if self.is_init_done:
            self.log.warn("clear boot hold after init done")
            return
        self._boot_holds.remove(hold)
        if not self._boot_holds:
            self.init_done()

    def _register_config_players(self):
        # todo move this to config_player module

        for name, module in self.machine_config['mpf-mc'][
                'config_players'].items():
            imported_module = importlib.import_module(module)
            setattr(self, '{}_player'.format(name),
                    imported_module.mc_player_cls(self))

    def displays_initialized(self, *args):
        del args
        from mpfmc.uix.window import Window
        Window.initialize(self)
        self.events.post('displays_initialized')
        '''event: displays_initialized
        desc: Posted as soon as MPF MC displays have been initialized.

        Note that this event is used as part of the internal MPF-MC startup
        process. In some cases it will be posted *before* the slide_player is
        ready, meaning that you *CANNOT* use this event to post slides or play
        sounds.

        Instead, use the *mc_ready* event, which is posted as early as possible
        once the slide player and sound players are setup.

        Note that this event is generated by the media controller and does not
        exist on the MPF side of things.

        Also note that if you're using a media controller other than the MPF-MC
        (such as the Unity 3D backbox controller), then this event won't exist.

        '''
        self.events.process_event_queue()
        self._init()

    def create_physical_dmds(self):
        """Create physical DMDs."""
        if 'physical_dmds' in self.machine_config:
            for name, config in self.machine_config['physical_dmds'].items():
                dmd = PhysicalDmd(self, name, config)
                self.physical_dmds.append(dmd)

    def create_physical_rgb_dmds(self):
        """Create physical RBG DMDs."""
        if 'physical_rgb_dmds' in self.machine_config:
            for name, config in self.machine_config['physical_rgb_dmds'].items():
                dmd = PhysicalRgbDmd(self, name, config)
                self.physical_rgb_dmds.append(dmd)

    def _init(self):
        # Since the window is so critical in Kivy, we can't continue the
        # boot process until the window is setup, and we can't set the
        # window up until the displays are initialized.

        self._register_config_players()
        self.events.post("init_phase_1")
        # no events docstring as this event is also in mpf
        self.events.process_event_queue()
        self.events.post("mc_ready")
        '''event: mc_ready
        desc: Posted when the MPF-MC is available to start showing slides and
        playing sounds.

        Note that this event does not mean the MC is done loading. Instead it's
        posted at the earliest possible moment that the core MC components are
        available, meaning you can trigger "boot" slides from this event (which
        could in turn be used to show asset loading status, boot progress,
        etc.)

        If you want to show slides that require images or video loaded from
        disk, use the event "init_done" instead which is posted once all the
        assets set to "preload" have been loaded.
        '''

        self.events.process_event_queue()
        self.events.post("init_phase_2")
        # no events docstring as this event is also in mpf
        self.events.process_event_queue()
        self.events.post("init_phase_3")
        # no events docstring as this event is also in mpf
        self.events.process_event_queue()
        self._load_scriptlets()
        self.events.post("init_phase_4")
        # no events docstring as this event is also in mpf
        self.events.process_event_queue()
        self.events.post("init_phase_5")
        # no events docstring as this event is also in mpf
        self.events.process_event_queue()
        self.clear_boot_hold('init')

    def init_done(self):
        self.is_init_done = True
        ConfigValidator.unload_config_spec()
        self.events.post("init_done")
        # no events docstring as this event is also in mpf
        self.events.process_event_queue()

        self.reset()

    def build(self):
        self.start_time = time.time()
        self.ticks = 0
        self.clock.schedule_interval(self.tick, 0)

    def on_stop(self):
        self.log.info("Stopping...")
        self.thread_stopper.set()

        self.events.post("shutdown")
        self.events.process_event_queue()

        try:
            self.log.info("Loop rate %s Hz", round(self.ticks / (time.time() - self.start_time), 2))
        except ZeroDivisionError:
            pass

    def reset(self, **kwargs):
        del kwargs
        self.player = None
        self.player_list = list()

        self.events.post('mc_reset_phase_1')
        '''event: mc_reset_phase_1
        desc: Posted on the MPF-MC only (e.g. not in MPF). This event is used
        internally as part of the MPF-MC reset process.
        '''
        self.events.process_event_queue()
        self.events.post('mc_reset_phase_2')
        '''event: mc_reset_phase_2
        desc: Posted on the MPF-MC only (e.g. not in MPF). This event is used
        internally as part of the MPF-MC reset process.
        '''
        self.events.process_event_queue()
        self.events.post('mc_reset_phase_3')
        '''event: mc_reset_phase_3
        desc: Posted on the MPF-MC only (e.g. not in MPF). This event is used
        internally as part of the MPF-MC reset process.
        '''
        self.events.process_event_queue()

    def game_start(self, **kargs):
        self.player = None
        self.player_list = list()
        self.num_players = 0
        self.events.post('game_started', **kargs)
        # no events docstring as this event is also in mpf

    def game_end(self, **kwargs):
        self.player = None
        self.events.post('game_ended', **kwargs)
        # no events docstring as this event is also in mpf

    def add_player(self, player_num):
        if player_num > len(self.player_list):
            player = Player(self, len(self.player_list))
            self.player_list.append(player)

            self.events.post('player_add_success', player=player,
                             num=player_num)
            # no events docstring as this event is also in mpf

    def update_player_var(self, name, value, player_num):
        try:
            self.player_list[int(player_num) - 1][name] = value
        except (IndexError, KeyError):
            pass

    def player_start_turn(self, number, **kwargs):
        del kwargs
        if ((self.player and self.player.number != number) or
                not self.player):

            try:
                self.player = self.player_list[int(number) - 1]
                self.events.post('player_turn_start', number=number,
                                 player=self.player)
            except IndexError:
                self.log.error('Received player turn start for player %s, but '
                               'only %s player(s) exist',
                               number, len(self.player_list))

    def create_machine_var(self, name, value):
        """Same as set_machine_var."""
        self.set_machine_var(name, value)

    def set_machine_var(self, name, value):
        """Set machine var and send it via BCP to MPF."""
        if hasattr(self, "bcp_processor") and self.bcp_processor.connected:
            self.bcp_processor.send_machine_var_to_mpf(name, value)

    def receive_machine_var_update(self, name, value, change, prev_value):
        """Update a machine var received via BCP."""
        self.machine_vars[name] = value

        if change:
            self.log.debug("Setting machine_var '%s' to: %s, (prior: %s, "
                           "change: %s)", name, value, prev_value,
                           change)
            self.events.post('machine_var_' + name,
                             value=value,
                             prev_value=prev_value,
                             change=change)
            # no events docstring as this event is also in mpf

        if self.machine_var_monitor:
            for callback in self.monitors['machine_var']:
                callback(name=name, value=self.vars[name],
                         prev_value=prev_value, change=change)

    def tick(self, time):
        del time
        self.ticks += 1
        self.events.process_event_queue()

    def _load_scriptlets(self):
        if 'mc_scriptlets' in self.machine_config:
            self.machine_config['mc_scriptlets'] = (
                self.machine_config['mc_scriptlets'].split(' '))

            self.log.debug("Loading scriptlets...")

            for scriptlet in self.machine_config['mc_scriptlets']:

                self.log.debug("Loading '%s' scriptlet", scriptlet)

                scriptlet_obj = Util.string_to_class(
                    self.machine_config['mpf-mc']['paths']['scriptlets'] +
                    "." + scriptlet)(mc=self,
                                     name=scriptlet.split('.')[1])

                self.scriptlets.append(scriptlet_obj)

    def _check_crash_queue(self, time):
        del time
        try:
            crash = self.crash_queue.get(block=False)
        except queue.Empty:
            pass
        else:
            self.log.info("Shutting down due to child thread crash")
            self.log.info("Crash details: %s", crash)
            self.stop()

    def register_monitor(self, monitor_class, monitor):
        """Registers a monitor.

        Args:
            monitor_class: String name of the monitor class for this monitor
                that's being registered.
            monitor: String name of the monitor.

        MPF uses monitors to allow components to monitor certain internal
        elements of MPF.

        For example, a player variable monitor could be setup to be notified of
        any changes to a player variable, or a switch monitor could be used to
        allow a plugin to be notified of any changes to any switches.

        The MachineController's list of registered monitors doesn't actually
        do anything. Rather it's a dictionary of sets which the monitors
        themselves can reference when they need to do something. We just needed
        a central registry of monitors.

        """
        if monitor_class not in self.monitors:
            self.monitors[monitor_class] = set()

        self.monitors[monitor_class].add(monitor)

    def post_mc_native_event(self, event, **kwargs):
        if self.bcp_processor.enabled and self.bcp_client_connected:
            self.bcp_processor.send('trigger', name=event, **kwargs)

        self.events.post(event, **kwargs)
コード例 #3
0
    def __init__(self, mpf_path, machine_path, args, **kwargs):
        del mpf_path
        del machine_path
        del args
        super().__init__(**kwargs)

        self.config_validator = ConfigValidator(self, True, False)
        self.mpf_config_processor = MpfConfigProcessor(self.config_validator)
        files = [
            os.path.join(mpfmc.__path__[0],
                         'tools/interactive_mc/imcconfig.yaml')
        ]
        self.machine_config = self.mpf_config_processor.load_config_files_with_cache(
            files, "machine")
        self.machine_config['mpf'] = dict()
        self.machine_config['mpf']['allow_invalid_config_sections'] = True
        self.config = self.machine_config
        self._initialized = False
        self.options = dict(bcp=True, production=False)
        self.clock = ClockBase(self)

        # needed for bcp
        self.settings = Settings()
        self.machine_vars = {}
        self.modes = []

        self.events = EventManager(self)
        self.mode_controller = ModeController(self)
        self.bcp = Bcp(self)
        self.slide_player = MpfSlidePlayer(self)
        self.slide_player.instances['imc'] = dict()

        self.clock.loop.run_until_complete(
            self.events.post_queue_async("init_phase_1"))
        self.events.process_event_queue()
        self.clock.loop.run_until_complete(
            self.events.post_queue_async("init_phase_2"))
        self.events.process_event_queue()
        self.clock.loop.run_until_complete(
            self.events.post_queue_async("init_phase_3"))
        self.events.process_event_queue()
        self.clock.loop.run_until_complete(
            self.events.post_queue_async("init_phase_4"))
        self.events.process_event_queue()
        self.clock.loop.run_until_complete(
            self.events.post_queue_async("init_phase_5"))

        self.sm = ScreenManager()
        self.slide_screen = Screen(name="Slide Player")
        self.widget_screen = Screen(name="Widget Player")
        self.sm.add_widget(self.slide_screen)
        self.sm.add_widget(self.widget_screen)
        self.slide_player_code = YamlCodeInput(lexer=YamlLexer(), tab_width=4)
        self.slide_player_code.bind(on_triple_tap=self.send_slide_to_mc)

        self.slide_player_code.text = '''my_test_slide:
    widgets:
      - type: text
        text: iMC
        color: red
      - type: line
        points: 1, 1, 1, 32, 128, 32, 128, 1, 1, 1
        color: lime
      - type: rectangle
        width: 50
        height: 20
        color: yellow
'''

        self.send_button = Button(text='Send',
                                  size=(150, 60),
                                  size_hint=(None, None),
                                  background_normal='',
                                  background_color=(0, .6, 0, 1),
                                  pos=(0, 1),
                                  pos_hint={
                                      'top': 0.1,
                                      'right': 0.95
                                  })

        self.send_button.bind(on_press=self.send_slide_to_mc)

        self.slide_screen.add_widget(self.slide_player_code)
        self.slide_screen.add_widget(self.send_button)

        self.slide_player.register_player_events(dict())
コード例 #4
0
class InteractiveMc(App):
    def __init__(self, mpf_path, machine_path, args, **kwargs):
        del mpf_path
        del machine_path
        del args
        super().__init__(**kwargs)

        self.config_validator = ConfigValidator(self, True, False)
        self.mpf_config_processor = MpfConfigProcessor(self.config_validator)
        files = [
            os.path.join(mpfmc.__path__[0],
                         'tools/interactive_mc/imcconfig.yaml')
        ]
        self.machine_config = self.mpf_config_processor.load_config_files_with_cache(
            files, "machine")
        self.machine_config['mpf'] = dict()
        self.machine_config['mpf']['allow_invalid_config_sections'] = True
        self.config = self.machine_config
        self._initialized = False
        self.options = dict(bcp=True, production=False)
        self.clock = ClockBase(self)

        # needed for bcp
        self.settings = Settings()
        self.machine_vars = {}
        self.modes = []

        self.events = EventManager(self)
        self.mode_controller = ModeController(self)
        self.bcp = Bcp(self)
        self.slide_player = MpfSlidePlayer(self)
        self.slide_player.instances['imc'] = dict()

        self.clock.loop.run_until_complete(
            self.events.post_queue_async("init_phase_1"))
        self.events.process_event_queue()
        self.clock.loop.run_until_complete(
            self.events.post_queue_async("init_phase_2"))
        self.events.process_event_queue()
        self.clock.loop.run_until_complete(
            self.events.post_queue_async("init_phase_3"))
        self.events.process_event_queue()
        self.clock.loop.run_until_complete(
            self.events.post_queue_async("init_phase_4"))
        self.events.process_event_queue()
        self.clock.loop.run_until_complete(
            self.events.post_queue_async("init_phase_5"))

        self.sm = ScreenManager()
        self.slide_screen = Screen(name="Slide Player")
        self.widget_screen = Screen(name="Widget Player")
        self.sm.add_widget(self.slide_screen)
        self.sm.add_widget(self.widget_screen)
        self.slide_player_code = YamlCodeInput(lexer=YamlLexer(), tab_width=4)
        self.slide_player_code.bind(on_triple_tap=self.send_slide_to_mc)

        self.slide_player_code.text = '''my_test_slide:
    widgets:
      - type: text
        text: iMC
        color: red
      - type: line
        points: 1, 1, 1, 32, 128, 32, 128, 1, 1, 1
        color: lime
      - type: rectangle
        width: 50
        height: 20
        color: yellow
'''

        self.send_button = Button(text='Send',
                                  size=(150, 60),
                                  size_hint=(None, None),
                                  background_normal='',
                                  background_color=(0, .6, 0, 1),
                                  pos=(0, 1),
                                  pos_hint={
                                      'top': 0.1,
                                      'right': 0.95
                                  })

        self.send_button.bind(on_press=self.send_slide_to_mc)

        self.slide_screen.add_widget(self.slide_player_code)
        self.slide_screen.add_widget(self.send_button)

        self.slide_player.register_player_events(dict())

    def register_monitor(self, monitor_class, monitor):
        pass

    def build(self):
        return self.sm

    def send_slide_to_mc(self, value):
        del value

        try:
            settings = YamlInterface.process(self.slide_player_code.text)
        except Exception as e:
            msg = str(e).replace('"', '\n')
            Popup(title='Error in your config',
                  content=Label(text=msg, size=(750, 350)),
                  size_hint=(None, None),
                  size=(Window.width, 400)).open()
            return

        try:
            settings = (self.slide_player.validate_config_entry(
                settings, 'slides'))
        except Exception as e:
            msg = str(e).replace('"', '\n')
            Popup(title='Error in your config',
                  content=Label(text=msg, size=(750, 350)),
                  size_hint=(None, None),
                  size=(Window.width, 400)).open()
            return

        if self._initialized:
            self.slide_player.clear_context('imc')
        else:
            self._initialized = True
        self.slide_player.play(settings, 'imc', 100)
        self.clock.loop.run_until_complete(
            asyncio.sleep(.1, loop=self.clock.loop))

    def set_machine_var(self, name, value):
        pass
コード例 #5
0
    def __init__(self, options, config: MpfMcConfig, thread_stopper=None):

        self.log = logging.getLogger('mpfmc')
        self.log.info("Mission Pinball Framework Media Controller v%s",
                      __version__)
        self.log.info("Mission Pinball Framework Game Engine v%s",
                      __mpfversion__)

        if (__version__.split('.')[0] != __mpfversion__.split('.')[0]
                or __version__.split('.')[1] != __mpfversion__.split('.')[1]):

            self.log.error(
                "MPF MC and MPF Game engines must be same "
                "major.minor versions. You have MPF v%s and MPF-MC"
                " v%s", __mpfversion__, __version__)

            raise ValueError(
                "MPF MC and MPF Game engines must be same "
                "major.minor versions. You have MPF v{} and MPF-MC"
                " v{}".format(__mpfversion__, __version__))

        super().__init__()

        self.options = options
        self.machine_path = config.get_machine_path()
        self.log.info("Machine path: %s", self.machine_path)

        # load machine into path to load modules
        if self.machine_path not in sys.path:
            sys.path.append(self.machine_path)
        self.mc_config = config
        self.config_validator = ConfigValidator(self, config.get_config_spec())
        self.machine_config = self.mc_config.get_machine_config()
        self.config = self.machine_config

        self.clock = Clock
        # pylint: disable-msg=protected-access
        self.log.info("Starting clock at %sHz", Clock._max_fps)
        self._boot_holds = set()
        self.is_init_done = threading.Event()
        self.mpf_path = os.path.dirname(mpf.__file__)
        self.modes = CaseInsensitiveDict()
        self.player_list = list()
        self.player = None
        self.num_players = 0
        self.bcp_client_connected = False
        self.placeholder_manager = McPlaceholderManager(self)
        self.settings = McSettingsController(self)

        self.animation_configs = dict()
        self.active_slides = dict()
        self.custom_code = list()

        self.register_boot_hold('init')
        self.displays = DeviceCollection(self, "displays", "displays")
        self.machine_vars = CaseInsensitiveDict()
        self.machine_var_monitor = False
        self.monitors = dict()
        self.targets = dict()
        """Dict which contains all the active slide frames in the machine that
        a slide can target. Will always contain an entry called 'default'
        which will be used if a slide doesn't specify targeting.
        """

        self.keyboard = None
        self.dmds = []
        self.rgb_dmds = []
        self.crash_queue = queue.Queue()
        self.ticks = 0
        self.start_time = 0
        self.debug_refs = []

        MYPY = False  # NOQA
        if MYPY:  # pragma: no cover
            self.videos = None  # type: Dict[str, VideoAsset]

        if thread_stopper:
            self.thread_stopper = thread_stopper
        else:
            self.thread_stopper = threading.Event()

        # Core components
        self.events = EventManager(self)
        self.mode_controller = ModeController(self)
        create_config_collections(
            self, self.machine_config['mpf-mc']['config_collections'])
        self._preprocess_config(self.config)

        self.config_processor = ConfigProcessor(self)
        self.transition_manager = TransitionManager(self)
        self.effects_manager = EffectsManager(self)

        self._set_machine_path()

        self._load_font_paths()

        # Initialize the sound system (must be done prior to creating the AssetManager).
        # If the sound system is not available, do not load any other sound-related modules.
        if SoundSystem is None or self.options.get("no_sound"):
            self.sound_system = None
        else:
            self.sound_system = SoundSystem(self)
            if self.sound_system.audio_interface is None:
                self.sound_system = None

        self.asset_manager = ThreadedAssetManager(self)
        self.bcp_processor = BcpProcessor(self)

        # Asset classes
        ImageAsset.initialize(self)
        VideoAsset.initialize(self)
        BitmapFontAsset.initialize(self)

        self._initialise_sound_system()

        self.clock.schedule_interval(self._check_crash_queue, 1)

        self.events.add_handler("client_connected", self._create_dmds)
        self.events.add_handler("player_turn_start", self.player_start_turn)

        self.create_machine_var('mpfmc_ver', __version__)
        # force setting it here so we have it before MPF connects
        self.receive_machine_var_update('mpfmc_ver', __version__, 0, True)
コード例 #6
0
class MpfMc(App):
    """Kivy app for the mpf media controller."""

    # pylint: disable-msg=too-many-statements
    def __init__(self, options, config: MpfMcConfig, thread_stopper=None):

        self.log = logging.getLogger('mpfmc')
        self.log.info("Mission Pinball Framework Media Controller v%s",
                      __version__)
        self.log.info("Mission Pinball Framework Game Engine v%s",
                      __mpfversion__)

        if (__version__.split('.')[0] != __mpfversion__.split('.')[0]
                or __version__.split('.')[1] != __mpfversion__.split('.')[1]):

            self.log.error(
                "MPF MC and MPF Game engines must be same "
                "major.minor versions. You have MPF v%s and MPF-MC"
                " v%s", __mpfversion__, __version__)

            raise ValueError(
                "MPF MC and MPF Game engines must be same "
                "major.minor versions. You have MPF v{} and MPF-MC"
                " v{}".format(__mpfversion__, __version__))

        super().__init__()

        self.options = options
        self.machine_path = config.get_machine_path()
        self.log.info("Machine path: %s", self.machine_path)

        # load machine into path to load modules
        if self.machine_path not in sys.path:
            sys.path.append(self.machine_path)
        self.mc_config = config
        self.config_validator = ConfigValidator(self, config.get_config_spec())
        self.machine_config = self.mc_config.get_machine_config()
        self.config = self.machine_config

        self.clock = Clock
        # pylint: disable-msg=protected-access
        self.log.info("Starting clock at %sHz", Clock._max_fps)
        self._boot_holds = set()
        self.is_init_done = threading.Event()
        self.mpf_path = os.path.dirname(mpf.__file__)
        self.modes = CaseInsensitiveDict()
        self.player_list = list()
        self.player = None
        self.num_players = 0
        self.bcp_client_connected = False
        self.placeholder_manager = McPlaceholderManager(self)
        self.settings = McSettingsController(self)

        self.animation_configs = dict()
        self.active_slides = dict()
        self.custom_code = list()

        self.register_boot_hold('init')
        self.displays = DeviceCollection(self, "displays", "displays")
        self.machine_vars = CaseInsensitiveDict()
        self.machine_var_monitor = False
        self.monitors = dict()
        self.targets = dict()
        """Dict which contains all the active slide frames in the machine that
        a slide can target. Will always contain an entry called 'default'
        which will be used if a slide doesn't specify targeting.
        """

        self.keyboard = None
        self.dmds = []
        self.rgb_dmds = []
        self.crash_queue = queue.Queue()
        self.ticks = 0
        self.start_time = 0
        self.debug_refs = []

        MYPY = False  # NOQA
        if MYPY:  # pragma: no cover
            self.videos = None  # type: Dict[str, VideoAsset]

        if thread_stopper:
            self.thread_stopper = thread_stopper
        else:
            self.thread_stopper = threading.Event()

        # Core components
        self.events = EventManager(self)
        self.mode_controller = ModeController(self)
        create_config_collections(
            self, self.machine_config['mpf-mc']['config_collections'])
        self._preprocess_config(self.config)

        self.config_processor = ConfigProcessor(self)
        self.transition_manager = TransitionManager(self)
        self.effects_manager = EffectsManager(self)

        self._set_machine_path()

        self._load_font_paths()

        # Initialize the sound system (must be done prior to creating the AssetManager).
        # If the sound system is not available, do not load any other sound-related modules.
        if SoundSystem is None or self.options.get("no_sound"):
            self.sound_system = None
        else:
            self.sound_system = SoundSystem(self)
            if self.sound_system.audio_interface is None:
                self.sound_system = None

        self.asset_manager = ThreadedAssetManager(self)
        self.bcp_processor = BcpProcessor(self)

        # Asset classes
        ImageAsset.initialize(self)
        VideoAsset.initialize(self)
        BitmapFontAsset.initialize(self)

        self._initialise_sound_system()

        self.clock.schedule_interval(self._check_crash_queue, 1)

        self.events.add_handler("client_connected", self._create_dmds)
        self.events.add_handler("player_turn_start", self.player_start_turn)

        self.create_machine_var('mpfmc_ver', __version__)
        # force setting it here so we have it before MPF connects
        self.receive_machine_var_update('mpfmc_ver', __version__, 0, True)

    def _load_named_colors(self):
        for name, color in self.machine_config.get('named_colors', {}).items():
            RGBColor.add_color(name, color)

    def track_leak_reference(self, element):
        """Track elements to find leaks."""
        if not self.options["production"]:
            self.debug_refs.append(weakref.ref(element))
            # cleanup all dead references
            self.debug_refs = [
                element for element in self.debug_refs if element()
            ]

    @staticmethod
    def _preprocess_config(config):
        kivy_config = config['kivy_config']

        try:
            kivy_config['graphics'].update(config['window'])
        except KeyError:
            pass

        if ('top' in kivy_config['graphics']
                and 'left' in kivy_config['graphics']):
            kivy_config['graphics']['position'] = 'custom'

        for section, settings in kivy_config.items():
            for k, v in settings.items():
                try:
                    if k in Config[section]:
                        Config.set(section, k, v)
                except KeyError:
                    continue

        try:  # config not validated yet, so we use try
            if config['window']['exit_on_escape']:
                Config.set('kivy', 'exit_on_escape', '1')
        except KeyError:
            pass

        Config.set('graphics', 'maxfps', int(config['mpf-mc']['fps']))

    def _load_config(self):
        files = [os.path.join(mpfmc.__path__[0], self.options["mcconfigfile"])]
        for config_file in self.options["configfile"]:
            files.append(os.path.join(self.machine_path, "config",
                                      config_file))
        mpf_config = self.mpf_config_processor.load_config_files_with_cache(
            files, "machine", True)

        self._preprocess_config(mpf_config)

        return mpf_config

    def _create_dmds(self, **kwargs):
        del kwargs
        self.create_dmds()
        self.create_rgb_dmds()
        self.events.remove_all_handlers_for_event("client_connected")

    def _load_font_paths(self):
        # Add local machine fonts path
        if os.path.isdir(
                os.path.join(self.machine_path,
                             self.machine_config['mpf-mc']['paths']['fonts'])):

            resource_add_path(
                os.path.join(self.machine_path,
                             self.machine_config['mpf-mc']['paths']['fonts']))

        # Add mpfmc fonts path
        resource_add_path(
            os.path.join(os.path.dirname(mpfmc.__file__), 'fonts'))

    def _initialise_sound_system(self):
        # Only initialize sound assets if sound system is loaded and enabled
        if self.sound_system is not None and self.sound_system.enabled:
            SoundAsset.extensions = tuple(
                self.sound_system.audio_interface.supported_extensions())
            SoundAsset.initialize(self)
        else:
            # If the sound system is not loaded or enabled, remove the
            # audio-related config_player modules and config collections
            del self.machine_config['mpf-mc']['config_players']['sound']
            del self.machine_config['mpf-mc']['config_players']['track']
            del self.machine_config['mpf-mc']['config_players']['sound_loop']
            del self.machine_config['mpf-mc']['config_players']['playlist']
            del self.machine_config['mpf-mc']['config_collections'][
                'sound_loop_set']
            del self.machine_config['mpf-mc']['config_collections']['playlist']

    def get_system_config(self):
        return self.machine_config['mpf-mc']

    def validate_machine_config_section(self, section):
        """Validate machine config."""
        if section not in self.config_validator.get_config_spec():
            return

        if section not in self.machine_config:
            self.machine_config[section] = dict()

        self.machine_config[section] = self.config_validator.validate_config(
            section, self.machine_config[section], section)

    def get_config(self):
        return self.machine_config

    def _set_machine_path(self):
        self.log.debug("Machine path: %s", self.machine_path)

        # Add the machine folder to sys.path so we can import modules from it
        sys.path.insert(0, self.machine_path)

    def register_boot_hold(self, hold):
        # print('registering boot hold', hold)
        if self.is_init_done.is_set():
            raise AssertionError("Register hold after init_done")
        self._boot_holds.add(hold)

    def clear_boot_hold(self, hold):
        if self.is_init_done.is_set():
            raise AssertionError("Register hold after init_done")
        self._boot_holds.remove(hold)
        # print('clearing boot hold', hold, self._boot_holds)
        self.log.debug('Clearing boot hold %s. Holds remaining: %s', hold,
                       self._boot_holds)
        if not self._boot_holds:
            self.init_done()

    def _register_config_players(self):
        # todo move this to config_player module

        for name, module in self.machine_config['mpf-mc'][
                'config_players'].items():
            imported_module = importlib.import_module(module)
            setattr(self, '{}_player'.format(name),
                    imported_module.McPlayerCls(self))

    def displays_initialized(self, *args):
        del args
        self.validate_machine_config_section('window')
        # pylint: disable-msg=import-outside-toplevel
        from mpfmc.uix.window import Window
        Window.initialize(self)
        self.events.post('displays_initialized')
        '''event: displays_initialized
        desc: Posted as soon as MPF MC displays have been initialized.

        Note that this event is used as part of the internal MPF-MC startup
        process. In some cases it will be posted *before* the slide_player is
        ready, meaning that you *CANNOT* use this event to post slides or play
        sounds.

        Instead, use the *mc_ready* event, which is posted as early as possible
        once the slide player and sound players are setup.

        Note that this event is generated by the media controller and does not
        exist on the MPF side of things.

        Also note that if you're using a media controller other than the MPF-MC
        (such as the Unity 3D backbox controller), then this event won't exist.

        '''
        self.events.process_event_queue()
        self.events.remove_all_handlers_for_event("displays_initialized")
        self._init()

    def create_dmds(self):
        """Create DMDs."""
        if 'dmds' in self.machine_config:
            for name, config in self.machine_config['dmds'].items():
                dmd = Dmd(self, name, config)
                self.dmds.append(dmd)

    def create_rgb_dmds(self):
        """Create RBG DMDs."""
        if 'rgb_dmds' in self.machine_config:
            for name, config in self.machine_config['rgb_dmds'].items():
                dmd = RgbDmd(self, name, config)
                self.rgb_dmds.append(dmd)

    def _init(self):
        # Since the window is so critical in Kivy, we can't continue the
        # boot process until the window is setup, and we can't set the
        # window up until the displays are initialized.

        self._load_named_colors()
        self._register_config_players()
        self.events.post("init_phase_1")
        # no events docstring as this event is also in mpf
        self.events.process_event_queue()
        self.events.post("mc_ready")
        '''event: mc_ready
        desc: Posted when the MPF-MC is available to start showing slides and
        playing sounds.

        Note that this event does not mean the MC is done loading. Instead it's
        posted at the earliest possible moment that the core MC components are
        available, meaning you can trigger "boot" slides from this event (which
        could in turn be used to show asset loading status, boot progress,
        etc.)

        If you want to show slides that require images or video loaded from
        disk, use the event "init_done" instead which is posted once all the
        assets set to "preload" have been loaded.
        '''

        self.events.process_event_queue()
        self.events.post("init_phase_2")
        # no events docstring as this event is also in mpf
        self.events.process_event_queue()
        self.events.post("init_phase_3")
        # no events docstring as this event is also in mpf
        self.events.process_event_queue()
        self._load_custom_code()
        self.events.post("init_phase_4")
        # no events docstring as this event is also in mpf
        self.events.process_event_queue()
        self.events.post("init_phase_5")
        # no events docstring as this event is also in mpf
        self.events.process_event_queue()
        self.clear_boot_hold('init')
        self.events.remove_all_handlers_for_event("init_phase_1")
        self.events.remove_all_handlers_for_event("init_phase_2")
        self.events.remove_all_handlers_for_event("init_phase_3")
        self.events.remove_all_handlers_for_event("init_phase_4")
        self.events.remove_all_handlers_for_event("init_phase_5")

    def init_done(self):
        self.is_init_done.set()
        self.events.post("init_done")
        # no events docstring as this event is also in mpf
        self.events.process_event_queue()

    def build(self):
        self.start_time = time.time()
        self.ticks = 0
        self.clock.schedule_interval(self.tick, 0)
        self.events.add_handler("debug_dump_stats", self._debug_dump_displays)

    def _debug_dump_displays(self, **kwargs):
        del kwargs
        self.log.info("--- DEBUG DUMP DISPLAYS ---")
        self.log.info(
            "Active slides: %s (Count: %s). Displays: %s (Count: %s). Available Slides: %s",
            self.active_slides, len(self.active_slides), self.displays,
            len(self.displays), len(self.slides))
        for display in self.displays:
            self.log.info("Listing children for display: %s", display)
            children = 0
            for child in display.walk():
                self.log.info(child)
                children += 1
            self.log.info("Total children: %s", children)
        self.log.info("--- DEBUG DUMP DISPLAYS END ---")
        gc.collect()
        if not self.options["production"]:
            self.log.info("--- DEBUG DUMP OBJECTS ---")
            self.log.info("Elements in list (may be dead): %s",
                          len(self.debug_refs))
            for element in self.debug_refs:
                real_element = element()
                if real_element:
                    self.log.info(real_element)
            self.log.info("--- DEBUG DUMP OBJECTS END ---")
        else:
            self.log.info(
                "--- DEBUG DUMP OBJECTS DISABLED BECAUSE OF PRODUCTION FLAG ---"
            )
        self.log.info("--- DEBUG DUMP CLOCK ---")
        ev = Clock._root_event  # pylint: disable-msg=protected-access
        while ev:
            self.log.info(ev)
            ev = ev.next
        self.log.info("--- DEBUG DUMP CLOCK END ---")

    def on_stop(self):
        self.log.info("Stopping...")
        self.thread_stopper.set()

        self.events.post("shutdown")
        self.events.process_event_queue()

        try:
            self.log.info(
                "Loop rate %s Hz",
                round(self.ticks / (time.time() - self.start_time), 2))
        except ZeroDivisionError:
            pass

    def reset(self, **kwargs):
        del kwargs
        self.player = None
        self.player_list = list()

        self.events.post('mc_reset_phase_1')
        '''event: mc_reset_phase_1
        desc: Posted on the MPF-MC only (e.g. not in MPF). This event is used
        internally as part of the MPF-MC reset process.
        '''
        self.events.process_event_queue()
        self.events.post('mc_reset_phase_2')
        '''event: mc_reset_phase_2
        desc: Posted on the MPF-MC only (e.g. not in MPF). This event is used
        internally as part of the MPF-MC reset process.
        '''
        self.events.process_event_queue()
        self.events.post('mc_reset_phase_3')
        '''event: mc_reset_phase_3
        desc: Posted on the MPF-MC only (e.g. not in MPF). This event is used
        internally as part of the MPF-MC reset process.
        '''
        self.events.process_event_queue()
        self.events.post('mc_reset_complete')
        '''event: mc_reset_complete
        desc: Posted on the MPF-MC only (e.g. not in MPF). This event is posted
        when the MPF-MC reset process is complete.
        '''

    def game_start(self, **kargs):
        self.player = None
        self.player_list = list()
        self.num_players = 0
        self.events.post('game_started', **kargs)
        # no events docstring as this event is also in mpf

    def game_end(self, **kwargs):
        self.player = None
        self.events.post('game_ended', **kwargs)
        # no events docstring as this event is also in mpf

    def add_player(self, player_num):
        if player_num > len(self.player_list):
            player = Player(self, len(self.player_list))
            self.player_list.append(player)

            self.events.post('player_added', player=player, num=player_num)
            # no events docstring as this event is also in mpf

            # Enable player var events and send all initial values
            player.enable_events(True, True)

    def update_player_var(self, name, value, player_num):
        try:
            self.player_list[int(player_num) - 1][name] = value
        except (IndexError, KeyError):
            pass

    def player_start_turn(self, number, **kwargs):
        del kwargs
        if ((self.player and self.player.number != number) or not self.player):

            try:
                self.player = self.player_list[int(number) - 1]
                self.events.post('player_turn_start',
                                 number=number,
                                 player=self.player)
            except IndexError:
                self.log.error(
                    'Received player turn start for player %s, but '
                    'only %s player(s) exist', number, len(self.player_list))

    def create_machine_var(self, name, value):
        """Same as set_machine_var."""
        self.set_machine_var(name, value)

    def set_machine_var(self, name, value):
        """Set machine var and send it via BCP to MPF."""
        if hasattr(self, "bcp_processor") and self.bcp_processor.connected:
            self.bcp_processor.send_machine_var_to_mpf(name, value)

    def receive_machine_var_update(self, name, value, change, prev_value):
        """Update a machine var received via BCP."""
        if value is None:
            try:
                del self.machine_vars[name]
            except KeyError:
                pass
        else:
            self.machine_vars[name] = value

        if change:
            self.log.debug(
                "Setting machine_var '%s' to: %s, (prior: %s, "
                "change: %s)", name, value, prev_value, change)
            self.events.post('machine_var_' + name,
                             value=value,
                             prev_value=prev_value,
                             change=change)
            # no events docstring as this event is also in mpf

    def tick(self, dt):
        """Process event queue."""
        del dt
        self.ticks += 1
        self.events.process_event_queue()

    def _load_custom_code(self):
        if 'mc_scriptlets' in self.machine_config:
            self.machine_config['mc_scriptlets'] = (
                self.machine_config['mc_scriptlets'].split(' '))

            self.log.debug("Loading scriptlets... (deprecated)")

            for scriptlet in self.machine_config['mc_scriptlets']:

                self.log.debug("Loading '%s' scriptlet (deprecated)",
                               scriptlet)

                scriptlet_obj = Util.string_to_class(
                    self.machine_config['mpf-mc']['paths']['scriptlets'] +
                    "." + scriptlet)(mc=self, name=scriptlet.split('.')[1])

                self.custom_code.append(scriptlet_obj)

        if 'mc_custom_code' in self.machine_config:
            self.log.debug("Loading custom_code...")

            for custom_code in self.machine_config['mc_custom_code']:

                self.log.debug("Loading '%s' custom_code", custom_code)

                custom_code_obj = Util.string_to_class(
                    self.machine_config['mpf-mc']['paths']['scriptlets'] +
                    "." + custom_code)(mc=self, name=custom_code.split('.')[1])

                self.custom_code.append(custom_code_obj)

    def _check_crash_queue(self, dt):
        del dt
        try:
            crash = self.crash_queue.get(block=False)
        except queue.Empty:
            pass
        else:
            self.log.critical("Shutting down due to child thread crash")
            self.log.critical("Crash details: %s", crash)
            self.stop()

    def register_monitor(self, monitor_class, monitor):
        """Registers a monitor.

        Args:
            monitor_class: String name of the monitor class for this monitor
                that's being registered.
            monitor: String name of the monitor.

        MPF uses monitors to allow components to monitor certain internal
        elements of MPF.

        For example, a player variable monitor could be setup to be notified of
        any changes to a player variable, or a switch monitor could be used to
        allow a plugin to be notified of any changes to any switches.

        The MachineController's list of registered monitors doesn't actually
        do anything. Rather it's a dictionary of sets which the monitors
        themselves can reference when they need to do something. We just needed
        a central registry of monitors.

        """
        if monitor_class not in self.monitors:
            self.monitors[monitor_class] = set()

        self.monitors[monitor_class].add(monitor)

    def post_mc_native_event(self, event, **kwargs):
        if self.bcp_processor.enabled and self.bcp_client_connected:
            self.bcp_processor.send('trigger', name=event, **kwargs)

        self.events.post(event, **kwargs)