예제 #1
0
class MachineController(LogMixin):

    """Base class for the Machine Controller object.

    The machine controller is the main entity of the entire framework. It's the
    main part that's in charge and makes things happen.

    Args:
        options(dict): A dictionary of options built from the command line options
            used to launch mpf.py.
        machine_path: The root path of this machine_files folder
    """

    # pylint: disable-msg=too-many-statements
    def __init__(self, mpf_path: str, machine_path: str, options: dict) -> None:
        """Initialize machine controller."""
        super().__init__()
        self.log = logging.getLogger("Machine")     # type: Logger
        self.log.info("Mission Pinball Framework Core Engine v%s", __version__)

        self.log.info("Command line arguments: %s", options)
        self.options = options
        self.config_processor = ConfigProcessor()

        self.log.info("MPF path: %s", mpf_path)
        self.mpf_path = mpf_path

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

        self.verify_system_info()
        self._exception = None      # type: Any
        self._boot_holds = set()    # type: Set[str]
        self.is_init_done = None    # type: asyncio.Event

        self._done = False
        self.monitors = dict()      # type: Dict[str, Set[Callable]]
        self.plugins = list()       # type: List[Any]
        self.scriptlets = list()    # type: List[Scriptlet]
        self.modes = DeviceCollection(self, 'modes', None)          # type: Dict[str, Mode]
        self.game = None            # type: Game
        self.machine_vars = dict()
        self.machine_var_monitor = False
        self.machine_var_data_manager = None    # type: DataManager
        self.thread_stopper = threading.Event()

        self.config = None      # type: Any

        # add some type hints
        MYPY = False    # noqa
        if MYPY:   # pragma: no cover
            # controllers
            self.events = None                          # type: EventManager
            self.switch_controller = None               # type: SwitchController
            self.mode_controller = None                 # type: ModeController
            self.settings = None                        # type: SettingsController
            self.bcp = None                             # type: Bcp
            self.asset_manager = None                   # type: BaseAssetManager
            self.ball_controller = None                 # type: BallController
            self.show_controller = None                 # type: ShowController
            self.placeholder_manager = None             # type: PlaceholderManager
            self.device_manager = None                  # type: DeviceManager
            self.auditor = None                         # type: Auditor
            self.tui = None                             # type: TextUi
            self.service = None                         # type: ServiceController

            # devices
            self.autofires = None                       # type: DeviceCollectionType[str, AutofireCoil]
            self.motors = None                          # type: DeviceCollectionType[str, Motor]
            self.digital_outputs = None                 # type: DeviceCollectionType[str, DigitalOutput]
            self.shows = None                           # type: DeviceCollectionType[str, Show]
            self.shots = None                           # type: DeviceCollectionType[str, Shot]
            self.shot_groups = None                     # type: DeviceCollectionType[str, ShotGroup]
            self.switches = None                        # type: DeviceCollectionType[str, Switch]
            self.coils = None                           # type: DeviceCollectionType[str, Driver]
            self.lights = None                          # type: DeviceCollectionType[str, Light]
            self.ball_devices = None                    # type: DeviceCollectionType[str, BallDevice]
            self.accelerometers = None                  # type: DeviceCollectionType[str, Accelerometer]
            self.playfield = None                       # type: Playfield
            self.playfields = None                      # type: DeviceCollectionType[str, Playfield]
            self.counters = None                        # type: DeviceCollectionType[str, Counter]
            self.sequences = None                       # type: DeviceCollectionType[str, Sequence]
            self.accruals = None                        # type: DeviceCollectionType[str, Accrual]
            self.drop_targets = None                    # type: DeviceCollectionType[str, DropTarget]
            self.servos = None                          # type: DeviceCollectionType[str, Servo]
            self.segment_displays = None                # type: DeviceCollectionType[str, SegmentDisplay]

        self._set_machine_path()

        self.config_validator = ConfigValidator(self)

        self._load_config()
        self.machine_config = self.config       # type: Any
        self.configure_logging(
            'Machine',
            self.config['logging']['console']['machine_controller'],
            self.config['logging']['file']['machine_controller'])

        self.delayRegistry = DelayManagerRegistry(self)
        self.delay = DelayManager(self.delayRegistry)

        self.hardware_platforms = dict()    # type: Dict[str, SmartVirtualHardwarePlatform]
        self.default_platform = None        # type: SmartVirtualHardwarePlatform

        self.clock = self._load_clock()
        self.stop_future = asyncio.Future(loop=self.clock.loop)     # type: asyncio.Future

    @asyncio.coroutine
    def initialise_core_and_hardware(self) -> Generator[int, None, None]:
        """Load core modules and hardware."""
        self._boot_holds = set()    # type: Set[str]
        self.is_init_done = asyncio.Event(loop=self.clock.loop)
        self.register_boot_hold('init')
        self._load_hardware_platforms()

        self._load_core_modules()
        # order is specified in mpfconfig.yaml

        self._validate_config()

        # This is called so hw platforms have a chance to register for events,
        # and/or anything else they need to do with core modules since
        # they're not set up yet when the hw platforms are constructed.
        yield from self._initialize_platforms()

    @asyncio.coroutine
    def initialise(self) -> Generator[int, None, None]:
        """Initialise machine."""
        yield from self.initialise_core_and_hardware()

        self._initialize_credit_string()

        self._register_config_players()
        self._register_system_events()
        self._load_machine_vars()
        yield from self._run_init_phases()
        self._init_phases_complete()

        yield from self._start_platforms()

        # wait until all boot holds were released
        yield from self.is_init_done.wait()
        yield from self.init_done()

    def _exception_handler(self, loop, context):    # pragma: no cover
        """Handle asyncio loop exceptions."""
        # call original exception handler
        loop.set_exception_handler(None)
        loop.call_exception_handler(context)

        # remember exception
        self._exception = context
        self.stop()

    # pylint: disable-msg=no-self-use
    def _load_clock(self) -> ClockBase:  # pragma: no cover
        """Load clock and loop."""
        clock = ClockBase(self)
        clock.loop.set_exception_handler(self._exception_handler)
        return clock

    @asyncio.coroutine
    def _run_init_phases(self) -> Generator[int, None, None]:
        """Run init phases."""
        yield from self.events.post_queue_async("init_phase_1")
        '''event: init_phase_1

        desc: Posted during the initial boot up of MPF.
        '''
        yield from self.events.post_queue_async("init_phase_2")
        '''event: init_phase_2

        desc: Posted during the initial boot up of MPF.
        '''
        self._load_plugins()
        yield from self.events.post_queue_async("init_phase_3")
        '''event: init_phase_3

        desc: Posted during the initial boot up of MPF.
        '''
        self._load_scriptlets()

        yield from self.events.post_queue_async("init_phase_4")
        '''event: init_phase_4

        desc: Posted during the initial boot up of MPF.
        '''

        yield from self.events.post_queue_async("init_phase_5")
        '''event: init_phase_5

        desc: Posted during the initial boot up of MPF.
        '''

    def _init_phases_complete(self, **kwargs) -> None:
        """Cleanup after init and remove boot holds."""
        del kwargs
        ConfigValidator.unload_config_spec()
        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")

        self.clear_boot_hold('init')

    @asyncio.coroutine
    def _initialize_platforms(self) -> Generator[int, None, None]:
        """Initialise all used hardware platforms."""
        init_done = []
        # collect all platform init futures
        for hardware_platform in list(self.hardware_platforms.values()):
            init_done.append(hardware_platform.initialize())

        # wait for all of them in parallel
        results = yield from asyncio.wait(init_done, loop=self.clock.loop)
        for result in results[0]:
            result.result()

    @asyncio.coroutine
    def _start_platforms(self) -> Generator[int, None, None]:
        """Start all used hardware platforms."""
        for hardware_platform in list(self.hardware_platforms.values()):
            yield from hardware_platform.start()
            if not hardware_platform.features['tickless']:
                self.clock.schedule_interval(hardware_platform.tick, 1 / self.config['mpf']['default_platform_hz'])

    def _initialize_credit_string(self):
        """Set default credit string."""
        # Do this here so there's a credit_string var even if they're not using
        # the credits mode
        try:
            credit_string = self.config['credits']['free_play_string']
        except KeyError:
            credit_string = 'FREE PLAY'

        self.set_machine_var('credits_string', credit_string)
        '''machine_var: credits_string

        desc: Holds a displayable string which shows how many
        credits are on the machine. For example, "CREDITS: 1". If the machine
        is set to free play, the value of this string will be "FREE PLAY".

        You can change the format and value of this string in the ``credits:``
        section of the machine config file.
        '''

    def _validate_config(self) -> None:
        """Validate game and machine config."""
        self.validate_machine_config_section('machine')
        self.validate_machine_config_section('game')
        self.validate_machine_config_section('mpf')

    def validate_machine_config_section(self, section: str) -> None:
        """Validate a config section."""
        if section not in ConfigValidator.config_spec:
            return

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

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

    def _register_system_events(self) -> None:
        """Register default event handlers."""
        self.events.add_handler('quit', self.stop)
        self.events.add_handler(self.config['mpf']['switch_tag_event'].
                                replace('%', 'quit'), self.stop)

    def _register_config_players(self) -> None:
        """Register config players."""
        # todo move this to config_player module
        for name, module_class in self.config['mpf']['config_players'].items():
            config_player_class = Util.string_to_class(module_class)
            setattr(self, '{}_player'.format(name),
                    config_player_class(self))

        self._register_plugin_config_players()

    def _register_plugin_config_players(self):
        """Register plugin config players."""
        self.debug_log("Registering Plugin Config Players")
        for entry_point in iter_entry_points(group='mpf.config_player',
                                             name=None):
            self.debug_log("Registering %s", entry_point)
            name, player = entry_point.load()(self)
            setattr(self, '{}_player'.format(name), player)

    def create_data_manager(self, config_name: str) -> DataManager:     # pragma: no cover
        """Return a new DataManager for a certain config.

        Args:
            config_name: Name of the config
        """
        return DataManager(self, config_name)

    def _load_machine_vars(self) -> None:
        """Load machine vars from data manager."""
        self.machine_var_data_manager = self.create_data_manager('machine_vars')

        current_time = self.clock.get_time()

        for name, settings in (
                iter(self.machine_var_data_manager.get_data().items())):

            if not isinstance(settings, dict) or "value" not in settings:
                continue

            if ('expire' in settings and settings['expire'] and
                    settings['expire'] < current_time):

                continue

            self.set_machine_var(name=name, value=settings['value'])

        self._load_initial_machine_vars()

        # Create basic system information machine variables
        self.set_machine_var(name="mpf_version", value=mpf_version)
        self.set_machine_var(name="mpf_extended_version", value=mpf_extended_version)
        self.set_machine_var(name="python_version", value=python_version())
        self.set_machine_var(name="platform", value=platform(aliased=True))
        platform_info = system_alias(system(), release(), version())
        self.set_machine_var(name="platform_system", value=platform_info[0])
        self.set_machine_var(name="platform_release", value=platform_info[1])
        self.set_machine_var(name="platform_version", value=platform_info[2])
        self.set_machine_var(name="platform_machine", value=machine())

    def _load_initial_machine_vars(self) -> None:
        """Load initial machine var values from config if they did not get loaded from data."""
        if 'machine_vars' not in self.config:
            return

        config = self.config['machine_vars']
        for name, element in config.items():
            if name not in self.machine_vars:
                element = self.config_validator.validate_config("machine_vars", copy.deepcopy(element))
                self.set_machine_var(name=name,
                                     value=Util.convert_to_type(element['initial_value'], element['value_type']))
            self.configure_machine_var(name=name, persist=element.get('persist', False))

    def _set_machine_path(self) -> None:
        """Add the machine folder to sys.path so we can import modules from it."""
        sys.path.insert(0, self.machine_path)

    def _load_config(self) -> None:     # pragma: no cover
        config_files = [self.options['mpfconfigfile']]

        for num, config_file in enumerate(self.options['configfile']):

            if not (config_file.startswith('/') or
                    config_file.startswith('\\')):

                config_files.append(os.path.join(self.machine_path, "config", config_file))

            self.log.info("Machine config file #%s: %s", num + 1, config_file)

        self.config = self.config_processor.load_config_files_with_cache(
            config_files, "machine", load_from_cache=not self.options['no_load_cache'],
            store_to_cache=self.options['create_config_cache'])

    def verify_system_info(self):
        """Dump information about the Python installation to the log.

        Information includes Python version, Python executable, platform, and
        core architecture.
        """
        python_version_info = sys.version_info

        if not (python_version_info[0] == 3 and python_version_info[1] in (4, 5, 6)):
            raise AssertionError("Incorrect Python version. MPF requires "
                                 "Python 3.4, 3.5 or 3.6. You have Python {}.{}.{}."
                                 .format(python_version_info[0], python_version_info[1],
                                         python_version_info[2]))

        self.log.info("Platform: %s", sys.platform)
        self.log.info("Python executable location: %s", sys.executable)

        if sys.maxsize < 2**32:
            self.log.info("Python version: %s.%s.%s (32-bit)", python_version_info[0],
                          python_version_info[1], python_version_info[2])
        else:
            self.log.info("Python version: %s.%s.%s (64-bit)", python_version_info[0],
                          python_version_info[1], python_version_info[2])

    def _load_core_modules(self) -> None:
        """Load core modules."""
        self.debug_log("Loading core modules...")
        for name, module_class in self.config['mpf']['core_modules'].items():
            self.debug_log("Loading '%s' core module", module_class)
            m = Util.string_to_class(module_class)(self)
            setattr(self, name, m)

    def _load_hardware_platforms(self) -> None:
        """Load all hardware platforms."""
        self.validate_machine_config_section('hardware')
        # if platform is forced use that one
        if self.options['force_platform']:
            self.add_platform(self.options['force_platform'])
            self.set_default_platform(self.options['force_platform'])
            return

        # otherwise load all platforms
        for section, platforms in self.config['hardware'].items():
            if section == 'driverboards':
                continue
            for hardware_platform in platforms:
                if hardware_platform.lower() != 'default':
                    self.add_platform(hardware_platform)

        # set default platform
        self.set_default_platform(self.config['hardware']['platform'][0])

    def _load_plugins(self) -> None:
        """Load plugins."""
        self.debug_log("Loading plugins...")

        # TODO: This should be cleaned up. Create a Plugins base class and
        # classmethods to determine if the plugins should be used.

        for plugin in Util.string_to_list(
                self.config['mpf']['plugins']):

            self.debug_log("Loading '%s' plugin", plugin)

            plugin_obj = Util.string_to_class(plugin)(self)
            self.plugins.append(plugin_obj)

    def _load_scriptlets(self) -> None:
        """Load scriptlets."""
        if 'scriptlets' in self.config:
            self.debug_log("Loading scriptlets...")

            for scriptlet in Util.string_to_list(self.config['scriptlets']):

                self.debug_log("Loading '%s' scriptlet", scriptlet)

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

                self.scriptlets.append(scriptlet_obj)

    @asyncio.coroutine
    def reset(self) -> Generator[int, None, None]:
        """Reset the machine.

        This method is safe to call. It essentially sets up everything from
        scratch without reloading the config files and assets from disk. This
        method is called after a game ends and before attract mode begins.
        """
        self.debug_log('Resetting...')

        yield from self.events.post_queue_async('machine_reset_phase_1')
        '''Event: machine_reset_phase_1

        Desc: The first phase of resetting the machine.

        These events are posted when MPF boots (after the init_phase events are
        posted), and they're also posted subsequently when the machine is reset
        (after existing the service mode, for example).

        This is a queue event. The machine reset phase 1 will not be complete
        until the queue is cleared.

        '''

        yield from self.events.post_queue_async('machine_reset_phase_2')
        '''Event: machine_reset_phase_2

        Desc: The second phase of resetting the machine.

        These events are posted when MPF boots (after the init_phase events are
        posted), and they're also posted subsequently when the machine is reset
        (after existing the service mode, for example).

        This is a queue event. The machine reset phase 2 will not be complete
        until the queue is cleared.

        '''

        yield from self.events.post_queue_async('machine_reset_phase_3')
        '''Event: machine_reset_phase_3

        Desc: The third phase of resetting the machine.

        These events are posted when MPF boots (after the init_phase events are
        posted), and they're also posted subsequently when the machine is reset
        (after existing the service mode, for example).

        This is a queue event. The machine reset phase 3 will not be complete
        until the queue is cleared.

        '''

        """Called when the machine reset process is complete."""
        self.debug_log('Reset Complete')
        yield from self.events.post_async('reset_complete')
        '''event: reset_complete

        desc: The machine reset process is complete

        '''

    def add_platform(self, name: str) -> None:
        """Make an additional hardware platform interface available to MPF.

        Args:
            name: String name of the platform to add. Must match the name of a
                platform file in the mpf/platforms folder (without the .py
                extension).
        """
        if name not in self.hardware_platforms:
            if name not in self.config['mpf']['platforms']:
                raise AssertionError("Invalid platform {}".format(name))

            try:
                hardware_platform = Util.string_to_class(self.config['mpf']['platforms'][name])
            except ImportError as e:     # pragma: no cover
                if e.name != name:  # do not swallow unrelated errors
                    raise
                raise ImportError("Cannot add hardware platform {}. This is "
                                  "not a valid platform name".format(name))

            self.hardware_platforms[name] = (
                hardware_platform(self))

    def set_default_platform(self, name: str) -> None:
        """Set the default platform.

        It is used if a device class-specific or device-specific platform is not specified.

        Args:
            name: String name of the platform to set to default.
        """
        try:
            self.default_platform = self.hardware_platforms[name]
            self.debug_log("Setting default platform to '%s'", name)
        except KeyError:
            raise AssertionError("Cannot set default platform to '{}', as that's not"
                                 " a currently active platform".format(name))

    def register_monitor(self, monitor_class: str, monitor: Callable[..., Any]) -> None:
        """Register a monitor.

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

        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 initialise_mpf(self):
        """Initialise MPF."""
        self.info_log("Initialise MPF.")
        timeout = 30 if self.options["production"] else None
        try:
            init = Util.ensure_future(self.initialise(), loop=self.clock.loop)
            self.clock.loop.run_until_complete(Util.first([init, self.stop_future], cancel_others=False,
                                                          loop=self.clock.loop, timeout=timeout))
        except asyncio.TimeoutError:
            self.shutdown()
            self.error_log("MPF needed more than {}s for initialisation. Aborting!".format(timeout))
            return
        except RuntimeError:
            self.shutdown()
            # do not show a runtime useless runtime error
            self.error_log("Failed to initialise MPF")
            return
        if init.exception():
            self.shutdown()
            self.error_log("Failed to initialise MPF: %s", init.exception())
            traceback.print_tb(init.exception().__traceback__)  # noqa
            return

    def run(self) -> None:
        """Start the main machine run loop."""
        self.initialise_mpf()

        self.info_log("Starting the main run loop.")
        self._run_loop()

    def stop(self, **kwargs) -> None:
        """Perform a graceful exit of MPF."""
        del kwargs
        if self.stop_future.done():
            return
        self.stop_future.set_result(True)

    def _do_stop(self) -> None:
        self.log.info("Shutting down...")
        self.events.post('shutdown')
        '''event: shutdown
        desc: Posted when the machine is shutting down to give all modules a
        chance to shut down gracefully.

        '''

        self.events.process_event_queue()
        self.shutdown()

    def shutdown(self) -> None:
        """Shutdown the machine."""
        self.thread_stopper.set()
        if hasattr(self, "device_manager"):
            self.device_manager.stop_devices()
        self._platform_stop()

        self.clock.loop.stop()
        # this is needed to properly close all sockets
        self.clock.loop.run_forever()
        self.clock.loop.close()

    def _run_loop(self) -> None:    # pragma: no cover
        # Main machine run loop with when the default platform interface
        # specifies the MPF should control the main timer

        try:
            self.clock.run(self.stop_future)
        except KeyboardInterrupt:
            print("Shutdown because of keyboard interrupts")

        self._do_stop()

        if self._exception:
            print("Shutdown because of an exception:")
            raise self._exception['exception']

    def _platform_stop(self) -> None:
        """Stop all platforms."""
        for hardware_platform in list(self.hardware_platforms.values()):
            hardware_platform.stop()

    def _write_machine_var_to_disk(self, name: str) -> None:
        """Write value to disk."""
        if self.machine_vars[name]['persist'] and self.config['mpf']['save_machine_vars_to_disk']:
            self._write_machine_vars_to_disk()

    def _write_machine_vars_to_disk(self):
        """Update machine vars on disk."""
        self.machine_var_data_manager.save_all(
            {name: {"value": var["value"], "expire": var['expire_secs']}
             for name, var in self.machine_vars.items() if var["persist"]})

    def get_machine_var(self, name: str) -> Any:
        """Return the value of a machine variable.

        Args:
            name: String name of the variable you want to get that value for.

        Returns:
            The value of the variable if it exists, or None if the variable
            does not exist.

        """
        try:
            return self.machine_vars[name]['value']
        except KeyError:
            return None

    def is_machine_var(self, name: str) -> bool:
        """Return true if machine variable exists."""
        return name in self.machine_vars

    def configure_machine_var(self, name: str, persist: bool, expire_secs: int = None) -> None:
        """Create a new machine variable.

        Args:
            name: String name of the variable.
            persist: Boolean as to whether this variable should be saved to
                disk so it's available the next time MPF boots.
            expire_secs: Optional number of seconds you'd like this variable
                to persist on disk for. When MPF boots, if the expiration time
                of the variable is in the past, it will not be loaded.
                For example, this lets you write the number of credits on
                the machine to disk to persist even during power off, but you
                could set it so that those only stay persisted for an hour.
        """
        if name not in self.machine_vars:
            self.machine_vars[name] = {'value': None, 'persist': persist, 'expire_secs': expire_secs}
        else:
            self.machine_vars[name]['persist'] = persist
            self.machine_vars[name]['expire_secs'] = expire_secs

    def set_machine_var(self, name: str, value: Any) -> None:
        """Set the value of a machine variable.

        Args:
            name: String name of the variable you're setting the value for.
            value: The value you're setting. This can be any Type.
        """
        if name not in self.machine_vars:
            self.configure_machine_var(name=name, persist=False)
            prev_value = None
            change = True
        else:
            prev_value = self.machine_vars[name]['value']
            try:
                change = value - prev_value
            except TypeError:
                change = prev_value != value

        # set value
        self.machine_vars[name]['value'] = value

        if change:
            self._write_machine_var_to_disk(name)

            self.debug_log("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)
            '''event: machine_var_(name)

            desc: Posted when a machine variable is added or changes value.
            (Machine variables are like player variables, except they're
            maintained machine-wide instead of per-player or per-game.)

            args:

            value: The new value of this machine variable.

            prev_value: The previous value of this machine variable, e.g. what
            it was before the current value.

            change: If the machine variable just changed, this will be the
            amount of the change. If it's not possible to determine a numeric
            change (for example, if this machine variable is a list), then this
            *change* value will be set to the boolean *True*.
            '''

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

    def remove_machine_var(self, name: str) -> None:
        """Remove a machine variable by name.

        If this variable persists to disk, it will remove it from there too.

        Args:
            name: String name of the variable you want to remove.
        """
        try:
            del self.machine_vars[name]
            self._write_machine_vars_to_disk()
        except KeyError:
            pass

    def remove_machine_var_search(self, startswith: str = '', endswith: str = '') -> None:
        """Remove a machine variable by matching parts of its name.

        Args:
            startswith: Optional start of the variable name to match.
            endswith: Optional end of the variable name to match.

        For example, if you pass startswit='player' and endswith='score', this
        method will match and remove player1_score, player2_score, etc.
        """
        for var in list(self.machine_vars.keys()):
            if var.startswith(startswith) and var.endswith(endswith):
                del self.machine_vars[var]

        self._write_machine_vars_to_disk()

    def get_platform_sections(self, platform_section: str, overwrite: str) -> "SmartVirtualHardwarePlatform":
        """Return platform section."""
        if self.options['force_platform']:
            return self.default_platform

        if not overwrite:
            if self.config['hardware'][platform_section][0] != 'default':
                return self.hardware_platforms[self.config['hardware'][platform_section][0]]
            else:
                return self.default_platform
        else:
            try:
                return self.hardware_platforms[overwrite]
            except KeyError:
                raise AssertionError("Platform \"{}\" has not been loaded. Please add it to your \"hardware\" section.".
                                     format(overwrite))

    def register_boot_hold(self, hold: str) -> None:
        """Register a boot 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: str) -> None:
        """Clear a boot hold."""
        if self.is_init_done.is_set():
            raise AssertionError("Clearing hold after init_done")
        self._boot_holds.remove(hold)
        self.debug_log('Clearing boot hold %s. Holds remaining: %s', hold, self._boot_holds)
        if not self._boot_holds:
            self.is_init_done.set()

    @asyncio.coroutine
    def init_done(self) -> Generator[int, None, None]:
        """Finish init.

        Called when init is done and all boot holds are cleared.
        """
        yield from self.events.post_async("init_done")
        '''event: init_done

        desc: Posted when the initial (one-time / boot) init phase is done. In
        other words, once this is posted, MPF is booted and ready to go.
        '''

        ConfigValidator.unload_config_spec()
        yield from self.reset()
