Beispiel #1
0
    def __init__(self, machine):
        self.machine = machine
        self.registered_switches = CaseInsensitiveDict()
        # Dictionary of switches and states that have been registered for
        # callbacks.

        self.active_timed_switches = defaultdict(list)
        # Dictionary of switches that are currently in a state counting ms
        # waiting to notify their handlers. In other words, this is the dict that
        # tracks current switches for things like "do foo() if switch bar is
        # active for 100ms."

        self.switches = CaseInsensitiveDict()
        # Dictionary which holds the master list of switches as well as their
        # current states. State here does factor in whether a switch is NO or NC,
        # so 1 = active and 0 = inactive.

        # register for events
        self.machine.events.add_handler('timer_tick', self._tick, 1000)
        self.machine.events.add_handler('init_phase_2',
                                        self._initialize_switches, 1000)
        # priority 1000 so this fires first

        self.machine.events.add_handler('machine_reset_phase_3',
                                        self.log_active_switches)
    def create_machine_var(self,
                           name,
                           value=0,
                           persist=False,
                           expire_secs=None,
                           silent=False):
        """Creates 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)
Beispiel #3
0
    def __init__(self, machine):
        self.machine = machine
        self.registered_switches = CaseInsensitiveDict()
        # Dictionary of switches and states that have been registered for
        # callbacks.

        self.active_timed_switches = defaultdict(list)
        # Dictionary of switches that are currently in a state counting ms
        # waiting to notify their handlers. In other words, this is the dict that
        # tracks current switches for things like "do foo() if switch bar is
        # active for 100ms."

        self.switches = CaseInsensitiveDict()
        # Dictionary which holds the master list of switches as well as their
        # current states. State here does factor in whether a switch is NO or NC,
        # so 1 = active and 0 = inactive.

        # register for events
        self.machine.events.add_handler('timer_tick', self._tick, 1000)
        self.machine.events.add_handler('init_phase_2',
                                        self._initialize_switches,
                                        1000)
                                        # priority 1000 so this fires first

        self.machine.events.add_handler('machine_reset_phase_3',
                                        self.log_active_switches)
    def set_machine_var(self, name, value, force_events=False):
        """Sets 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:
            if prev_value != value:
                change = True
            else:
                change = False

        if change or force_events:

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

                if self.machine_vars[name]['expire_secs']:
                    disk_var['expire'] = (
                        time.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)

            if self.machine_var_monitor:
                for callback in self.monitors['machine_vars']:
                    callback(name=name,
                             value=value,
                             prev_value=prev_value,
                             change=change)
Beispiel #5
0
    def _get_merged_settings(self, section_name):
        # Returns a dict_merged dict of a config section from the machine-wide
        # config with the mode-specific config merged in.

        if section_name in self.machine.config:
            return_dict = copy.deepcopy(self.machine.config[section_name])
        else:
            return_dict = CaseInsensitiveDict()

        if section_name in self.config:
            return_dict = Util.dict_merge(return_dict,
                                          self.config[section_name],
                                          combine_lists=False)

        return return_dict
