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)
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_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
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)
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]
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)
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]
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()
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()