예제 #2
0
파일: machine.py 프로젝트: wszamotula/mpf
class MachineController(LogMixin):
    """Base class for the Machine Controller object.

    The machine controller is the main entity of the entire framework. It's the
    main part that's in charge and makes things happen.

    Args:
        options(dict): A dictionary of options built from the command line options
            used to launch mpf.py.
        machine_path: The root path of this machine_files folder
    """

    __slots__ = [
        "log", "options", "config_processor", "mpf_path", "machine_path",
        "_exception", "_boot_holds", "is_init_done", "_done", "monitors",
        "plugins", "custom_code", "modes", "game", "variables",
        "thread_stopper", "config", "config_validator", "machine_config",
        "delay", "hardware_platforms", "default_platform", "clock",
        "stop_future", "events", "switch_controller", "mode_controller",
        "settings", "asset_manager", "bcp", "ball_controller",
        "show_controller", "placeholder_manager", "device_manager", "auditor",
        "tui", "service", "switches", "shows", "coils", "ball_devices",
        "lights", "playfield", "playfields", "autofires", "_crash_handlers",
        "__dict__"
    ]

    # pylint: disable-msg=too-many-statements
    def __init__(self, mpf_path: str, machine_path: str,
                 options: dict) -> None:
        """Initialize machine controller."""
        super().__init__()
        self.log = logging.getLogger("Machine")  # type: Logger
        self.log.info("Mission Pinball Framework Core Engine v%s", __version__)
        self._crash_handlers = []

        self.log.info("Command line arguments: %s", options)
        self.options = options
        self.config_validator = ConfigValidator(self,
                                                not options["no_load_cache"],
                                                options["create_config_cache"])
        self.config_processor = ConfigProcessor(self.config_validator)

        self.log.info("MPF path: %s", mpf_path)
        self.mpf_path = mpf_path

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

        self.verify_system_info()
        self._exception = None  # type: Any
        self._boot_holds = set()  # type: Set[str]
        self.is_init_done = None  # type: asyncio.Event

        self._done = False
        self.monitors = dict()  # type: Dict[str, Set[Callable]]
        self.plugins = list()  # type: List[Any]
        self.custom_code = list()  # type: List[CustomCode]
        self.modes = DeviceCollection(self, 'modes',
                                      None)  # type: Dict[str, Mode]
        self.game = None  # type: Game
        self.variables = MachineVariables(self)  # type: MachineVariables
        self.thread_stopper = threading.Event()

        self.config = None  # type: Any

        # add some type hints
        if MYPY:  # pragma: no cover
            # controllers
            self.events = None  # type: EventManager
            self.switch_controller = None  # type: SwitchController
            self.mode_controller = None  # type: ModeController
            self.settings = None  # type: SettingsController
            self.bcp = None  # type: Bcp
            self.asset_manager = None  # type: BaseAssetManager
            self.ball_controller = None  # type: BallController
            self.show_controller = None  # type: ShowController
            self.placeholder_manager = None  # type: PlaceholderManager
            self.device_manager = None  # type: DeviceManager
            self.auditor = None  # type: Auditor
            self.tui = None  # type: TextUi
            self.service = None  # type: ServiceController
            self.show_player = None  # type: ShowPlayer
            self.light_controller = None  # type: LightController

            # devices
            self.autofires = None  # type: Dict[str, AutofireCoil]
            self.motors = None  # type: Dict[str, Motor]
            self.digital_outputs = None  # type: Dict[str, DigitalOutput]
            self.shows = None  # type: Dict[str, Show]
            self.shots = None  # type: Dict[str, Shot]
            self.shot_groups = None  # type: Dict[str, ShotGroup]
            self.switches = None  # type: Dict[str, Switch]
            self.steppers = None  # type: Dict[str, Stepper]
            self.coils = None  # type: Dict[str, Driver]
            self.lights = None  # type: Dict[str, Light]
            self.ball_devices = None  # type: Dict[str, BallDevice]
            self.accelerometers = None  # type: Dict[str, Accelerometer]
            self.playfield = None  # type: Playfield
            self.playfields = None  # type: Dict[str, Playfield]
            self.counters = None  # type: Dict[str, Counter]
            self.sequences = None  # type: Dict[str, Sequence]
            self.accruals = None  # type: Dict[str, Accrual]
            self.drop_targets = None  # type: Dict[str, DropTarget]
            self.servos = None  # type: Dict[str, Servo]
            self.segment_displays = None  # type: Dict[str, SegmentDisplay]
            self.dmds = None  # type: Dict[str, Dmd]
            self.rgb_dmds = None  # type: Dict[str, RgbDmd]
            self.flippers = None  # type: Dict[str, Flipper]
            self.diverters = None  # type: Dict[str, Diverter]
            self.multiball_locks = None  # type: Dict[str, MultiballLock]
            self.multiballs = None  # type: Dict[str, Multiball]
            self.ball_holds = None  # type: Dict[str, BallHold]

        self._set_machine_path()

        self._load_config()
        self.machine_config = self.config  # type: Any
        self.configure_logging(
            'Machine', self.config['logging']['console']['machine_controller'],
            self.config['logging']['file']['machine_controller'])

        self.delay = DelayManager(self)

        self.hardware_platforms = dict(
        )  # type: Dict[str, SmartVirtualHardwarePlatform]
        self.default_platform = None  # type: SmartVirtualHardwarePlatform

        self.clock = self._load_clock()
        self.stop_future = asyncio.Future(
            loop=self.clock.loop)  # type: asyncio.Future

    def add_crash_handler(self, handler: Callable):
        """Add a crash handler which is called on a crash.

        This can be used to restore the output and prepare logging.
        """
        self._crash_handlers.append(handler)

    @asyncio.coroutine
    def initialise_core_and_hardware(self) -> Generator[int, None, None]:
        """Load core modules and hardware."""
        self._boot_holds = set()  # type: Set[str]
        self.is_init_done = asyncio.Event(loop=self.clock.loop)
        self.register_boot_hold('init')
        self._load_hardware_platforms()

        self._load_core_modules()
        # order is specified in mpfconfig.yaml

        self._validate_config()

        # This is called so hw platforms have a chance to register for events,
        # and/or anything else they need to do with core modules since
        # they're not set up yet when the hw platforms are constructed.
        yield from self._initialize_platforms()

    @asyncio.coroutine
    def initialise(self) -> Generator[int, None, None]:
        """Initialise machine."""
        yield from self.initialise_core_and_hardware()

        self._initialize_credit_string()

        self._register_config_players()
        self._register_system_events()
        self._load_machine_vars()
        yield from self._run_init_phases()
        self._init_phases_complete()

        yield from self._start_platforms()

        # wait until all boot holds were released
        yield from self.is_init_done.wait()
        yield from self.init_done()

    def _exception_handler(self, loop, context):  # pragma: no cover
        """Handle asyncio loop exceptions."""
        # call original exception handler
        loop.set_exception_handler(None)
        loop.call_exception_handler(context)

        # remember exception
        self._exception = context
        self.stop("Exception thrown")

    # pylint: disable-msg=no-self-use
    def _load_clock(self) -> ClockBase:  # pragma: no cover
        """Load clock and loop."""
        clock = ClockBase(self)
        clock.loop.set_exception_handler(self._exception_handler)
        return clock

    @asyncio.coroutine
    def _run_init_phases(self) -> Generator[int, None, None]:
        """Run init phases."""
        yield from self.events.post_queue_async("init_phase_1")
        '''event: init_phase_1

        desc: Posted during the initial boot up of MPF.
        '''
        yield from self.events.post_queue_async("init_phase_2")
        '''event: init_phase_2

        desc: Posted during the initial boot up of MPF.
        '''
        self._load_plugins()
        yield from self.events.post_queue_async("init_phase_3")
        '''event: init_phase_3

        desc: Posted during the initial boot up of MPF.
        '''
        self._load_custom_code()

        yield from self.events.post_queue_async("init_phase_4")
        '''event: init_phase_4

        desc: Posted during the initial boot up of MPF.
        '''

        yield from self.events.post_queue_async("init_phase_5")
        '''event: init_phase_5

        desc: Posted during the initial boot up of MPF.
        '''

    def _init_phases_complete(self, **kwargs) -> None:
        """Cleanup after init and remove boot holds."""
        del kwargs
        # self.config_validator.unload_config_spec()
        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")

        self.clear_boot_hold('init')

    @asyncio.coroutine
    def _initialize_platforms(self) -> Generator[int, None, None]:
        """Initialise all used hardware platforms."""
        init_done = []
        # collect all platform init futures
        for hardware_platform in list(self.hardware_platforms.values()):
            init_done.append(hardware_platform.initialize())

        # wait for all of them in parallel
        results = yield from asyncio.wait(init_done, loop=self.clock.loop)
        for result in results[0]:
            result.result()

    @asyncio.coroutine
    def _start_platforms(self) -> Generator[int, None, None]:
        """Start all used hardware platforms."""
        for hardware_platform in list(self.hardware_platforms.values()):
            yield from hardware_platform.start()
            if not hardware_platform.features['tickless']:
                self.clock.schedule_interval(
                    hardware_platform.tick,
                    1 / self.config['mpf']['default_platform_hz'])

    def _initialize_credit_string(self):
        """Set default credit string."""
        # Do this here so there's a credit_string var even if they're not using
        # the credits mode
        try:
            credit_string = self.config['credits']['free_play_string']
        except KeyError:
            credit_string = 'FREE PLAY'

        self.variables.set_machine_var('credits_string', credit_string)
        '''machine_var: credits_string

        desc: Holds a displayable string which shows how many
        credits are on the machine. For example, "CREDITS: 1". If the machine
        is set to free play, the value of this string will be "FREE PLAY".

        You can change the format and value of this string in the ``credits:``
        section of the machine config file.
        '''

    def _validate_config(self) -> None:
        """Validate game and machine config."""
        self.validate_machine_config_section('machine')
        self.validate_machine_config_section('game')
        self.validate_machine_config_section('mpf')

    def validate_machine_config_section(self, section: str) -> None:
        """Validate a config section."""
        if section not in self.config_validator.get_config_spec():
            return

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

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

    def _register_system_events(self) -> None:
        """Register default event handlers."""
        self.events.add_handler('quit', self.stop)
        self.events.add_handler(
            self.config['mpf']['switch_tag_event'].replace('%', 'quit'),
            self.stop)

    def _register_config_players(self) -> None:
        """Register config players."""
        # todo move this to config_player module
        for name, module_class in self.config['mpf']['config_players'].items():
            config_player_class = Util.string_to_class(module_class)
            setattr(self, '{}_player'.format(name), config_player_class(self))

        self._register_plugin_config_players()

    def _register_plugin_config_players(self):
        """Register plugin config players."""
        self.debug_log("Registering Plugin Config Players")
        for entry_point in iter_entry_points(group='mpf.config_player',
                                             name=None):
            self.debug_log("Registering %s", entry_point)
            name, player = entry_point.load()(self)
            setattr(self, '{}_player'.format(name), player)

    def create_data_manager(
            self, config_name: str) -> DataManager:  # pragma: no cover
        """Return a new DataManager for a certain config.

        Args:
            config_name: Name of the config
        """
        return DataManager(self, config_name)

    def _load_machine_vars(self) -> None:
        """Load machine vars from data manager."""
        machine_var_data_manager = self.create_data_manager('machine_vars')
        current_time = self.clock.get_time()

        self.variables.load_machine_vars(machine_var_data_manager,
                                         current_time)

    def _set_machine_path(self) -> None:
        """Add the machine folder to sys.path so we can import modules from it."""
        sys.path.insert(0, self.machine_path)

    def _load_config(self) -> None:  # pragma: no cover
        config_files = [self.options['mpfconfigfile']]

        for num, config_file in enumerate(self.options['configfile']):

            if not (config_file.startswith('/')
                    or config_file.startswith('\\')):

                config_files.append(
                    os.path.join(self.machine_path, "config", config_file))

            self.log.info("Machine config file #%s: %s", num + 1, config_file)

        self.config = self.config_processor.load_config_files_with_cache(
            config_files,
            "machine",
            load_from_cache=not self.options['no_load_cache'],
            store_to_cache=self.options['create_config_cache'])

    def verify_system_info(self):
        """Dump information about the Python installation to the log.

        Information includes Python version, Python executable, platform, and
        core architecture.
        """
        python_version_info = sys.version_info

        if not (python_version_info[0] == 3
                and python_version_info[1] in (4, 5, 6, 7)):
            raise AssertionError(
                "Incorrect Python version. MPF requires "
                "Python 3.4, 3.5, 3.6 or 3.7. You have Python {}.{}.{}.".
                format(python_version_info[0], python_version_info[1],
                       python_version_info[2]))

        self.log.info("Platform: %s", sys.platform)
        self.log.info("Python executable location: %s", sys.executable)

        if sys.maxsize < 2**32:
            self.log.info("Python version: %s.%s.%s (32-bit)",
                          python_version_info[0], python_version_info[1],
                          python_version_info[2])
        else:
            self.log.info("Python version: %s.%s.%s (64-bit)",
                          python_version_info[0], python_version_info[1],
                          python_version_info[2])

    def _load_core_modules(self) -> None:
        """Load core modules."""
        self.debug_log("Loading core modules...")
        for name, module_class in self.config['mpf']['core_modules'].items():
            self.debug_log("Loading '%s' core module", module_class)
            m = Util.string_to_class(module_class)(self)
            setattr(self, name, m)

    def _load_hardware_platforms(self) -> None:
        """Load all hardware platforms."""
        self.validate_machine_config_section('hardware')

        # load internal platforms
        self.add_platform("drivers")

        # if platform is forced use that one
        if self.options['force_platform']:
            self.add_platform(self.options['force_platform'])
            self.set_default_platform(self.options['force_platform'])
            return

        # otherwise load all platforms
        for section, platforms in self.config['hardware'].items():
            if section == 'driverboards':
                continue
            for hardware_platform in platforms:
                if hardware_platform.lower() != 'default':
                    self.add_platform(hardware_platform)

        # set default platform
        self.set_default_platform(self.config['hardware']['platform'][0])

    def _load_plugins(self) -> None:
        """Load plugins."""
        self.debug_log("Loading plugins...")

        # TODO: This should be cleaned up. Create a Plugins base class and
        # classmethods to determine if the plugins should be used.

        for plugin in Util.string_to_list(self.config['mpf']['plugins']):

            self.debug_log("Loading '%s' plugin", plugin)

            plugin_obj = Util.string_to_class(plugin)(self)
            self.plugins.append(plugin_obj)

    def _load_custom_code(self) -> None:
        """Load custom code."""
        if 'scriptlets' in self.config:
            self.debug_log("Loading scriptlets (deprecated).")
            for scriptlet in Util.string_to_list(self.config['scriptlets']):
                self.debug_log("Loading '%s' scriptlet (deprecated)",
                               scriptlet)
                scriptlet_obj = Util.string_to_class(
                    self.config['mpf']['paths']['scriptlets'] + "." +
                    scriptlet)(machine=self, name=scriptlet.split('.')[1])
                self.custom_code.append(scriptlet_obj)

        if 'custom_code' in self.config:
            self.debug_log("Loading custom code.")

            for custom_code in Util.string_to_list(self.config['custom_code']):

                self.debug_log("Loading '%s' custom code", custom_code)

                custom_code_obj = Util.string_to_class(custom_code)(
                    machine=self, name=custom_code)

                self.custom_code.append(custom_code_obj)

    @asyncio.coroutine
    def reset(self) -> Generator[int, None, None]:
        """Reset the machine.

        This method is safe to call. It essentially sets up everything from
        scratch without reloading the config files and assets from disk. This
        method is called after a game ends and before attract mode begins.
        """
        self.debug_log('Resetting...')

        yield from self.events.post_queue_async('machine_reset_phase_1')
        '''Event: machine_reset_phase_1

        Desc: The first phase of resetting the machine.

        These events are posted when MPF boots (after the init_phase events are
        posted), and they're also posted subsequently when the machine is reset
        (after existing the service mode, for example).

        This is a queue event. The machine reset phase 1 will not be complete
        until the queue is cleared.

        '''

        yield from self.events.post_queue_async('machine_reset_phase_2')
        '''Event: machine_reset_phase_2

        Desc: The second phase of resetting the machine.

        These events are posted when MPF boots (after the init_phase events are
        posted), and they're also posted subsequently when the machine is reset
        (after existing the service mode, for example).

        This is a queue event. The machine reset phase 2 will not be complete
        until the queue is cleared.

        '''

        yield from self.events.post_queue_async('machine_reset_phase_3')
        '''Event: machine_reset_phase_3

        Desc: The third phase of resetting the machine.

        These events are posted when MPF boots (after the init_phase events are
        posted), and they're also posted subsequently when the machine is reset
        (after existing the service mode, for example).

        This is a queue event. The machine reset phase 3 will not be complete
        until the queue is cleared.

        '''
        """Called when the machine reset process is complete."""
        self.debug_log('Reset Complete')
        yield from self.events.post_async('reset_complete')
        '''event: reset_complete

        desc: The machine reset process is complete

        '''

    def add_platform(self, name: str) -> None:
        """Make an additional hardware platform interface available to MPF.

        Args:
            name: String name of the platform to add. Must match the name of a
                platform file in the mpf/platforms folder (without the .py
                extension).
        """
        if name not in self.hardware_platforms:
            if name in self.config['mpf']['platforms']:
                # if platform is in config load it
                try:
                    hardware_platform = Util.string_to_class(
                        self.config['mpf']['platforms'][name])
                except ImportError as e:  # pragma: no cover
                    if e.name != name:  # do not swallow unrelated errors
                        raise
                    raise ImportError(
                        "Cannot add hardware platform {}. This is "
                        "not a valid platform name".format(name))

            else:
                # check entry points
                entry_points = list(
                    iter_entry_points(group='mpf.platforms', name=name))
                if entry_points:
                    # load platform from entry point
                    self.debug_log(
                        "Loading platform %s from external entry_point", name)
                    if len(entry_points) != 1:
                        raise AssertionError(
                            "Got more than one entry point for platform {}: {}"
                            .format(name, entry_points))

                    hardware_platform = entry_points[0].load()
                else:
                    raise AssertionError("Unknown platform {}".format(name))

            self.hardware_platforms[name] = hardware_platform(self)

    def set_default_platform(self, name: str) -> None:
        """Set the default platform.

        It is used if a device class-specific or device-specific platform is not specified.

        Args:
            name: String name of the platform to set to default.
        """
        try:
            self.default_platform = self.hardware_platforms[name]
            self.debug_log("Setting default platform to '%s'", name)
        except KeyError:
            raise AssertionError(
                "Cannot set default platform to '{}', as that's not"
                " a currently active platform".format(name))

    def register_monitor(self, monitor_class: str,
                         monitor: Callable[..., Any]) -> None:
        """Register a monitor.

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

        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 initialise_mpf(self):
        """Initialise MPF."""
        self.info_log("Initialise MPF.")
        timeout = 30 if self.options["production"] else None
        try:
            init = Util.ensure_future(self.initialise(), loop=self.clock.loop)
            self.clock.loop.run_until_complete(
                Util.first([init, self.stop_future],
                           cancel_others=False,
                           loop=self.clock.loop,
                           timeout=timeout))
        except asyncio.TimeoutError:
            self._crash_shutdown()
            self.error_log(
                "MPF needed more than {}s for initialisation. Aborting!".
                format(timeout))
            return False
        except RuntimeError:
            self._crash_shutdown()
            # do not show a runtime useless runtime error
            self.error_log("Failed to initialise MPF")
            return False
        if init.done() and init.exception():
            self._crash_shutdown()
            try:
                raise init.exception()
            except:  # noqa
                self.log.exception("Failed to initialise MPF")
            return False

        return True

    def run(self) -> None:
        """Start the main machine run loop."""
        if not self.initialise_mpf():
            return

        self.info_log("Starting the main run loop.")
        self._run_loop()

    def stop(self, reason=None, **kwargs) -> None:
        """Perform a graceful exit of MPF."""
        del kwargs
        if self.stop_future.done():
            return

        if hasattr(asyncio, "run_coroutine_threadsafe"):
            asyncio.run_coroutine_threadsafe(self._stop_loop(reason),
                                             self.clock.loop)
        else:
            # fallback for python 3.4 versions without run_coroutine_threadsafe
            self.stop_future.set_result(reason)

    @asyncio.coroutine
    def _stop_loop(self, reason):
        self.stop_future.set_result(reason)

    def _do_stop(self) -> None:
        self.log.info("Shutting down...")
        self.events.post('shutdown')
        '''event: shutdown
        desc: Posted when the machine is shutting down to give all modules a
        chance to shut down gracefully.

        '''

        self.events.process_event_queue()
        self.shutdown()

    def _crash_shutdown(self):
        """MPF crashed. Cleanup as good as we can."""
        # call crash handlers
        for handler in self._crash_handlers:
            handler()
        if hasattr(self, "events") and hasattr(self, "clock"):
            # if we already got events and a clock use normal shutdown
            self._do_stop()
        else:
            # otherwise just shutdown
            self.shutdown()

    def shutdown(self) -> None:
        """Shutdown the machine."""
        self.thread_stopper.set()
        if hasattr(self, "device_manager"):
            self.device_manager.stop_devices()
        self._platform_stop()

        self.clock.loop.stop()
        # this is needed to properly close all sockets
        self.clock.loop.run_forever()
        self.clock.loop.close()

    def _run_loop(self) -> None:  # pragma: no cover
        # Main machine run loop with when the default platform interface
        # specifies the MPF should control the main timer

        try:
            reason = self.clock.run(self.stop_future)
        except KeyboardInterrupt:
            print("Shutdown because of keyboard interrupts")
            return
        except BaseException:
            # this happens when receiving a signal
            self.log.exception("Loop exited with exception")
            return

        if self._exception:
            self._crash_shutdown()

            print("Shutdown because of an exception:")
            try:
                raise self._exception['exception']
            except:  # noqa
                self.log.exception("Runtime Exception")
        else:
            self._do_stop()
            print("Shutdown reason: {}".format(reason))

    def _platform_stop(self) -> None:
        """Stop all platforms."""
        for hardware_platform in list(self.hardware_platforms.values()):
            hardware_platform.stop()

    def get_platform_sections(
            self, platform_section: str,
            overwrite: str) -> "SmartVirtualHardwarePlatform":
        """Return platform section."""
        if overwrite == "drivers":
            return self.hardware_platforms[overwrite]

        if self.options['force_platform']:
            return self.default_platform

        if not overwrite:
            if self.config['hardware'][platform_section][0] != 'default':
                return self.hardware_platforms[self.config['hardware']
                                               [platform_section][0]]
            else:
                return self.default_platform
        else:
            try:
                return self.hardware_platforms[overwrite]
            except KeyError:
                raise AssertionError(
                    "Platform \"{}\" has not been loaded. Please add it to your \"hardware\" section."
                    .format(overwrite))

    def register_boot_hold(self, hold: str) -> None:
        """Register a boot 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: str) -> None:
        """Clear a boot hold."""
        if self.is_init_done.is_set():
            raise AssertionError("Clearing hold after init_done")
        self._boot_holds.remove(hold)
        self.debug_log('Clearing boot hold %s. Holds remaining: %s', hold,
                       self._boot_holds)
        if not self._boot_holds:
            self.is_init_done.set()

    @asyncio.coroutine
    def init_done(self) -> Generator[int, None, None]:
        """Finish init.

        Called when init is done and all boot holds are cleared.
        """
        yield from self.events.post_async("init_done")
        '''event: init_done

        desc: Posted when the initial (one-time / boot) init phase is done. In
        other words, once this is posted, MPF is booted and ready to go.
        '''

        yield from self.reset()