Beispiel #6
0
    def __init__(self, machine, config_section, path_string, asset_class,
                 asset_attribute, file_extensions):

        self.log = logging.getLogger(config_section + ' Asset Manager')
        self.log.debug("Initializing...")

        self.machine = machine
        self.loader_thread.exception_queue = self.machine.crash_queue
        self.max_memory = None
        self.registered_assets = set()
        self.path_string = path_string
        self.config_section = config_section
        self.asset_class = asset_class
        self.file_extensions = file_extensions

        self.machine.asset_managers[config_section] = self

        if not hasattr(self.machine, asset_attribute):
            setattr(self.machine, asset_attribute, CaseInsensitiveDict())

        self.asset_list = getattr(self.machine, asset_attribute)

        self.machine.mode_controller.register_load_method(self.load_assets,
                                            self.config_section,
                                            load_key='preload',
                                            priority=asset_class.load_priority)

        self.machine.mode_controller.register_start_method(self.load_assets,
                                            self.config_section,
                                            load_key='mode_start',
                                            priority=asset_class.load_priority)

        # register & load systemwide assets
        self.machine.events.add_handler('init_phase_4',
                                        self.register_and_load_machine_assets,
                                        priority=self.asset_class.load_priority)

        self.defaults = self.setup_defaults(self.machine.config)
    def __init__(self, options):
        self.options = options
        self.log = logging.getLogger("Machine")
        self.log.info("Mission Pinball Framework v%s", version.__version__)
        self.log.debug("Command line arguments: {}".format(self.options))
        self.verify_system_info()

        self.loop_start_time = 0
        self.tick_num = 0
        self.done = False
        self.machine_path = None  # Path to this machine's folder root
        self.monitors = dict()
        self.plugins = list()
        self.scriptlets = list()
        self.modes = list()
        self.asset_managers = dict()
        self.game = None
        self.active_debugger = dict()
        self.machine_vars = CaseInsensitiveDict()
        self.machine_var_monitor = False
        self.machine_var_data_manager = None

        self.flag_bcp_reset_complete = False
        self.asset_loader_complete = False

        self.delay = DelayManager()

        self.crash_queue = Queue.Queue()
        Task.create(self._check_crash_queue)

        FileManager.init()
        self.config = dict()
        self._load_mpf_config()
        self._set_machine_path()
        self._load_machine_config()

        self.configure_debugger()

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

        if not self.options['force_platform']:
            for section, platform in self.config['hardware'].iteritems():
                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'])

        # 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)

        self._load_system_modules()

        # This is called so hw platforms have a change to register for events,
        # and/or anything else they need to do with system modules since
        # they're not set up yet when the hw platforms are constructed.
        for platform in self.hardware_platforms.values():
            platform.initialize()

        self.validate_machine_config_section('machine')
        self.validate_machine_config_section('timing')
        self.validate_machine_config_section('hardware')
        self.validate_machine_config_section('game')

        self._register_system_events()
        self._load_machine_vars()
        self.events.post("init_phase_1")
        self.events._process_event_queue()
        self.events.post("init_phase_2")
        self.events._process_event_queue()
        self._load_plugins()
        self.events.post("init_phase_3")
        self.events._process_event_queue()
        self._load_scriptlets()
        self.events.post("init_phase_4")
        self.events._process_event_queue()
        self.events.post("init_phase_5")
        self.events._process_event_queue()

        self.reset()
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: A dictionary of options built from the command line options
            used to launch mpf.py.
        config: A dictionary of machine's configuration settings, merged from
            various sources.
        done: Boolean. Set to True and MPF exits.
        machine_path: The root path of this machine_files folder
        display:
        plugins:
        scriptlets:
        platform:
        events:
    """
    def __init__(self, options):
        self.options = options
        self.log = logging.getLogger("Machine")
        self.log.info("Mission Pinball Framework v%s", version.__version__)
        self.log.debug("Command line arguments: {}".format(self.options))
        self.verify_system_info()

        self.loop_start_time = 0
        self.tick_num = 0
        self.done = False
        self.machine_path = None  # Path to this machine's folder root
        self.monitors = dict()
        self.plugins = list()
        self.scriptlets = list()
        self.modes = list()
        self.asset_managers = dict()
        self.game = None
        self.active_debugger = dict()
        self.machine_vars = CaseInsensitiveDict()
        self.machine_var_monitor = False
        self.machine_var_data_manager = None

        self.flag_bcp_reset_complete = False
        self.asset_loader_complete = False

        self.delay = DelayManager()

        self.crash_queue = Queue.Queue()
        Task.create(self._check_crash_queue)

        FileManager.init()
        self.config = dict()
        self._load_mpf_config()
        self._set_machine_path()
        self._load_machine_config()

        self.configure_debugger()

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

        if not self.options['force_platform']:
            for section, platform in self.config['hardware'].iteritems():
                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'])

        # 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)

        self._load_system_modules()

        # This is called so hw platforms have a change to register for events,
        # and/or anything else they need to do with system modules since
        # they're not set up yet when the hw platforms are constructed.
        for platform in self.hardware_platforms.values():
            platform.initialize()

        self.validate_machine_config_section('machine')
        self.validate_machine_config_section('timing')
        self.validate_machine_config_section('hardware')
        self.validate_machine_config_section('game')

        self._register_system_events()
        self._load_machine_vars()
        self.events.post("init_phase_1")
        self.events._process_event_queue()
        self.events.post("init_phase_2")
        self.events._process_event_queue()
        self._load_plugins()
        self.events.post("init_phase_3")
        self.events._process_event_queue()
        self._load_scriptlets()
        self.events.post("init_phase_4")
        self.events._process_event_queue()
        self.events.post("init_phase_5")
        self.events._process_event_queue()

        self.reset()

    def validate_machine_config_section(self, section):
        if section not in self.config['config_validator']:
            return

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

        self.config[section] = self.config_processor.process_config2(
            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.quit)
        self.events.add_handler(
            self.config['mpf']['switch_tag_event'].replace('%', 'quit'),
            self.quit)
        self.events.add_handler('timer_tick', self._loading_tick)

    def _load_machine_vars(self):
        self.machine_var_data_manager = DataManager(self, 'machine_vars')

        current_time = time.time()

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

            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):
        try:
            crash = self.crash_queue.get(block=False)
        except Queue.Empty:
            yield 1000
        else:
            self.log.critical("MPF Shutting down due to child thread crash")
            self.log.critical("Crash details: %s", crash)
            self.done = True

    def _load_mpf_config(self):
        self.config = Config.load_config_file(self.options['mpfconfigfile'])

    def _set_machine_path(self):
        # If the machine folder value passed starts with a forward or
        # backward slash, then we assume it's from the mpf root. Otherwise we
        # assume it's in the mpf/machine_files folder
        if (self.options['machine_path'].startswith('/')
                or self.options['machine_path'].startswith('\\')):
            machine_path = self.options['machine_path']
        else:
            machine_path = os.path.join(
                self.config['mpf']['paths']['machine_files'],
                self.options['machine_path'])

        self.machine_path = os.path.abspath(machine_path)
        self.log.debug("Machine path: {}".format(self.machine_path))

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

    def _load_machine_config(self):
        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,
                                          Config.load_config_file(config_file))

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

        Information includes Python version, Python executable, platform, and
        system architecture.

        """
        python_version = sys.version_info

        if python_version[0] != 2 or python_version[1] != 7:
            self.log.error(
                "Incorrect Python version. MPF requires Python 2.7."
                "x. You have Python %s.%s.%s.", python_version[0],
                python_version[1], python_version[2])
            sys.exit()

        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_system_modules(self):
        self.log.info("Loading system modules...")
        for module in self.config['mpf']['system_modules']:
            self.log.debug("Loading '%s' system module", module[1])
            m = self.string_to_class(module[1])(self)
            setattr(self, module[0], m)

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

        # TODO: This should be cleaned up. Create a Plugins superclass 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)

            pluginObj = self.string_to_class(plugin)(self)
            self.plugins.append(pluginObj)

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

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

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

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

                i = __import__(self.config['mpf']['paths']['scriptlets'] +
                               '.' + scriptlet.split('.')[0],
                               fromlist=[''])

                self.scriptlets.append(
                    getattr(i,
                            scriptlet.split('.')[1])(
                                machine=self, name=scriptlet.split('.')[1]))

    def _prepare_to_reset(self):
        pass

        # wipe all event handlers

    def reset(self):
        """Resets 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.events.post('Resetting...')
        self.events._process_event_queue()
        self.events.post('machine_reset_phase_1')
        self.events._process_event_queue()
        self.events.post('machine_reset_phase_2')
        self.events._process_event_queue()
        self.events.post('machine_reset_phase_3')
        self.events._process_event_queue()
        self.log.debug('Reset Complete')

    def add_platform(self, name):
        """Makes 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:
            hardware_platform = __import__('mpf.platform.%s' % name,
                                           fromlist=["HardwarePlatform"])

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

    def set_default_platform(self, name):
        """Sets the default platform which is used if a device class-specific or
        device-specific platform is not specified. The default platform also
        controls whether a platform timer or MPF's timer is used.

        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:
            self.log.error(
                "Cannot set default platform to '%s', as that's not"
                " a currently active platform", name)

    def string_to_class(self, class_string):
        """Converts a string like mpf.system.events.EventManager into a python
        class.

        Args:
            class_string(str): The input string

        Returns:
            A reference to the python class object

        This function came from here:
        http://stackoverflow.com/questions/452969/
        does-python-have-an-equivalent-to-java-class-forname

        """
        parts = class_string.split('.')
        module = ".".join(parts[:-1])
        m = __import__(module)
        for comp in parts[1:]:
            m = getattr(m, comp)
        return m

    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 run(self):
        """Starts the main machine run loop."""
        self.log.debug("Starting the main run loop.")

        self.default_platform.timer_initialize()

        self.loop_start_time = time.time()

        if self.default_platform.features['hw_timer']:
            self.default_platform.run_loop()
        else:
            self._mpf_timer_run_loop()

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

        start_time = time.time()
        loops = 0
        secs_per_tick = timing.Timing.secs_per_tick
        sleep_sec = self.config['timing']['hw_thread_sleep_ms'] / 1000.0

        self.default_platform.next_tick_time = time.time()

        try:
            while self.done is False:
                time.sleep(sleep_sec)
                self.default_platform.tick()
                loops += 1
                if self.default_platform.next_tick_time <= time.time(
                ):  # todo change this
                    self.timer_tick()
                    self.default_platform.next_tick_time += secs_per_tick

        except KeyboardInterrupt:
            pass

        self.log_loop_rate()
        self._platform_stop()

        try:
            self.log.info("Hardware loop rate: %s Hz",
                          round(loops / (time.time() - start_time), 2))
        except ZeroDivisionError:
            self.log.info("Hardware loop rate: 0 Hz")

    def timer_tick(self):
        """Called to "tick" MPF at a rate specified by the machine Hz setting.

        This method is called by the MPF run loop or the platform run loop,
        depending on the platform. (Some platforms drive the loop, and others
        let MPF drive.)

        """
        self.tick_num += 1  # used to calculate the loop rate when MPF exits
        self.timing.timer_tick()  # notifies the timing module
        self.events.post('timer_tick')  # sends the timer_tick system event
        tasks.Task.timer_tick()  # notifies tasks
        tasks.DelayManager.timer_tick(self)
        self.events._process_event_queue()

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

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

        This method is not yet implemented.
        """
        pass

    def quit(self):
        """Performs a graceful exit of MPF."""
        self.log.info("Shutting down...")
        self.events.post('shutdown')
        self.events._process_event_queue()
        self.done = True

    def log_loop_rate(self):
        self.log.info("Target MPF loop rate: %s Hz", timing.Timing.HZ)

        try:
            self.log.info(
                "Actual MPF loop rate: %s Hz",
                round(self.tick_num / (time.time() - self.loop_start_time), 2))
        except ZeroDivisionError:
            self.log.info("Actual MPF loop rate: 0 Hz")

    def _loading_tick(self):
        if not self.asset_loader_complete:

            if AssetManager.loader_queue.qsize():
                self.log.debug(
                    "Holding Attract start while MPF assets load. "
                    "Remaining: %s", AssetManager.loader_queue.qsize())
                self.bcp.bcp_trigger(
                    'assets_to_load',
                    total=AssetManager.total_assets,
                    remaining=AssetManager.loader_queue.qsize())
            else:
                self.bcp.bcp_trigger('assets_to_load',
                                     total=AssetManager.total_assets,
                                     remaining=0)
                self.asset_loader_complete = True

        elif self.bcp.active_connections and not self.flag_bcp_reset_complete:
            if self.tick_num % Timing.HZ == 0:
                self.log.info("Waiting for BCP reset_complete...")

        else:
            self.log.debug("Asset loading complete")
            self._reset_complete()

    def bcp_reset_complete(self):
        self.flag_bcp_reset_complete = True

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

    def configure_debugger(self):
        pass

    def get_debug_status(self, debug_path):

        if (self.options['loglevel'] > 10
                or self.options['consoleloglevel'] > 10):
            return True

        class_, module = debug_path.split('|')

        try:
            if module in self.active_debugger[class_]:
                return True
            else:
                return False
        except KeyError:
            return False

    def set_machine_var(self, name, value, force_events=False):
        """Sets 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:
            if prev_value != value:
                change = True
            else:
                change = False

        if change or force_events:

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

                if self.machine_vars[name]['expire_secs']:
                    disk_var['expire'] = (
                        time.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)

            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):
        """Returns 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):
        if name in self.machine_vars:
            return True
        else:
            return False

    def create_machine_var(self,
                           name,
                           value=0,
                           persist=False,
                           expire_secs=None,
                           silent=False):
        """Creates 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):
        """Removes 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=''):
        """Removes 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 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)
Beispiel #9
0
class SwitchController(object):
    """Base class for the switch controller, which is responsible for receiving
    all switch activity in the machine and converting them into events.

    More info:
    http://missionpinball.com/docs/system-components/switch-controller/

    """

    log = logging.getLogger('SwitchController')

    def __init__(self, machine):
        self.machine = machine
        self.registered_switches = CaseInsensitiveDict()
        # Dictionary of switches and states that have been registered for
        # callbacks.

        self.active_timed_switches = defaultdict(list)
        # Dictionary of switches that are currently in a state counting ms
        # waiting to notify their handlers. In other words, this is the dict that
        # tracks current switches for things like "do foo() if switch bar is
        # active for 100ms."

        self.switches = CaseInsensitiveDict()
        # Dictionary which holds the master list of switches as well as their
        # current states. State here does factor in whether a switch is NO or NC,
        # so 1 = active and 0 = inactive.

        # register for events
        self.machine.events.add_handler('timer_tick', self._tick, 1000)
        self.machine.events.add_handler('init_phase_2',
                                        self._initialize_switches,
                                        1000)
                                        # priority 1000 so this fires first

        self.machine.events.add_handler('machine_reset_phase_3',
                                        self.log_active_switches)

    def _initialize_switches(self):

        # Set "start active" switches

        start_active = list()

        if not self.machine.physical_hw:

            try:
                start_active = Config.string_to_lowercase_list(
                    self.machine.config['virtual platform start active switches'])
            except KeyError:
                pass

        for switch in self.machine.switches:

            # Populate self.switches
            if switch.name in start_active:
                switch.state = 1  # set state based on physical state
            self.set_state(switch.name, switch.state, reset_time=True)

            # Populate self.registered_switches
            self.registered_switches[switch.name + '-0'] = list()
            self.registered_switches[switch.name + '-1'] = list()

    def verify_switches(self):
        """Loops through all the switches and queries their hardware states via
        their platform interfaces and them compares that to the state that MPF
        thinks the switches are in.

        Throws logging warnings if anything doesn't match.

        This method is notification only. It doesn't fix anything.

        """

        for switch in self.machine.switches:
            hw_state = switch.platform.get_switch_state(switch)
            sw_state = self.machine.switches[switch.name].state

            if self.machine.switches[switch.name].type == 'NC':
                sw_state = sw_state ^ 1
            if sw_state != hw_state:
                self.log.warning("Switch State Error! Switch: %s, HW State: "
                                 "%s, MPF State: %s", switch.name, hw_state,
                                 sw_state)

    def is_state(self, switch_name, state, ms=0):
        """Queries whether a switch is in a given state and (optionally)
        whether it has been in that state for the specified number of ms.

        Returns True if the switch_name has been in the state for the given
        number of ms. If ms is not specified, returns True if the switch
        is in the state regardless of how long it's been in that state.

        """

        if self.switches[switch_name]['state'] == state:
            if ms <= self.ms_since_change(switch_name):
                return True
            else:
                return False
        else:
            return False

    def is_active(self, switch_name, ms=None):
        """Queries whether a switch is active.

        Returns True if the current switch is active. If optional arg ms
        is passed, will only return true if switch has been active for that
        many ms.

        Note this method does consider whether a switch is NO or NC. So an NC
        switch will show as active if it is open, rather than closed.
        """

        return self.is_state(switch_name=switch_name,
                             state=1,
                             ms=ms)

    def is_inactive(self, switch_name, ms=None):
        """Queries whether a switch is inactive.

        Returns True if the current switch is inactive. If optional arg
        `ms` is passed, will only return true if switch has been inactive
        for that many ms.

        Note this method does consider whether a switch is NO or NC. So an NC
        switch will show as active if it is closed, rather than open.
        """

        return self.is_state(switch_name=switch_name,
                             state=0,
                             ms=ms)

    def ms_since_change(self, switch_name):
        """Returns the number of ms that have elapsed since this switch
        last changed state.
        """

        return (time.time() - self.switches[switch_name]['time']) * 1000.0

    def secs_since_change(self, switch_name):
        """Returns the number of ms that have elapsed since this switch
        last changed state.
        """

        return time.time() - self.switches[switch_name]['time']

    def set_state(self, switch_name, state=1, reset_time=False):
        """Sets the state of a switch."""

        if reset_time:
            timestamp = 1
        else:
            timestamp = time.time()

        self.switches.update({switch_name: {'state': state,
                                            'time': timestamp
                                            }
                              })

        # todo this method does not set the switch device's state. Either get
        # rid of it, or move the switch device settings from process_switch()
        # to here.

    def process_switch(self, name=None, state=1, logical=False, num=None,
                       obj=None, debounced=True):
        """Processes a new switch state change.

        Args:
            name: The string name of the switch. This is optional if you specify
                the switch via the 'num' or 'obj' parameters.
            state: The state of the switch you're processing, 1 is active, 0 is
                inactive.
            logical: Boolean which specifies whether the 'state' argument
                represents the "physical" or "logical" state of the switch. If
                True, a 1 means this switch is active and a 0 means it's
                inactive, regardless of the NC/NO configuration of the switch.
                If False, then the state paramenter passed will be inverted if
                the switch is configured to be an 'NC' type. Typically the
                hardware will send switch states in their raw (logical=False)
                states, but other interfaces like the keyboard and OSC will use
                logical=True.
            num: The hardware number of the switch.
            obj: The switch object.
            debounced: Whether or not the update for the switch you're sending
                has been debounced or not. Default is True

        Note that there are three different paramter options to specify the
        switch: 'name', 'num', and 'obj'. You only need to pass one of them.

        This is the method that is called by the platform driver whenever a
        switch changes state. It's also used by the "other" modules that
        activate switches, including the keyboard and OSC interfaces.

        State 0 means the switch changed from active to inactive, and 1 means
        it changed from inactive to active. (The hardware & platform code
        handles NC versus NO switches and translates them to 'active' versus
        'inactive'.)

        """

        # Find the switch name
        # todo find a better way to do this ...
        if num is not None:
            for switch in self.machine.switches:
                if switch.number == num:
                    name = switch.name
                    break

        elif obj:
            name = obj.name

        if not name:
            self.log.warning("Received a state change from non-configured "
                             "switch. Number: %s", num)
            return

        # flip the logical & physical states for NC switches
        hw_state = state

        if self.machine.switches[name].type == 'NC':
            if logical:  # NC + logical means hw_state is opposite of state
                hw_state = hw_state ^ 1
            else:
                # NC w/o logical (i.e. hardware state was sent) means logical
                # state is the opposite
                state = state ^ 1

        # If this update is not debounced, only proceed if this switch is
        # configured to not be debounced.

        if not debounced:
            if self.machine.switches[name].config['debounce']:
                return

        # update the switch device
        self.machine.switches[name].state = state
        self.machine.switches[name].hw_state = hw_state

        # if the switch is already in this state, then abort
        if self.switches[name]['state'] == state:
            # todo log this as potential hw error??
            self.log.debug("Received duplicate switch state, which means this "
                           "switch had some non-debounced state changes. This "
                           "could be nothing, but if it happens a lot it could "
                           "indicate noise or interference on the line. Switch:"
                           "%s", name)
            return

        self.log.info("<<<<< switch: %s, State:%s >>>>>", name, state)

        # Update the switch controller's logical state for this switch
        self.set_state(name, state)

        # Combine name & state so we can look it up
        switch_key = str(name) + '-' + str(state)

        # Do we have any registered handlers for this switch/state combo?
        if switch_key in self.registered_switches:
            for entry in self.registered_switches[switch_key]:  # generator?
                # Found an entry.

                if entry['ms']:
                    # This entry is for a timed switch, so add it to our
                    # active timed switch list
                    key = time.time() + (entry['ms'] / 1000.0)
                    value = {'switch_action': str(name) + '-' + str(state),
                             'callback': entry['callback'],
                             'switch_name': name,
                             'state': state,
                             'ms': entry['ms'],
                             'return_info': entry['return_info']}
                    self.active_timed_switches[key].append(value)
                    self.log.debug("Found timed switch handler for k/v %s / %s",
                                   key, value)
                else:
                    # This entry doesn't have a timed delay, so do the action
                    # now
                    if entry['return_info']:

                        entry['callback'](switch_name=name, state=state, ms=0)
                    else:
                        entry['callback']()

                # todo need to add args and kwargs support to callback

        # now check if the opposite state is in the active timed switches list
        # if so, remove it
        for k, v, in self.active_timed_switches.items():
            # using items() instead of iteritems() since we might want to
            # delete while iterating

            for item in v:
                if item['switch_action'] == str(name) + '-' + str(state ^ 1):
                    # ^1 in above line invertes the state
                    del self.active_timed_switches[k]

        self._post_switch_events(name, state)

    def add_switch_handler(self, switch_name, callback, state=1, ms=0,
                           return_info=False):
        """Register a handler to take action on a switch event.

        Args:
            switch_name: String name of the switch you're adding this handler
                for.
            callback: The method you want called when this switch handler fires.
            state: Integer of the state transition you want to callback to be
                triggered on. Default is 1 which means it's called when the
                switch goes from inactive to active, but you can also use 0
                which means your callback will be called when the switch becomes
                inactive
            ms: Integer. If you specify a 'ms' parameter, the handler won't be
                called until the witch is in that state for that many
                milliseconds (rounded up to the nearst machine timer tick).
            return_info: If True, the switch controller will pass the
                parameters of the switch handler as arguments to the callback,
                including switch_name, state, and ms. If False (default), it
                just calls the callback with no parameters.

        You can mix & match entries for the same switch here.

        """

        # todo add support for other parameters to the callback?

        self.log.debug("Registering switch handler: %s, %s, state: %s, ms: %s"
                       ", info: %s", switch_name, callback, state, ms,
                       return_info)

        entry_val = {'ms': ms, 'callback': callback,
                     'return_info': return_info}
        entry_key = str(switch_name) + '-' + str(state)

        self.registered_switches[entry_key].append(entry_val)

        # If the switch handler that was just registered has a delay (i.e. ms>0,
        # then let's see if the switch is currently in the state that the
        # handler was registered for. If so, and if the switch has been in this
        # state for less time than the ms registered, then we need to add this
        # switch to our active_timed_switches list so this handler is called
        # when this switch's active time expires. (in other words, we're
        # catching delayed switches that were in progress when this handler was
        # registered.

        if ms:  # only do this for handlers that have delays
            if state == 1:
                if self.is_active(switch_name, 0) and (
                        self.ms_since_change(switch_name) < ms):
                    # figure out when this handler should fire based on the
                    # switch's original activation time.
                    key = (time.time() + ((ms - self.ms_since_change(switch_name))
                                                                   / 1000.0))
                    value = {'switch_action': entry_key,
                             'callback': callback,
                             'switch_name': switch_name,
                             'state': state,
                             'ms': ms,
                             'return_info': return_info}
                    self.active_timed_switches[key].append(value)
            elif state == 0:
                if self.is_inactive(switch_name, 0) and (
                        self.ms_since_change(switch_name) < ms):

                    key = (time.time() + ((ms - self.ms_since_change(switch_name))
                                                                   / 1000.0))
                    value = {'switch_action': entry_key,
                             'callback': callback,
                             'switch_name': switch_name,
                             'state': state,
                             'ms': ms,
                             'return_info': return_info}
                    self.active_timed_switches[key].append(value)

        # Return the args we used to setup this handler for easy removal later
        return {'switch_name': switch_name,
                'callback': callback,
                'state': state,
                'ms': ms}

    def remove_switch_handler(self, switch_name, callback, state=1, ms=0):
        """Removes a registered switch handler.

        Currently this only works if you specify everything exactly as you set
        it up. (Except for return_info, which doesn't matter if true or false, it
        will remove either / both.

        """

        self.log.debug("Removing switch handler. Switch: %s, State: %s, ms: %s",
                      switch_name, state, ms)

        # Try first with return_info: False
        entry_val = {'ms': ms, 'callback': callback, 'return_info': False}
        entry_key = str(switch_name) + '-' + str(state)

        if entry_val in self.registered_switches[entry_key]:
            self.registered_switches[entry_key].remove(entry_val)

        # And try again with return_info: True
        entry_val = {'ms': ms, 'callback': callback, 'return_info': True}
        if entry_val in self.registered_switches[entry_key]:
            self.registered_switches[entry_key].remove(entry_val)

    def log_active_switches(self):
        """Writes out entries to the log file of all switches that are
        currently active.

        This is used to set the "initial" switch states of standalone testing
        tools, like our log file playback utility, but it might be useful in
        other scenarios when weird things are happening.

        This method dumps these events with logging level "INFO."

        """

        self.log.info("Dumping current active switches")

        for k, v in self.switches.iteritems():
            if v['state']:
                self.log.info("Active Switch|%s",k)

    def _post_switch_events(self, switch_name, state):
        """Posts the game events based on this switch changing state. """

        # post events based on the switch tags

        # the following events all fire the moment a switch goes active
        if state == 1:

            for tag in self.machine.switches[switch_name].tags:
                self.machine.events.post('sw_' + tag)

            for event in self.machine.switches[switch_name].activation_events:
                self.machine.events.post(event)

        # the following events all fire the moment a switch becomes inactive
        elif state == 0:
            for event in self.machine.switches[switch_name].deactivation_events:
                self.machine.events.post(event)

    def _tick(self):
        """Called once per machine tick.

        Checks the current list of active timed switches to see if it's
        time to take action on any of them. If so, does the callback and then
        removes that entry from the list.

        """

        for k in self.active_timed_switches.keys():
            if k <= time.time():  # change to generator?
                for entry in self.active_timed_switches[k]:
                    self.log.debug("Processing timed switch handler. Switch: %s "
                                  " State: %s, ms: %s", entry['switch_name'],
                                  entry['state'], entry['ms'])
                    if entry['return_info']:
                        entry['callback'](switch_name=entry['switch_name'],
                                         state=entry['state'],
                                         ms=entry['ms'])
                    else:
                        entry['callback']()
                del self.active_timed_switches[k]
Beispiel #10
0
    def __init__(self, options):
        self.options = options
        self.log = logging.getLogger("Machine")
        self.log.info("Mission Pinball Framework v%s", version.__version__)
        self.log.debug("Command line arguments: {}".format(self.options))
        self.verify_system_info()

        self.loop_start_time = 0
        self.tick_num = 0
        self.done = False
        self.machine_path = None  # Path to this machine's folder root
        self.monitors = dict()
        self.plugins = list()
        self.scriptlets = list()
        self.modes = list()
        self.asset_managers = dict()
        self.game = None
        self.active_debugger = dict()
        self.machine_vars = CaseInsensitiveDict()
        self.machine_var_monitor = False
        self.machine_var_data_manager = None

        self.flag_bcp_reset_complete = False
        self.asset_loader_complete = False

        self.delay = DelayManager()

        self.crash_queue = Queue.Queue()
        Task.create(self._check_crash_queue)

        FileManager.init()
        self.config = dict()
        self._load_mpf_config()
        self._set_machine_path()
        self._load_machine_config()

        self.configure_debugger()

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

        if not self.options['force_platform']:
            for section, platform in self.config['hardware'].iteritems():
                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'])

        # 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)

        self._load_system_modules()

        # This is called so hw platforms have a change to register for events,
        # and/or anything else they need to do with system modules since
        # they're not set up yet when the hw platforms are constructed.
        for platform in self.hardware_platforms.values():
            platform.initialize()

        self.validate_machine_config_section('machine')
        self.validate_machine_config_section('timing')
        self.validate_machine_config_section('hardware')
        self.validate_machine_config_section('game')

        self._register_system_events()
        self._load_machine_vars()
        self.events.post("init_phase_1")
        self.events._process_event_queue()
        self.events.post("init_phase_2")
        self.events._process_event_queue()
        self._load_plugins()
        self.events.post("init_phase_3")
        self.events._process_event_queue()
        self._load_scriptlets()
        self.events.post("init_phase_4")
        self.events._process_event_queue()
        self.events.post("init_phase_5")
        self.events._process_event_queue()

        self.reset()
Beispiel #11
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: A dictionary of options built from the command line options
            used to launch mpf.py.
        config: A dictionary of machine's configuration settings, merged from
            various sources.
        done: Boolean. Set to True and MPF exits.
        machine_path: The root path of this machine_files folder
        display:
        plugins:
        scriptlets:
        platform:
        events:
    """
    def __init__(self, options):
        self.options = options
        self.log = logging.getLogger("Machine")
        self.log.info("Mission Pinball Framework v%s", version.__version__)
        self.log.debug("Command line arguments: {}".format(self.options))
        self.verify_system_info()

        self.loop_start_time = 0
        self.tick_num = 0
        self.done = False
        self.machine_path = None  # Path to this machine's folder root
        self.monitors = dict()
        self.plugins = list()
        self.scriptlets = list()
        self.modes = list()
        self.asset_managers = dict()
        self.game = None
        self.active_debugger = dict()
        self.machine_vars = CaseInsensitiveDict()
        self.machine_var_monitor = False
        self.machine_var_data_manager = None

        self.flag_bcp_reset_complete = False
        self.asset_loader_complete = False

        self.delay = DelayManager()

        self.crash_queue = Queue.Queue()
        Task.create(self._check_crash_queue)

        FileManager.init()
        self.config = dict()
        self._load_mpf_config()
        self._set_machine_path()
        self._load_machine_config()

        self.configure_debugger()

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

        if not self.options['force_platform']:
            for section, platform in self.config['hardware'].iteritems():
                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'])

        # 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)

        self._load_system_modules()

        # This is called so hw platforms have a change to register for events,
        # and/or anything else they need to do with system modules since
        # they're not set up yet when the hw platforms are constructed.
        for platform in self.hardware_platforms.values():
            platform.initialize()

        self.validate_machine_config_section('machine')
        self.validate_machine_config_section('timing')
        self.validate_machine_config_section('hardware')
        self.validate_machine_config_section('game')

        self._register_system_events()
        self._load_machine_vars()
        self.events.post("init_phase_1")
        self.events._process_event_queue()
        self.events.post("init_phase_2")
        self.events._process_event_queue()
        self._load_plugins()
        self.events.post("init_phase_3")
        self.events._process_event_queue()
        self._load_scriptlets()
        self.events.post("init_phase_4")
        self.events._process_event_queue()
        self.events.post("init_phase_5")
        self.events._process_event_queue()

        self.reset()

    def validate_machine_config_section(self, section):
        if section not in self.config['config_validator']:
            return

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

        self.config[section] = self.config_processor.process_config2(
            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.quit)
        self.events.add_handler(self.config['mpf']['switch_tag_event'].
                                replace('%', 'quit'), self.quit)
        self.events.add_handler('timer_tick', self._loading_tick)

    def _load_machine_vars(self):
        self.machine_var_data_manager = DataManager(self, 'machine_vars')

        current_time = time.time()

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

            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):
        try:
            crash = self.crash_queue.get(block=False)
        except Queue.Empty:
            yield 1000
        else:
            self.log.critical("MPF Shutting down due to child thread crash")
            self.log.critical("Crash details: %s", crash)
            self.done = True

    def _load_mpf_config(self):
        self.config = Config.load_config_file(self.options['mpfconfigfile'])

    def _set_machine_path(self):
        # If the machine folder value passed starts with a forward or
        # backward slash, then we assume it's from the mpf root. Otherwise we
        # assume it's in the mpf/machine_files folder
        if (self.options['machine_path'].startswith('/') or
                self.options['machine_path'].startswith('\\')):
            machine_path = self.options['machine_path']
        else:
            machine_path = os.path.join(self.config['mpf']['paths']
                                        ['machine_files'],
                                        self.options['machine_path'])

        self.machine_path = os.path.abspath(machine_path)
        self.log.debug("Machine path: {}".format(self.machine_path))

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

    def _load_machine_config(self):
        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,
                Config.load_config_file(config_file))

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

        Information includes Python version, Python executable, platform, and
        system architecture.

        """
        python_version = sys.version_info

        if python_version[0] != 2 or python_version[1] != 7:
            self.log.error("Incorrect Python version. MPF requires Python 2.7."
                           "x. You have Python %s.%s.%s.", python_version[0],
                           python_version[1], python_version[2])
            sys.exit()

        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_system_modules(self):
        self.log.info("Loading system modules...")
        for module in self.config['mpf']['system_modules']:
            self.log.debug("Loading '%s' system module", module[1])
            m = self.string_to_class(module[1])(self)
            setattr(self, module[0], m)

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

        # TODO: This should be cleaned up. Create a Plugins superclass 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)

            pluginObj = self.string_to_class(plugin)(self)
            self.plugins.append(pluginObj)

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

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

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

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

                i = __import__(self.config['mpf']['paths']['scriptlets'] + '.'
                               + scriptlet.split('.')[0], fromlist=[''])

                self.scriptlets.append(getattr(i, scriptlet.split('.')[1])
                                       (machine=self,
                                        name=scriptlet.split('.')[1]))

    def _prepare_to_reset(self):
        pass

        # wipe all event handlers

    def reset(self):
        """Resets 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.events.post('Resetting...')
        self.events._process_event_queue()
        self.events.post('machine_reset_phase_1')
        self.events._process_event_queue()
        self.events.post('machine_reset_phase_2')
        self.events._process_event_queue()
        self.events.post('machine_reset_phase_3')
        self.events._process_event_queue()
        self.log.debug('Reset Complete')

    def add_platform(self, name):
        """Makes 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:
            hardware_platform = __import__('mpf.platform.%s' % name,
                                           fromlist=["HardwarePlatform"])

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

    def set_default_platform(self, name):
        """Sets the default platform which is used if a device class-specific or
        device-specific platform is not specified. The default platform also
        controls whether a platform timer or MPF's timer is used.

        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:
            self.log.error("Cannot set default platform to '%s', as that's not"
                           " a currently active platform", name)

    def string_to_class(self, class_string):
        """Converts a string like mpf.system.events.EventManager into a python
        class.

        Args:
            class_string(str): The input string

        Returns:
            A reference to the python class object

        This function came from here:
        http://stackoverflow.com/questions/452969/
        does-python-have-an-equivalent-to-java-class-forname

        """
        parts = class_string.split('.')
        module = ".".join(parts[:-1])
        m = __import__(module)
        for comp in parts[1:]:
            m = getattr(m, comp)
        return m

    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 run(self):
        """Starts the main machine run loop."""
        self.log.debug("Starting the main run loop.")

        self.default_platform.timer_initialize()

        self.loop_start_time = time.time()

        if self.default_platform.features['hw_timer']:
            self.default_platform.run_loop()
        else:
            self._mpf_timer_run_loop()

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

        start_time = time.time()
        loops = 0
        secs_per_tick = timing.Timing.secs_per_tick
        sleep_sec = self.config['timing']['hw_thread_sleep_ms'] / 1000.0

        self.default_platform.next_tick_time = time.time()

        try:
            while self.done is False:
                time.sleep(sleep_sec)
                self.default_platform.tick()
                loops += 1
                if self.default_platform.next_tick_time <= time.time():  # todo change this
                    self.timer_tick()
                    self.default_platform.next_tick_time += secs_per_tick

        except KeyboardInterrupt:
            pass

        self.log_loop_rate()
        self._platform_stop()

        try:
            self.log.info("Hardware loop rate: %s Hz",
                          round(loops / (time.time() - start_time), 2))
        except ZeroDivisionError:
            self.log.info("Hardware loop rate: 0 Hz")

    def timer_tick(self):
        """Called to "tick" MPF at a rate specified by the machine Hz setting.

        This method is called by the MPF run loop or the platform run loop,
        depending on the platform. (Some platforms drive the loop, and others
        let MPF drive.)

        """
        self.tick_num += 1  # used to calculate the loop rate when MPF exits
        self.timing.timer_tick()  # notifies the timing module
        self.events.post('timer_tick')  # sends the timer_tick system event
        tasks.Task.timer_tick()  # notifies tasks
        tasks.DelayManager.timer_tick(self)
        self.events._process_event_queue()

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

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

        This method is not yet implemented.
        """
        pass

    def quit(self):
        """Performs a graceful exit of MPF."""
        self.log.info("Shutting down...")
        self.events.post('shutdown')
        self.events._process_event_queue()
        self.done = True

    def log_loop_rate(self):
        self.log.info("Target MPF loop rate: %s Hz", timing.Timing.HZ)

        try:
            self.log.info("Actual MPF loop rate: %s Hz",
                          round(self.tick_num /
                                (time.time() - self.loop_start_time), 2))
        except ZeroDivisionError:
            self.log.info("Actual MPF loop rate: 0 Hz")

    def _loading_tick(self):
        if not self.asset_loader_complete:

            if AssetManager.loader_queue.qsize():
                self.log.debug("Holding Attract start while MPF assets load. "
                               "Remaining: %s",
                               AssetManager.loader_queue.qsize())
                self.bcp.bcp_trigger('assets_to_load',
                     total=AssetManager.total_assets,
                     remaining=AssetManager.loader_queue.qsize())
            else:
                self.bcp.bcp_trigger('assets_to_load',
                     total=AssetManager.total_assets,
                     remaining=0)
                self.asset_loader_complete = True

        elif self.bcp.active_connections and not self.flag_bcp_reset_complete:
            if self.tick_num % Timing.HZ == 0:
                self.log.info("Waiting for BCP reset_complete...")

        else:
            self.log.debug("Asset loading complete")
            self._reset_complete()

    def bcp_reset_complete(self):
        self.flag_bcp_reset_complete = True

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

    def configure_debugger(self):
        pass

    def get_debug_status(self, debug_path):

        if (self.options['loglevel'] > 10 or
                    self.options['consoleloglevel'] > 10):
            return True

        class_, module = debug_path.split('|')

        try:
            if module in self.active_debugger[class_]:
                return True
            else:
                return False
        except KeyError:
            return False

    def set_machine_var(self, name, value, force_events=False):
        """Sets 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:
            if prev_value != value:
                change = True
            else:
                change = False

        if change or force_events:

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

                if self.machine_vars[name]['expire_secs']:
                    disk_var['expire'] = (time.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)

            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):
        """Returns 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):
        if name in self.machine_vars:
            return True
        else:
            return False

    def create_machine_var(self, name, value=0, persist=False,
                           expire_secs=None, silent=False):
        """Creates 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):
        """Removes 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=''):
        """Removes 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 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)
Beispiel #12
0
class SwitchController(object):
    """Base class for the switch controller, which is responsible for receiving
    all switch activity in the machine and converting them into events.

    More info:
    http://missionpinball.com/docs/system-components/switch-controller/

    """

    log = logging.getLogger('SwitchController')

    def __init__(self, machine):
        self.machine = machine
        self.registered_switches = CaseInsensitiveDict()
        # Dictionary of switches and states that have been registered for
        # callbacks.

        self.active_timed_switches = defaultdict(list)
        # Dictionary of switches that are currently in a state counting ms
        # waiting to notify their handlers. In other words, this is the dict that
        # tracks current switches for things like "do foo() if switch bar is
        # active for 100ms."

        self.switches = CaseInsensitiveDict()
        # Dictionary which holds the master list of switches as well as their
        # current states. State here does factor in whether a switch is NO or NC,
        # so 1 = active and 0 = inactive.

        # register for events
        self.machine.events.add_handler('timer_tick', self._tick, 1000)
        self.machine.events.add_handler('init_phase_2',
                                        self._initialize_switches, 1000)
        # priority 1000 so this fires first

        self.machine.events.add_handler('machine_reset_phase_3',
                                        self.log_active_switches)

    def _initialize_switches(self):

        # Set "start active" switches

        start_active = list()

        if not self.machine.physical_hw:

            try:
                start_active = Config.string_to_lowercase_list(
                    self.machine.
                    config['virtual platform start active switches'])
            except KeyError:
                pass

        for switch in self.machine.switches:

            # Populate self.switches
            if switch.name in start_active:
                switch.state = 1  # set state based on physical state
            self.set_state(switch.name, switch.state, reset_time=True)

            # Populate self.registered_switches
            self.registered_switches[switch.name + '-0'] = list()
            self.registered_switches[switch.name + '-1'] = list()

    def verify_switches(self):
        """Loops through all the switches and queries their hardware states via
        their platform interfaces and them compares that to the state that MPF
        thinks the switches are in.

        Throws logging warnings if anything doesn't match.

        This method is notification only. It doesn't fix anything.

        """

        for switch in self.machine.switches:
            hw_state = switch.platform.get_switch_state(switch)
            sw_state = self.machine.switches[switch.name].state

            if self.machine.switches[switch.name].type == 'NC':
                sw_state = sw_state ^ 1
            if sw_state != hw_state:
                self.log.warning(
                    "Switch State Error! Switch: %s, HW State: "
                    "%s, MPF State: %s", switch.name, hw_state, sw_state)

    def is_state(self, switch_name, state, ms=0):
        """Queries whether a switch is in a given state and (optionally)
        whether it has been in that state for the specified number of ms.

        Returns True if the switch_name has been in the state for the given
        number of ms. If ms is not specified, returns True if the switch
        is in the state regardless of how long it's been in that state.

        """

        if self.switches[switch_name]['state'] == state:
            if ms <= self.ms_since_change(switch_name):
                return True
            else:
                return False
        else:
            return False

    def is_active(self, switch_name, ms=None):
        """Queries whether a switch is active.

        Returns True if the current switch is active. If optional arg ms
        is passed, will only return true if switch has been active for that
        many ms.

        Note this method does consider whether a switch is NO or NC. So an NC
        switch will show as active if it is open, rather than closed.
        """

        return self.is_state(switch_name=switch_name, state=1, ms=ms)

    def is_inactive(self, switch_name, ms=None):
        """Queries whether a switch is inactive.

        Returns True if the current switch is inactive. If optional arg
        `ms` is passed, will only return true if switch has been inactive
        for that many ms.

        Note this method does consider whether a switch is NO or NC. So an NC
        switch will show as active if it is closed, rather than open.
        """

        return self.is_state(switch_name=switch_name, state=0, ms=ms)

    def ms_since_change(self, switch_name):
        """Returns the number of ms that have elapsed since this switch
        last changed state.
        """

        return (time.time() - self.switches[switch_name]['time']) * 1000.0

    def secs_since_change(self, switch_name):
        """Returns the number of ms that have elapsed since this switch
        last changed state.
        """

        return time.time() - self.switches[switch_name]['time']

    def set_state(self, switch_name, state=1, reset_time=False):
        """Sets the state of a switch."""

        if reset_time:
            timestamp = 1
        else:
            timestamp = time.time()

        self.switches.update(
            {switch_name: {
                'state': state,
                'time': timestamp
            }})

        # todo this method does not set the switch device's state. Either get
        # rid of it, or move the switch device settings from process_switch()
        # to here.

    def process_switch(self,
                       name=None,
                       state=1,
                       logical=False,
                       num=None,
                       obj=None,
                       debounced=True):
        """Processes a new switch state change.

        Args:
            name: The string name of the switch. This is optional if you specify
                the switch via the 'num' or 'obj' parameters.
            state: The state of the switch you're processing, 1 is active, 0 is
                inactive.
            logical: Boolean which specifies whether the 'state' argument
                represents the "physical" or "logical" state of the switch. If
                True, a 1 means this switch is active and a 0 means it's
                inactive, regardless of the NC/NO configuration of the switch.
                If False, then the state paramenter passed will be inverted if
                the switch is configured to be an 'NC' type. Typically the
                hardware will send switch states in their raw (logical=False)
                states, but other interfaces like the keyboard and OSC will use
                logical=True.
            num: The hardware number of the switch.
            obj: The switch object.
            debounced: Whether or not the update for the switch you're sending
                has been debounced or not. Default is True

        Note that there are three different paramter options to specify the
        switch: 'name', 'num', and 'obj'. You only need to pass one of them.

        This is the method that is called by the platform driver whenever a
        switch changes state. It's also used by the "other" modules that
        activate switches, including the keyboard and OSC interfaces.

        State 0 means the switch changed from active to inactive, and 1 means
        it changed from inactive to active. (The hardware & platform code
        handles NC versus NO switches and translates them to 'active' versus
        'inactive'.)

        """

        # Find the switch name
        # todo find a better way to do this ...
        if num is not None:
            for switch in self.machine.switches:
                if switch.number == num:
                    name = switch.name
                    break

        elif obj:
            name = obj.name

        if not name:
            self.log.warning(
                "Received a state change from non-configured "
                "switch. Number: %s", num)
            return

        # flip the logical & physical states for NC switches
        hw_state = state

        if self.machine.switches[name].type == 'NC':
            if logical:  # NC + logical means hw_state is opposite of state
                hw_state = hw_state ^ 1
            else:
                # NC w/o logical (i.e. hardware state was sent) means logical
                # state is the opposite
                state = state ^ 1

        # If this update is not debounced, only proceed if this switch is
        # configured to not be debounced.

        if not debounced:
            if self.machine.switches[name].config['debounce']:
                return

        # update the switch device
        self.machine.switches[name].state = state
        self.machine.switches[name].hw_state = hw_state

        # if the switch is already in this state, then abort
        if self.switches[name]['state'] == state:
            # todo log this as potential hw error??
            self.log.debug(
                "Received duplicate switch state, which means this "
                "switch had some non-debounced state changes. This "
                "could be nothing, but if it happens a lot it could "
                "indicate noise or interference on the line. Switch:"
                "%s", name)
            return

        self.log.info("<<<<< switch: %s, State:%s >>>>>", name, state)

        # Update the switch controller's logical state for this switch
        self.set_state(name, state)

        # Combine name & state so we can look it up
        switch_key = str(name) + '-' + str(state)

        # Do we have any registered handlers for this switch/state combo?
        if switch_key in self.registered_switches:
            for entry in self.registered_switches[switch_key]:  # generator?
                # Found an entry.

                if entry['ms']:
                    # This entry is for a timed switch, so add it to our
                    # active timed switch list
                    key = time.time() + (entry['ms'] / 1000.0)
                    value = {
                        'switch_action': str(name) + '-' + str(state),
                        'callback': entry['callback'],
                        'switch_name': name,
                        'state': state,
                        'ms': entry['ms'],
                        'return_info': entry['return_info']
                    }
                    self.active_timed_switches[key].append(value)
                    self.log.debug(
                        "Found timed switch handler for k/v %s / %s", key,
                        value)
                else:
                    # This entry doesn't have a timed delay, so do the action
                    # now
                    if entry['return_info']:

                        entry['callback'](switch_name=name, state=state, ms=0)
                    else:
                        entry['callback']()

                # todo need to add args and kwargs support to callback

        # now check if the opposite state is in the active timed switches list
        # if so, remove it
        for k, v, in self.active_timed_switches.items():
            # using items() instead of iteritems() since we might want to
            # delete while iterating

            for item in v:
                if item['switch_action'] == str(name) + '-' + str(state ^ 1):
                    # ^1 in above line invertes the state
                    del self.active_timed_switches[k]

        self._post_switch_events(name, state)

    def add_switch_handler(self,
                           switch_name,
                           callback,
                           state=1,
                           ms=0,
                           return_info=False):
        """Register a handler to take action on a switch event.

        Args:
            switch_name: String name of the switch you're adding this handler
                for.
            callback: The method you want called when this switch handler fires.
            state: Integer of the state transition you want to callback to be
                triggered on. Default is 1 which means it's called when the
                switch goes from inactive to active, but you can also use 0
                which means your callback will be called when the switch becomes
                inactive
            ms: Integer. If you specify a 'ms' parameter, the handler won't be
                called until the witch is in that state for that many
                milliseconds (rounded up to the nearst machine timer tick).
            return_info: If True, the switch controller will pass the
                parameters of the switch handler as arguments to the callback,
                including switch_name, state, and ms. If False (default), it
                just calls the callback with no parameters.

        You can mix & match entries for the same switch here.

        """

        # todo add support for other parameters to the callback?

        self.log.debug(
            "Registering switch handler: %s, %s, state: %s, ms: %s"
            ", info: %s", switch_name, callback, state, ms, return_info)

        entry_val = {
            'ms': ms,
            'callback': callback,
            'return_info': return_info
        }
        entry_key = str(switch_name) + '-' + str(state)

        self.registered_switches[entry_key].append(entry_val)

        # If the switch handler that was just registered has a delay (i.e. ms>0,
        # then let's see if the switch is currently in the state that the
        # handler was registered for. If so, and if the switch has been in this
        # state for less time than the ms registered, then we need to add this
        # switch to our active_timed_switches list so this handler is called
        # when this switch's active time expires. (in other words, we're
        # catching delayed switches that were in progress when this handler was
        # registered.

        if ms:  # only do this for handlers that have delays
            if state == 1:
                if self.is_active(
                        switch_name,
                        0) and (self.ms_since_change(switch_name) < ms):
                    # figure out when this handler should fire based on the
                    # switch's original activation time.
                    key = (time.time() +
                           ((ms - self.ms_since_change(switch_name)) / 1000.0))
                    value = {
                        'switch_action': entry_key,
                        'callback': callback,
                        'switch_name': switch_name,
                        'state': state,
                        'ms': ms,
                        'return_info': return_info
                    }
                    self.active_timed_switches[key].append(value)
            elif state == 0:
                if self.is_inactive(
                        switch_name,
                        0) and (self.ms_since_change(switch_name) < ms):

                    key = (time.time() +
                           ((ms - self.ms_since_change(switch_name)) / 1000.0))
                    value = {
                        'switch_action': entry_key,
                        'callback': callback,
                        'switch_name': switch_name,
                        'state': state,
                        'ms': ms,
                        'return_info': return_info
                    }
                    self.active_timed_switches[key].append(value)

        # Return the args we used to setup this handler for easy removal later
        return {
            'switch_name': switch_name,
            'callback': callback,
            'state': state,
            'ms': ms
        }

    def remove_switch_handler(self, switch_name, callback, state=1, ms=0):
        """Removes a registered switch handler.

        Currently this only works if you specify everything exactly as you set
        it up. (Except for return_info, which doesn't matter if true or false, it
        will remove either / both.

        """

        self.log.debug(
            "Removing switch handler. Switch: %s, State: %s, ms: %s",
            switch_name, state, ms)

        # Try first with return_info: False
        entry_val = {'ms': ms, 'callback': callback, 'return_info': False}
        entry_key = str(switch_name) + '-' + str(state)

        if entry_val in self.registered_switches[entry_key]:
            self.registered_switches[entry_key].remove(entry_val)

        # And try again with return_info: True
        entry_val = {'ms': ms, 'callback': callback, 'return_info': True}
        if entry_val in self.registered_switches[entry_key]:
            self.registered_switches[entry_key].remove(entry_val)

    def log_active_switches(self):
        """Writes out entries to the log file of all switches that are
        currently active.

        This is used to set the "initial" switch states of standalone testing
        tools, like our log file playback utility, but it might be useful in
        other scenarios when weird things are happening.

        This method dumps these events with logging level "INFO."

        """

        self.log.info("Dumping current active switches")

        for k, v in self.switches.iteritems():
            if v['state']:
                self.log.info("Active Switch|%s", k)

    def _post_switch_events(self, switch_name, state):
        """Posts the game events based on this switch changing state. """

        # post events based on the switch tags

        # the following events all fire the moment a switch goes active
        if state == 1:

            for tag in self.machine.switches[switch_name].tags:
                self.machine.events.post('sw_' + tag)

            for event in self.machine.switches[switch_name].activation_events:
                self.machine.events.post(event)

        # the following events all fire the moment a switch becomes inactive
        elif state == 0:
            for event in self.machine.switches[
                    switch_name].deactivation_events:
                self.machine.events.post(event)

    def _tick(self):
        """Called once per machine tick.

        Checks the current list of active timed switches to see if it's
        time to take action on any of them. If so, does the callback and then
        removes that entry from the list.

        """

        for k in self.active_timed_switches.keys():
            if k <= time.time():  # change to generator?
                for entry in self.active_timed_switches[k]:
                    self.log.debug(
                        "Processing timed switch handler. Switch: %s "
                        " State: %s, ms: %s", entry['switch_name'],
                        entry['state'], entry['ms'])
                    if entry['return_info']:
                        entry['callback'](switch_name=entry['switch_name'],
                                          state=entry['state'],
                                          ms=entry['ms'])
                    else:
                        entry['callback']()
                del self.active_timed_switches[k]