예제 #3
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)
예제 #4
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)
예제 #5
0
class MachineController(object):
    """Base class for the Machine Controller object.

    The machine controller is the main entity of the entire framework. It's the
    main part that's in charge and makes things happen.

    Args:
        options: Dictionary of options the machine controller uses to configure
            itself.

    Attributes:
        options(dict): A dictionary of options built from the command line options
            used to launch mpf.py.
        config(dict): A dictionary of machine's configuration settings, merged from
            various sources.
        game(mpf.modes.game.code.game.Game): the current game
        machine_path: The root path of this machine_files folder
        plugins:
        scriptlets:
        hardware_platforms:
        events(mpf.core.events.EventManager):

    """
    def __init__(self, mpf_path: str, machine_path: str, options: dict):
        """Initialise machine controller."""
        self.log = logging.getLogger("Machine")
        self.log.info("Mission Pinball Framework Core Engine v%s", __version__)

        self.log.debug("Command line arguments: %s", options)
        self.options = options

        self.log.debug("MPF path: %s", mpf_path)
        self.mpf_path = mpf_path

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

        self.log.debug("Command line arguments: %s", self.options)
        self.verify_system_info()
        self._exception = None

        self._boot_holds = set()
        self.is_init_done = False
        self.register_boot_hold('init')

        self._done = False
        self.monitors = dict()
        self.plugins = list()
        self.scriptlets = list()
        self.modes = DeviceCollection(self, 'modes', None)
        self.game = None
        self.active_debugger = dict()
        self.machine_vars = CaseInsensitiveDict()
        self.machine_var_monitor = False
        self.machine_var_data_manager = None
        self.thread_stopper = threading.Event()

        self.delayRegistry = DelayManagerRegistry(self)
        self.delay = DelayManager(self.delayRegistry)

        self.crash_queue = queue.Queue()

        self.config = None
        self.events = None
        self.machine_config = None
        self._set_machine_path()

        self.config_validator = ConfigValidator(self)

        self._load_config()

        self.clock = self._load_clock()
        self._crash_queue_checker = self.clock.schedule_interval(
            self._check_crash_queue, 1)

        self.hardware_platforms = dict()
        self.default_platform = None

        self._load_hardware_platforms()

        self._initialize_credit_string()

        self._load_core_modules()
        # order is specified in mpfconfig.yaml

        # This is called so hw platforms have a chance to register for events,
        # and/or anything else they need to do with core modules since
        # they're not set up yet when the hw platforms are constructed.
        self._initialize_platforms()

        self._validate_config()

        self._register_config_players()
        self._register_system_events()
        self._load_machine_vars()
        self._run_init_phases()

        ConfigValidator.unload_config_spec()

        self.clear_boot_hold('init')

    def _exception_handler(self, loop, context):  # pragma: no cover
        # stop machine
        self.stop()

        # call original exception handler
        loop.set_exception_handler(None)
        loop.call_exception_handler(context)

        # remember exception
        self._exception = context

    # pylint: disable-msg=no-self-use
    def _load_clock(self):  # pragma: no cover
        clock = ClockBase()
        clock.loop.set_exception_handler(self._exception_handler)
        return clock

    def _run_init_phases(self):
        self.events.post("init_phase_1")
        '''event: init_phase_1

        desc: Posted during the initial boot up of MPF.
        '''

        self.events.process_event_queue()
        self.events.post("init_phase_2")
        '''event: init_phase_2

        desc: Posted during the initial boot up of MPF.
        '''
        self.events.process_event_queue()
        self._load_plugins()
        self.events.post("init_phase_3")
        '''event: init_phase_3

        desc: Posted during the initial boot up of MPF.
        '''
        self.events.process_event_queue()
        self._load_scriptlets()
        self.events.post("init_phase_4")
        '''event: init_phase_4

        desc: Posted during the initial boot up of MPF.
        '''
        self.events.process_event_queue()
        self.events.post("init_phase_5")
        '''event: init_phase_5

        desc: Posted during the initial boot up of MPF.
        '''
        self.events.process_event_queue()

    def _initialize_platforms(self):
        for platform in list(self.hardware_platforms.values()):
            platform.initialize()
            if not platform.features['tickless']:
                self.clock.schedule_interval(
                    platform.tick,
                    1 / self.config['mpf']['default_platform_hz'])

    def _initialize_credit_string(self):
        # Do this here so there's a credit_string var even if they're not using
        # the credits mode
        try:
            credit_string = self.config['credits']['free_play_string']
        except KeyError:
            credit_string = 'FREE PLAY'

        self.create_machine_var('credits_string', credit_string, silent=True)

    def _validate_config(self):
        self.validate_machine_config_section('machine')
        self.validate_machine_config_section('hardware')
        self.validate_machine_config_section('game')

    def validate_machine_config_section(self, section):
        """Validate a config section."""
        if section not in ConfigValidator.config_spec:
            return

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

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

    def _register_system_events(self):
        self.events.add_handler('shutdown', self.power_off)
        self.events.add_handler(
            self.config['mpf']['switch_tag_event'].replace('%', 'shutdown'),
            self.power_off)
        self.events.add_handler('quit', self.stop)
        self.events.add_handler(
            self.config['mpf']['switch_tag_event'].replace('%', 'quit'),
            self.stop)

    def _register_config_players(self):
        # todo move this to config_player module
        for name, module in self.config['mpf']['config_players'].items():
            imported_module = importlib.import_module(module)
            setattr(self, '{}_player'.format(name),
                    imported_module.player_cls(self))

        self._register_plugin_config_players()

    def _register_plugin_config_players(self):

        self.log.debug("Registering Plugin Config Players")
        for entry_point in iter_entry_points(group='mpf.config_player',
                                             name=None):
            self.log.debug("Registering %s", entry_point)
            entry_point.load()(self)

    def create_data_manager(self, config_name):  # pragma: no cover
        """Return a new DataManager for a certain config.

        Args:
            config_name: Name of the config
        """
        return DataManager(self, config_name)

    def _load_machine_vars(self):
        self.machine_var_data_manager = self.create_data_manager(
            'machine_vars')

        current_time = self.clock.get_time()

        for name, settings in (iter(
                self.machine_var_data_manager.get_data().items())):

            if not isinstance(settings, dict) or "value" not in settings:
                continue

            if ('expire' in settings and settings['expire']
                    and settings['expire'] < current_time):

                settings['value'] = 0

            self.create_machine_var(name=name, value=settings['value'])

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

    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 _get_mpfcache_file_name(self):
        cache_dir = tempfile.gettempdir()
        path_hash = hashlib.md5(bytes(self.machine_path, 'UTF-8')).hexdigest()
        path_hash += '-'.join(self.options['configfile'])
        result = os.path.join(cache_dir, path_hash)
        return result

    def _load_config(self):  # pragma: no cover
        if self.options['no_load_cache']:
            load_from_cache = False
        else:
            try:
                if self._get_latest_config_mod_time() > os.path.getmtime(
                        self._get_mpfcache_file_name()):
                    load_from_cache = False  # config is newer
                else:
                    load_from_cache = True  # cache is newer

            except OSError as exception:
                if exception.errno != errno.ENOENT:
                    raise  # some unknown error?
                else:
                    load_from_cache = False  # cache file doesn't exist

        config_loaded = False
        if load_from_cache:
            config_loaded = self._load_config_from_cache()

        if not config_loaded:
            self._load_config_from_files()

    def _load_config_from_files(self):
        self.log.info("Loading config from original files")

        self.config = self._get_mpf_config()
        self.config['_mpf_version'] = __version__

        for num, config_file in enumerate(self.options['configfile']):

            if not (config_file.startswith('/')
                    or config_file.startswith('\\')):

                config_file = os.path.join(
                    self.machine_path, self.config['mpf']['paths']['config'],
                    config_file)

            self.log.info("Machine config file #%s: %s", num + 1, config_file)

            self.config = Util.dict_merge(
                self.config,
                ConfigProcessor.load_config_file(config_file,
                                                 config_type='machine'))
            self.machine_config = self.config

        if self.options['create_config_cache']:
            self._cache_config()

    def _get_mpf_config(self):
        return ConfigProcessor.load_config_file(self.options['mpfconfigfile'],
                                                config_type='machine')

    def _load_config_from_cache(self):
        self.log.info("Loading cached config: %s",
                      self._get_mpfcache_file_name())

        with open(self._get_mpfcache_file_name(), 'rb') as f:

            try:
                self.config = pickle.load(f)
                self.machine_config = self.config

            # unfortunately pickle can raise all kinds of exceptions and we dont want to crash on corrupted cache
            # pylint: disable-msg=broad-except
            except Exception:  # pragma: no cover
                self.log.warning("Could not load config from cache")
                return False

            if self.config.get('_mpf_version') != __version__:
                self.log.info(
                    "Cached config is from a different version of MPF.")
                return False

            return True

    def _get_latest_config_mod_time(self):

        latest_time = os.path.getmtime(self.options['mpfconfigfile'])

        for root, dirs, files in os.walk(
                os.path.join(self.machine_path, 'config')):
            for name in files:
                if not name.startswith('.'):
                    if os.path.getmtime(os.path.join(root,
                                                     name)) > latest_time:
                        latest_time = os.path.getmtime(os.path.join(
                            root, name))

            for name in dirs:
                if not name.startswith('.'):
                    if os.path.getmtime(os.path.join(root,
                                                     name)) > latest_time:
                        latest_time = os.path.getmtime(os.path.join(
                            root, name))

        return latest_time

    def _cache_config(self):  # pragma: no cover
        with open(self._get_mpfcache_file_name(), 'wb') as f:
            pickle.dump(self.config, f, protocol=4)
            self.log.info('Config file cache created: %s',
                          self._get_mpfcache_file_name())

    def verify_system_info(self):
        """Dump information about the Python installation to the log.

        Information includes Python version, Python executable, platform, and
        core architecture.
        """
        python_version = sys.version_info

        if not (python_version[0] == 3 and
                (python_version[1] == 4 or python_version[1] == 5)):
            raise AssertionError(
                "Incorrect Python version. MPF requires "
                "Python 3.4 or 3.5. You have Python {}.{}.{}.".format(
                    python_version[0], python_version[1], python_version[2]))

        self.log.debug("Python version: %s.%s.%s", python_version[0],
                       python_version[1], python_version[2])
        self.log.debug("Platform: %s", sys.platform)
        self.log.debug("Python executable location: %s", sys.executable)
        self.log.debug("32-bit Python? %s", sys.maxsize < 2**32)

    def _load_core_modules(self):
        self.log.debug("Loading core modules...")
        for name, module in self.config['mpf']['core_modules'].items():
            self.log.debug("Loading '%s' core module", module)
            m = Util.string_to_class(module)(self)
            setattr(self, name, m)

    def _load_hardware_platforms(self):
        if not self.options['force_platform']:
            for section, platform in self.config['hardware'].items():
                if platform.lower() != 'default' and section != 'driverboards':
                    self.add_platform(platform)
            self.set_default_platform(self.config['hardware']['platform'])

        else:
            self.add_platform(self.options['force_platform'])
            self.set_default_platform(self.options['force_platform'])

    def _load_plugins(self):
        self.log.debug("Loading plugins...")

        # TODO: This should be cleaned up. Create a Plugins base class and
        # classmethods to determine if the plugins should be used.

        for plugin in Util.string_to_list(self.config['mpf']['plugins']):

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

            plugin_obj = Util.string_to_class(plugin)(self)
            self.plugins.append(plugin_obj)

    def _load_scriptlets(self):
        if 'scriptlets' in self.config:
            self.config['scriptlets'] = self.config['scriptlets'].split(' ')

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

            for scriptlet in self.config['scriptlets']:

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

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

                self.scriptlets.append(scriptlet_obj)

    def reset(self):
        """Reset the machine.

        This method is safe to call. It essentially sets up everything from
        scratch without reloading the config files and assets from disk. This
        method is called after a game ends and before attract mode begins.

        Note: This method is not yet implemented.
        """
        self.log.debug('Resetting...')
        self.events.process_event_queue()
        self.events.post('machine_reset_phase_1')
        '''Event: machine_reset_phase_1

        Desc: The first phase of resetting the machine.

        These events are posted when MPF boots (after the init_phase events are
        posted), and they're also posted subsequently when the machine is reset
        (after existing the service mode, for example).

        '''
        self.events.process_event_queue()
        self.events.post('machine_reset_phase_2')
        '''Event: machine_reset_phase_2

        Desc: The second phase of resetting the machine.

        These events are posted when MPF boots (after the init_phase events are
        posted), and they're also posted subsequently when the machine is reset
        (after existing the service mode, for example).

        '''
        self.events.process_event_queue()
        self.events.post('machine_reset_phase_3')
        '''Event: machine_reset_phase_3

        Desc: The third phase of resetting the machine.

        These events are posted when MPF boots (after the init_phase events are
        posted), and they're also posted subsequently when the machine is reset
        (after existing the service mode, for example).

        '''
        self.events.process_event_queue()
        self.log.debug('Reset Complete')
        self._reset_complete()

    def add_platform(self, name):
        """Make an additional hardware platform interface available to MPF.

        Args:
            name: String name of the platform to add. Must match the name of a
                platform file in the mpf/platforms folder (without the .py
                extension).
        """
        if name not in self.hardware_platforms:

            try:
                hardware_platform = Util.string_to_class(
                    self.config['mpf']['platforms'][name])
            except ImportError:  # pragma: no cover
                raise ImportError("Cannot add hardware platform {}. This is "
                                  "not a valid platform name".format(name))

            self.hardware_platforms[name] = (hardware_platform(self))

    def set_default_platform(self, name):
        """Set the default platform.

        It is used if a device class-specific or device-specific platform is not specified.

        Args:
            name: String name of the platform to set to default.
        """
        try:
            self.default_platform = self.hardware_platforms[name]
            self.log.debug("Setting default platform to '%s'", name)
        except KeyError:
            raise AssertionError(
                "Cannot set default platform to '{}', as that's not"
                " a currently active platform".format(name))

    def register_monitor(self, monitor_class, monitor):
        """Register 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 run(self):
        """Start the main machine run loop."""
        self.log.debug("Starting the main run loop.")

        self._run_loop()

    def stop(self, **kwargs):
        """Perform a graceful exit of MPF."""
        del kwargs
        if self._done:
            return

        self.log.info("Shutting down...")
        self.events.post('shutdown')
        '''event: shutdown
        desc: Posted when the machine is shutting down to give all modules a
        chance to shut down gracefully.

        '''
        self.events.process_event_queue()
        self.thread_stopper.set()
        self._platform_stop()

        self.clock.loop.stop()

    def _do_stop(self):
        if self._done:
            return

        self._done = True
        self.clock.loop.stop()
        # this is needed to properly close all sockets
        self.clock.loop.run_forever()

    def _run_loop(self):  # pragma: no cover
        # Main machine run loop with when the default platform interface
        # specifies the MPF should control the main timer

        try:
            self.clock.run()
        except KeyboardInterrupt:
            self.stop()

        if self._exception:
            print("Shutdown because of an exception:")
            raise self._exception['exception']

        self._do_stop()
        self.clock.loop.close()

    def _platform_stop(self):
        for platform in list(self.hardware_platforms.values()):
            platform.stop()

    def power_off(self, **kwargs):
        """Attempt to perform a power down of the pinball machine and ends MPF.

        This method is not yet implemented.
        """
        pass

    def _reset_complete(self):
        self.log.debug('Reset Complete')
        self.events.post('reset_complete')
        '''event: reset_complete

        desc: The machine reset process is complete

        '''

    def set_machine_var(self, name, value, force_events=False):
        """Set the value of a machine variable.

        Args:
            name: String name of the variable you're setting the value for.
            value: The value you're setting. This can be any Type.
            force_events: Boolean which will force the event posting, the
                machine monitor callback, and writing the variable to disk (if
                it's set to persist). By default these things only happen if
                the new value is different from the old value.
        """
        if name not in self.machine_vars:
            self.log.warning(
                "Received request to set machine_var '%s', but "
                "that is not a valid machine_var.", name)
            return

        prev_value = self.machine_vars[name]['value']
        self.machine_vars[name]['value'] = value

        try:
            change = value - prev_value
        except TypeError:
            change = prev_value != value

        if change or force_events:

            if self.machine_vars[name]['persist'] and self.config['mpf'][
                    'save_machine_vars_to_disk']:
                disk_var = CaseInsensitiveDict()
                disk_var['value'] = value

                if self.machine_vars[name]['expire_secs']:
                    disk_var['expire'] = self.clock.get_time(
                    ) + self.machine_vars[name]['expire_secs']

                self.machine_var_data_manager.save_key(name, disk_var)

            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)
            '''event: machine_var_(name)

            desc: Posted when a machine variable is added or changes value.
            (Machine variables are like player variables, except they're
            maintained machine-wide instead of per-player or per-game.)

            args:

            value: The new value of this machine variable.

            prev_value: The previous value of this machine variable, e.g. what
            it was before the current value.

            change: If the machine variable just changed, this will be the
            amount of the change. If it's not possible to determine a numeric
            change (for example, if this machine variable is a list), then this
            *change* value will be set to the boolean *True*.
            '''

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

    def get_machine_var(self, name):
        """Return the value of a machine variable.

        Args:
            name: String name of the variable you want to get that value for.

        Returns:
            The value of the variable if it exists, or None if the variable
            does not exist.

        """
        try:
            return self.machine_vars[name]['value']
        except KeyError:
            return None

    def is_machine_var(self, name):
        """Return true if machine variable exists."""
        return name in self.machine_vars

    # pylint: disable-msg=too-many-arguments
    def create_machine_var(self,
                           name,
                           value=0,
                           persist=False,
                           expire_secs=None,
                           silent=False):
        """Create a new machine variable.

        Args:
            name: String name of the variable.
            value: The value of the variable. This can be any Type.
            persist: Boolean as to whether this variable should be saved to
                disk so it's available the next time MPF boots.
            expire_secs: Optional number of seconds you'd like this variable
                to persist on disk for. When MPF boots, if the expiration time
                of the variable is in the past, it will be loaded with a value
                of 0. For example, this lets you write the number of credits on
                the machine to disk to persist even during power off, but you
                could set it so that those only stay persisted for an hour.
        """
        var = CaseInsensitiveDict()

        var['value'] = value
        var['persist'] = persist
        var['expire_secs'] = expire_secs

        self.machine_vars[name] = var

        if not silent:
            self.set_machine_var(name, value, force_events=True)

    def remove_machine_var(self, name):
        """Remove a machine variable by name.

        If this variable persists to disk, it will remove it from there too.

        Args:
            name: String name of the variable you want to remove.
        """
        try:
            del self.machine_vars[name]
            self.machine_var_data_manager.remove_key(name)
        except KeyError:
            pass

    def remove_machine_var_search(self, startswith='', endswith=''):
        """Remove a machine variable by matching parts of its name.

        Args:
            startswith: Optional start of the variable name to match.
            endswith: Optional end of the variable name to match.

        For example, if you pass startswit='player' and endswith='score', this
        method will match and remove player1_score, player2_score, etc.
        """
        for var in list(self.machine_vars.keys()):
            if var.startswith(startswith) and var.endswith(endswith):
                del self.machine_vars[var]
                self.machine_var_data_manager.remove_key(var)

    def get_platform_sections(self, platform_section, overwrite):
        """Return platform section."""
        if not self.options['force_platform']:
            if not overwrite:
                if self.config['hardware'][platform_section] != 'default':
                    return self.hardware_platforms[self.config['hardware']
                                                   [platform_section]]
                else:
                    return self.default_platform
            else:
                try:
                    return self.hardware_platforms[overwrite]
                except KeyError:
                    self.add_platform(overwrite)
                    return self.hardware_platforms[overwrite]
        else:
            return self.default_platform

    def register_boot_hold(self, hold):
        """Register a boot hold."""
        if self.is_init_done:
            raise AssertionError("Register hold after init_done")
        self._boot_holds.add(hold)

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

    def init_done(self):
        """Finish init.

        Called when init is done and all boot holds are cleared.
        """
        self.is_init_done = True

        self.events.post("init_done")
        '''event: init_done

        desc: Posted when the initial (one-time / boot) init phase is done. In
        other words, once this is posted, MPF is booted and ready to go.
        '''
        self.events.process_event_queue()

        ConfigValidator.unload_config_spec()
        self.reset()