Beispiel #13
0
    def __init__(self, options):
        self.options = options

        self.log = logging.getLogger("MediaController")
        self.log.info("Media Controller Version %s", version.__version__)
        self.log.info("Backbox Control Protocol Version %s",
                      version.__bcp_version__)
        self.log.info("Config File Version %s",
                      version.__config_version__)

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

        self.config = dict()
        self.done = False  # todo
        self.machine_path = None
        self.asset_managers = dict()
        self.num_assets_to_load = 0
        self.window = None
        self.window_manager = None
        self.pygame = False
        self.pygame_requested = False
        self.registered_pygame_handlers = dict()
        self.pygame_allowed_events = list()
        self.socket_thread = None
        self.receive_queue = Queue.Queue()
        self.sending_queue = Queue.Queue()
        self.crash_queue = Queue.Queue()
        self.game_modes = CaseInsensitiveDict()
        self.player_list = list()
        self.player = None
        self.HZ = 0
        self.next_tick_time = 0
        self.secs_per_tick = 0

        Task.Create(self._check_crash_queue)

        self.bcp_commands = {'hello': self.bcp_hello,
                             'goodbye': self.bcp_goodbye,
                             'reset': self.reset,
                             'mode_start': self.bcp_mode_start,
                             'mode_stop': self.bcp_mode_stop,
                             'error': self.bcp_error,
                             'ball_start': self.bcp_ball_start,
                             'ball_end': self.bcp_ball_end,
                             'game_start': self.bcp_game_start,
                             'game_end': self.bcp_game_end,
                             'player_added': self.bcp_player_add,
                             'player_variable': self.bcp_player_variable,
                             'player_score': self.bcp_player_score,
                             'player_turn_start': self.bcp_player_turn_start,
                             'attract_start': self.bcp_attract_start,
                             'attract_stop': self.bcp_attract_stop,
                             'trigger': self.bcp_trigger,
                             'switch': self.bcp_switch,
                             'get': self.bcp_get,
                             'set': self.bcp_set,
                             'config': self.bcp_config,
                             'timer': self.bcp_timer
                            }

        # load the MPF config & machine defaults
        self.config = (
            Config.load_config_yaml(config=self.config,
                                    yaml_file=self.options['mcconfigfile']))

        # Find the machine_files location. If it starts with a forward or
        # backward slash, then we assume it's from the mpf root. Otherwise we
        # assume it's from the subfolder location specified in the
        # mpfconfigfile location

        if (options['machinepath'].startswith('/') or
                options['machinepath'].startswith('\\')):
            machine_path = options['machinepath']
        else:
            machine_path = os.path.join(self.config['mediacontroller']['paths']
                                        ['machine_files'],
                                        options['machinepath'])

        self.machine_path = os.path.abspath(machine_path)

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

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

        # Now find the config file location. Same as machine_file with the
        # slash uses to specify an absolute path

        if (options['configfile'].startswith('/') or
                options['configfile'].startswith('\\')):
            config_file = options['configfile']
        else:

            if not options['configfile'].endswith('.yaml'):
                options['configfile'] += '.yaml'

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

        self.log.info("Base machine config file: %s", config_file)

        # Load the machine-specific config
        self.config = Config.load_config_yaml(config=self.config,
                                              yaml_file=config_file)

        mediacontroller_config_spec = '''
                        exit_on_disconnect: boolean|True
                        port: int|5050
                        '''

        self.config['mediacontroller'] = (
            Config.process_config(mediacontroller_config_spec,
                                  self.config['mediacontroller']))

        self.events = EventManager(self)
        self.timing = Timing(self)

        # Load the media controller modules
        self.config['mediacontroller']['modules'] = (
            self.config['mediacontroller']['modules'].split(' '))
        for module in self.config['mediacontroller']['modules']:
            self.log.info("Loading module: %s", module)
            module_parts = module.split('.')
            exec('self.' + module_parts[0] + '=' + module + '(self)')

            # todo there's probably a more pythonic way to do this, and I know
            # exec() is supposedly unsafe, but meh, if you have access to put
            # malicious files in the system folder then you have access to this
            # code too.

        self.start_socket_thread()

        self.events.post("init_phase_1")
        self.events.post("init_phase_2")
        self.events.post("init_phase_3")
        self.events.post("init_phase_4")
        self.events.post("init_phase_5")

        self.reset()
Beispiel #14
0
    def __init__(self, options):
        self.options = options

        self.log = logging.getLogger("MediaController")
        self.log.debug("Command line arguments: {}".format(self.options))
        self.log.info("Media Controller Version %s", version.__version__)
        self.log.debug("Backbox Control Protocol Version %s",
                      version.__bcp_version__)
        self.log.debug("Config File Version %s",
                      version.__config_version__)

        python_version = sys.version_info

        if python_version[0] != 2 or python_version[1] != 7:
            self.log.error("Incorrect Python version. MPF requires Python 2.7."
                           "x. You have Python %s.%s.%s.", python_version[0],
                           python_version[1], python_version[2])
            sys.exit()

        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)

        self.active_debugger = dict()

        self.config = dict()
        self.done = False  # todo
        self.machine_path = None
        self.asset_managers = dict()
        self.window = None
        self.window_manager = None
        self.pygame = False
        self.pygame_requested = False
        self.registered_pygame_handlers = dict()
        self.pygame_allowed_events = list()
        self.socket_thread = None
        self.receive_queue = Queue.Queue()
        self.sending_queue = Queue.Queue()
        self.crash_queue = Queue.Queue()
        self.modes = CaseInsensitiveDict()
        self.player_list = list()
        self.player = None
        self.HZ = 0
        self.next_tick_time = 0
        self.secs_per_tick = 0
        self.machine_vars = CaseInsensitiveDict()
        self.machine_var_monitor = False
        self.tick_num = 0
        self.delay = DelayManager()

        self._pc_assets_to_load = 0
        self._pc_total_assets = 0
        self.pc_connected = False

        Task.create(self._check_crash_queue)

        self.bcp_commands = {'ball_start': self.bcp_ball_start,
                             'ball_end': self.bcp_ball_end,
                             'config': self.bcp_config,
                             'error': self.bcp_error,
                             'get': self.bcp_get,
                             'goodbye': self.bcp_goodbye,
                             'hello': self.bcp_hello,
                             'machine_variable': self.bcp_machine_variable,
                             'mode_start': self.bcp_mode_start,
                             'mode_stop': self.bcp_mode_stop,
                             'player_added': self.bcp_player_add,
                             'player_score': self.bcp_player_score,
                             'player_turn_start': self.bcp_player_turn_start,
                             'player_variable': self.bcp_player_variable,
                             'reset': self.reset,
                             'set': self.bcp_set,
                             'shot': self.bcp_shot,
                             'switch': self.bcp_switch,
                             'timer': self.bcp_timer,
                             'trigger': self.bcp_trigger,
                            }

        FileManager.init()
        self.config = dict()
        self._load_mc_config()
        self._set_machine_path()
        self._load_machine_config()

        # Find the machine_files location. If it starts with a forward or
        # backward slash, then we assume it's from the mpf root. Otherwise we
        # assume it's from the subfolder location specified in the
        # mpfconfig file location

        if (options['machine_path'].startswith('/') or
                options['machine_path'].startswith('\\')):
            machine_path = options['machine_path']
        else:
            machine_path = os.path.join(self.config['media_controller']['paths']
                                        ['machine_files'],
                                        options['machine_path'])

        self.machine_path = os.path.abspath(machine_path)

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

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

        mediacontroller_config_spec = '''
                        exit_on_disconnect: boolean|True
                        port: int|5050
                        '''

        self.config['media_controller'] = (
            Config.process_config(mediacontroller_config_spec,
                                  self.config['media_controller']))

        self.events = EventManager(self, setup_event_player=False)
        self.timing = Timing(self)

        # Load the media controller modules
        self.config['media_controller']['modules'] = (
            self.config['media_controller']['modules'].split(' '))
        self.log.info("Loading Modules...")
        for module in self.config['media_controller']['modules']:
            self.log.debug("Loading module: %s", module)
            module_parts = module.split('.')
            exec('self.' + module_parts[0] + '=' + module + '(self)')

            # todo there's probably a more pythonic way to do this, and I know
            # exec() is supposedly unsafe, but meh, if you have access to put
            # malicious files in the system folder then you have access to this
            # code too.

        self.start_socket_thread()

        self.events.post("init_phase_1")
        self.events._process_event_queue()
        self.events.post("init_phase_2")
        self.events._process_event_queue()
        self.events.post("init_phase_3")
        self.events._process_event_queue()
        self.events.post("init_phase_4")
        self.events._process_event_queue()
        self.events.post("init_phase_5")
        self.events._process_event_queue()

        self.reset()